Merge remote-tracking branch 'remote/master' into feature/add-xml-support-to-http-monitors

pull/2610/head
Faruk Genç 2 years ago
commit 86ba6f829e

@ -32,6 +32,10 @@ if (! exists) {
process.exit(1); process.exit(1);
} }
/**
* Commit updated files
* @param {string} version Version to update to
*/
function commit(version) { function commit(version) {
let msg = "Update to " + version; let msg = "Update to " + version;
@ -47,6 +51,10 @@ function commit(version) {
console.log(res.stdout.toString().trim()); console.log(res.stdout.toString().trim());
} }
/**
* Create a tag with the specified version
* @param {string} version Tag to create
*/
function tag(version) { function tag(version) {
let res = childProcess.spawnSync("git", [ "tag", version ]); let res = childProcess.spawnSync("git", [ "tag", version ]);
console.log(res.stdout.toString().trim()); console.log(res.stdout.toString().trim());
@ -55,6 +63,11 @@ function tag(version) {
console.log(res.stdout.toString().trim()); console.log(res.stdout.toString().trim());
} }
/**
* Check if a tag exists for the specified version
* @param {string} version Version to check
* @returns {boolean} Does the tag already exist
*/
function tagExists(version) { function tagExists(version) {
if (! version) { if (! version) {
throw new Error("invalid version"); throw new Error("invalid version");

@ -25,6 +25,10 @@ if (platform === "linux/amd64") {
const file = fs.createWriteStream("cloudflared.deb"); const file = fs.createWriteStream("cloudflared.deb");
get("https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-" + arch + ".deb"); get("https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-" + arch + ".deb");
/**
* Download specified file
* @param {string} url URL to request
*/
function get(url) { function get(url) {
http.get(url, function (res) { http.get(url, function (res) {
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) { if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {

@ -43,6 +43,11 @@ const main = async () => {
console.log("Finished."); console.log("Finished.");
}; };
/**
* Ask question of user
* @param {string} question Question to ask
* @returns {Promise<string>} Users response
*/
function question(question) { function question(question) {
return new Promise((resolve) => { return new Promise((resolve) => {
rl.question(question, (answer) => { rl.question(question, (answer) => {

@ -53,6 +53,11 @@ const main = async () => {
console.log("Finished."); console.log("Finished.");
}; };
/**
* Ask question of user
* @param {string} question Question to ask
* @returns {Promise<string>} Users response
*/
function question(question) { function question(question) {
return new Promise((resolve) => { return new Promise((resolve) => {
rl.question(question, (answer) => { rl.question(question, (answer) => {

@ -135,6 +135,11 @@ server.listen({
udp: 5300 udp: 5300
}); });
/**
* Get human readable request type from request code
* @param {number} code Request code to translate
* @returns {string} Human readable request type
*/
function type(code) { function type(code) {
for (let name in Packet.TYPE) { for (let name in Packet.TYPE) {
if (Packet.TYPE[name] === code) { if (Packet.TYPE[name] === code) {

@ -11,6 +11,7 @@ class SimpleMqttServer {
this.port = port; this.port = port;
} }
/** Start the MQTT server */
start() { start() {
this.server.listen(this.port, () => { this.server.listen(this.port, () => {
console.log("server started and listening on port ", this.port); console.log("server started and listening on port ", this.port);

@ -36,10 +36,8 @@ if (! exists) {
} }
/** /**
* Updates the version number in package.json and commits it to git. * Commit updated files
* @param {string} version - The new version number * @param {string} version Version to update to
*
* Generated by Trelent
*/ */
function commit(version) { function commit(version) {
let msg = "Update to " + version; let msg = "Update to " + version;
@ -53,16 +51,19 @@ function commit(version) {
} }
} }
/**
* Create a tag with the specified version
* @param {string} version Tag to create
*/
function tag(version) { function tag(version) {
let res = childProcess.spawnSync("git", [ "tag", version ]); let res = childProcess.spawnSync("git", [ "tag", version ]);
console.log(res.stdout.toString().trim()); console.log(res.stdout.toString().trim());
} }
/** /**
* Checks if a given version is already tagged in the git repository. * Check if a tag exists for the specified version
* @param {string} version - The version to check for. * @param {string} version Version to check
* * @returns {boolean} Does the tag already exist
* Generated by Trelent
*/ */
function tagExists(version) { function tagExists(version) {
if (! version) { if (! version) {

@ -10,6 +10,10 @@ if (!newVersion) {
updateWiki(newVersion); updateWiki(newVersion);
/**
* Update the wiki with new version number
* @param {string} newVersion Version to update to
*/
function updateWiki(newVersion) { function updateWiki(newVersion) {
const wikiDir = "./tmp/wiki"; const wikiDir = "./tmp/wiki";
const howToUpdateFilename = "./tmp/wiki/🆙-How-to-Update.md"; const howToUpdateFilename = "./tmp/wiki/🆙-How-to-Update.md";
@ -39,6 +43,10 @@ function updateWiki(newVersion) {
safeDelete(wikiDir); safeDelete(wikiDir);
} }
/**
* Check if a directory exists and then delete it
* @param {string} dir Directory to delete
*/
function safeDelete(dir) { function safeDelete(dir) {
if (fs.existsSync(dir)) { if (fs.existsSync(dir)) {
fs.rm(dir, { fs.rm(dir, {

15066
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -1,6 +1,6 @@
{ {
"name": "uptime-kuma", "name": "uptime-kuma",
"version": "1.19.4", "version": "1.19.6",
"license": "MIT", "license": "MIT",
"repository": { "repository": {
"type": "git", "type": "git",
@ -39,7 +39,7 @@
"build-docker-nightly-amd64": "docker buildx build -f docker/dockerfile --platform linux/amd64 -t louislam/uptime-kuma:nightly-amd64 --target nightly . --push --progress plain", "build-docker-nightly-amd64": "docker buildx build -f docker/dockerfile --platform linux/amd64 -t louislam/uptime-kuma:nightly-amd64 --target nightly . --push --progress plain",
"build-docker-pr-test": "docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64 -t louislam/uptime-kuma:pr-test --target pr-test . --push", "build-docker-pr-test": "docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64 -t louislam/uptime-kuma:pr-test --target pr-test . --push",
"upload-artifacts": "docker buildx build -f docker/dockerfile --platform linux/amd64 -t louislam/uptime-kuma:upload-artifact --build-arg VERSION --build-arg GITHUB_TOKEN --target upload-artifact . --progress plain", "upload-artifacts": "docker buildx build -f docker/dockerfile --platform linux/amd64 -t louislam/uptime-kuma:upload-artifact --build-arg VERSION --build-arg GITHUB_TOKEN --target upload-artifact . --progress plain",
"setup": "git checkout 1.19.4 && npm ci --production && npm run download-dist", "setup": "git checkout 1.19.6 && npm ci --production && npm run download-dist",
"download-dist": "node extra/download-dist.js", "download-dist": "node extra/download-dist.js",
"mark-as-nightly": "node extra/mark-as-nightly.js", "mark-as-nightly": "node extra/mark-as-nightly.js",
"reset-password": "node extra/reset-password.js", "reset-password": "node extra/reset-password.js",
@ -67,7 +67,7 @@
}, },
"dependencies": { "dependencies": {
"@grpc/grpc-js": "~1.7.3", "@grpc/grpc-js": "~1.7.3",
"@louislam/ping": "~0.4.2-mod.0", "@louislam/ping": "~0.4.2-mod.1",
"@louislam/sqlite3": "15.1.2", "@louislam/sqlite3": "15.1.2",
"args-parser": "~1.3.0", "args-parser": "~1.3.0",
"axios": "~0.27.0", "axios": "~0.27.0",
@ -96,6 +96,7 @@
"jsonwebtoken": "~9.0.0", "jsonwebtoken": "~9.0.0",
"jwt-decode": "~3.1.2", "jwt-decode": "~3.1.2",
"limiter": "~2.1.0", "limiter": "~2.1.0",
"mongodb": "~4.13.0",
"mqtt": "~4.3.7", "mqtt": "~4.3.7",
"mssql": "~8.1.4", "mssql": "~8.1.4",
"mysql2": "~2.3.3", "mysql2": "~2.3.3",
@ -109,7 +110,7 @@
"prom-client": "~13.2.0", "prom-client": "~13.2.0",
"prometheus-api-metrics": "~3.2.1", "prometheus-api-metrics": "~3.2.1",
"protobufjs": "~7.1.1", "protobufjs": "~7.1.1",
"redbean-node": "0.1.4", "redbean-node": "~0.2.0",
"redis": "~4.5.1", "redis": "~4.5.1",
"socket.io": "~4.5.3", "socket.io": "~4.5.3",
"socket.io-client": "~4.5.3", "socket.io-client": "~4.5.3",

@ -63,6 +63,12 @@ function myAuthorizer(username, password, callback) {
}); });
} }
/**
* Use basic auth if auth is not disabled
* @param {express.Request} req Express request object
* @param {express.Response} res Express response object
* @param {express.NextFunction} next
*/
exports.basicAuth = async function (req, res, next) { exports.basicAuth = async function (req, res, next) {
const middleware = basicAuth({ const middleware = basicAuth({
authorizer: myAuthorizer, authorizer: myAuthorizer,

@ -37,6 +37,10 @@ class CacheableDnsHttpAgent {
this.enable = isEnable; this.enable = isEnable;
} }
/**
* Attach cacheable to HTTP agent
* @param {http.Agent} agent Agent to install
*/
static install(agent) { static install(agent) {
this.cacheable.install(agent); this.cacheable.install(agent);
} }

@ -32,6 +32,7 @@ const initBackgroundJobs = function (args) {
return bree; return bree;
}; };
/** Stop all background jobs if running */
const stopBackgroundJobs = function () { const stopBackgroundJobs = function () {
if (bree) { if (bree) {
bree.stop(); bree.stop();

@ -112,6 +112,11 @@ class Maintenance extends BeanModel {
return this.toPublicJSON(timezone); return this.toPublicJSON(timezone);
} }
/**
* Get a list of weekdays that the maintenance is active for
* Monday=1, Tuesday=2 etc.
* @returns {number[]} Array of active weekdays
*/
getDayOfWeekList() { getDayOfWeekList() {
log.debug("timeslot", "List: " + this.weekdays); log.debug("timeslot", "List: " + this.weekdays);
return JSON.parse(this.weekdays).sort(function (a, b) { return JSON.parse(this.weekdays).sort(function (a, b) {
@ -119,12 +124,20 @@ class Maintenance extends BeanModel {
}); });
} }
/**
* Get a list of days in month that maintenance is active for
* @returns {number[]} Array of active days in month
*/
getDayOfMonthList() { getDayOfMonthList() {
return JSON.parse(this.days_of_month).sort(function (a, b) { return JSON.parse(this.days_of_month).sort(function (a, b) {
return a - b; return a - b;
}); });
} }
/**
* Get the start date and time for maintenance
* @returns {dayjs.Dayjs} Start date and time
*/
getStartDateTime() { getStartDateTime() {
let startOfTheDay = dayjs.utc(this.start_date).format("HH:mm"); let startOfTheDay = dayjs.utc(this.start_date).format("HH:mm");
log.debug("timeslot", "startOfTheDay: " + startOfTheDay); log.debug("timeslot", "startOfTheDay: " + startOfTheDay);
@ -137,6 +150,10 @@ class Maintenance extends BeanModel {
return dayjs.utc(this.start_date).add(startTimeSecond, "second"); return dayjs.utc(this.start_date).add(startTimeSecond, "second");
} }
/**
* Get the duraction of maintenance in seconds
* @returns {number} Duration of maintenance
*/
getDuration() { getDuration() {
let duration = dayjs.utc(this.end_time, "HH:mm").diff(dayjs.utc(this.start_time, "HH:mm"), "second"); let duration = dayjs.utc(this.end_time, "HH:mm").diff(dayjs.utc(this.start_time, "HH:mm"), "second");
// Add 24hours if it is across day // Add 24hours if it is across day
@ -146,6 +163,12 @@ class Maintenance extends BeanModel {
return duration; return duration;
} }
/**
* 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
*/
static jsonToBean(bean, obj) { static jsonToBean(bean, obj) {
if (obj.id) { if (obj.id) {
bean.id = obj.id; bean.id = obj.id;

@ -6,6 +6,11 @@ const { UptimeKumaServer } = require("../uptime-kuma-server");
class MaintenanceTimeslot extends BeanModel { class MaintenanceTimeslot extends BeanModel {
/**
* Return an object that ready to parse to JSON for public
* Only show necessary data to public
* @returns {Object}
*/
async toPublicJSON() { async toPublicJSON() {
const serverTimezoneOffset = UptimeKumaServer.getInstance().getTimezoneOffset(); const serverTimezoneOffset = UptimeKumaServer.getInstance().getTimezoneOffset();
@ -21,6 +26,10 @@ class MaintenanceTimeslot extends BeanModel {
return obj; return obj;
} }
/**
* Return an object that ready to parse to JSON
* @returns {Object}
*/
async toJSON() { async toJSON() {
return await this.toPublicJSON(); return await this.toPublicJSON();
} }

@ -4,7 +4,7 @@ const axios = require("axios");
const { Prometheus } = require("../prometheus"); const { Prometheus } = require("../prometheus");
const { log, UP, DOWN, PENDING, MAINTENANCE, flipStatus, TimeLogger, MAX_INTERVAL_SECOND, MIN_INTERVAL_SECOND } = require("../../src/util"); const { log, UP, DOWN, PENDING, MAINTENANCE, flipStatus, TimeLogger, MAX_INTERVAL_SECOND, MIN_INTERVAL_SECOND } = require("../../src/util");
const { tcping, ping, dnsResolve, checkCertificate, checkStatusCode, getTotalClientInRoom, setting, mssqlQuery, postgresQuery, mysqlQuery, mqttAsync, setSetting, httpNtlm, radius, grpcQuery, const { tcping, ping, dnsResolve, checkCertificate, checkStatusCode, getTotalClientInRoom, setting, mssqlQuery, postgresQuery, mysqlQuery, mqttAsync, setSetting, httpNtlm, radius, grpcQuery,
redisPingAsync redisPingAsync, mongodbPing,
} = require("../util-server"); } = require("../util-server");
const { R } = require("redbean-node"); const { R } = require("redbean-node");
const { BeanModel } = require("redbean-node/dist/bean-model"); const { BeanModel } = require("redbean-node/dist/bean-model");
@ -38,7 +38,6 @@ class Monitor extends BeanModel {
id: this.id, id: this.id,
name: this.name, name: this.name,
sendUrl: this.sendUrl, sendUrl: this.sendUrl,
maintenance: await Monitor.isUnderMaintenance(this.id),
}; };
if (this.sendUrl) { if (this.sendUrl) {
@ -509,14 +508,18 @@ class Monitor extends BeanModel {
const options = { const options = {
url: `/containers/${this.docker_container}/json`, url: `/containers/${this.docker_container}/json`,
timeout: this.interval * 1000 * 0.8,
headers: { headers: {
"Accept": "*/*", "Accept": "*/*",
"User-Agent": "Uptime-Kuma/" + version, "User-Agent": "Uptime-Kuma/" + version,
}, },
httpsAgent: new https.Agent({ httpsAgent: CacheableDnsHttpAgent.getHttpsAgent({
maxCachedSessions: 0, // Use Custom agent to disable session reuse (https://github.com/nodejs/node/issues/3940) maxCachedSessions: 0, // Use Custom agent to disable session reuse (https://github.com/nodejs/node/issues/3940)
rejectUnauthorized: !this.getIgnoreTls(), rejectUnauthorized: !this.getIgnoreTls(),
}), }),
httpAgent: CacheableDnsHttpAgent.getHttpAgent({
maxCachedSessions: 0,
}),
}; };
if (dockerHost._dockerType === "socket") { if (dockerHost._dockerType === "socket") {
@ -596,6 +599,15 @@ class Monitor extends BeanModel {
bean.msg = ""; bean.msg = "";
bean.status = UP; bean.status = UP;
bean.ping = dayjs().valueOf() - startTime; 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") { } else if (this.type === "radius") {
let startTime = dayjs().valueOf(); let startTime = dayjs().valueOf();
@ -769,6 +781,13 @@ class Monitor extends BeanModel {
} }
} }
/**
* Make a request using axios
* @param {Object} options Options for Axios
* @param {boolean} finalCall Should this be the final call i.e
* don't retry on faliure
* @returns {Object} Axios response
*/
async makeAxiosRequest(options, finalCall = false) { async makeAxiosRequest(options, finalCall = false) {
try { try {
let res; let res;
@ -1250,6 +1269,7 @@ class Monitor extends BeanModel {
return maintenance.count !== 0; return maintenance.count !== 0;
} }
/** Make sure monitor interval is between bounds */
validate() { validate() {
if (this.interval > MAX_INTERVAL_SECOND) { if (this.interval > MAX_INTERVAL_SECOND) {
throw new Error(`Interval cannot be more than ${MAX_INTERVAL_SECOND} seconds`); throw new Error(`Interval cannot be more than ${MAX_INTERVAL_SECOND} seconds`);

@ -8,6 +8,14 @@ class PromoSMS extends NotificationProvider {
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) { async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
let okMsg = "Sent Successfully."; let okMsg = "Sent Successfully.";
if (notification.promosmsAllowLongSMS === undefined) {
notification.promosmsAllowLongSMS = false;
}
//TODO: Add option for enabling special characters. It will decrese message max length from 160 to 70 chars.
//Lets remove non ascii char
let cleanMsg = msg.replace(/[^\x00-\x7F]/g, "");
try { try {
let config = { let config = {
headers: { headers: {
@ -18,8 +26,9 @@ class PromoSMS extends NotificationProvider {
}; };
let data = { let data = {
"recipients": [ notification.promosmsPhoneNumber ], "recipients": [ notification.promosmsPhoneNumber ],
//Lets remove non ascii char //Trim message to maximum length of 1 SMS or 4 if we allowed long messages
"text": msg.replace(/[^\x00-\x7F]/g, ""), "text": notification.promosmsAllowLongSMS ? cleanMsg.substring(0, 639) : cleanMsg.substring(0, 159),
"long-sms": notification.promosmsAllowLongSMS,
"type": Number(notification.promosmsSMSType), "type": Number(notification.promosmsSMSType),
"sender": notification.promosmsSenderName "sender": notification.promosmsSenderName
}; };

@ -21,6 +21,12 @@ class ServerChan extends NotificationProvider {
} }
} }
/**
* Get the formatted title for message
* @param {?Object} monitorJSON Monitor details (For Up/Down only)
* @param {?Object} heartbeatJSON Heartbeat details (For Up/Down only)
* @returns {string} Formatted title
*/
checkStatus(heartbeatJSON, monitorJSON) { checkStatus(heartbeatJSON, monitorJSON) {
let title = "UptimeKuma Message"; let title = "UptimeKuma Message";
if (heartbeatJSON != null && heartbeatJSON["status"] === UP) { if (heartbeatJSON != null && heartbeatJSON["status"] === UP) {

@ -0,0 +1,113 @@
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
const { UP, DOWN, getMonitorRelativeURL } = require("../../src/util");
const { setting } = require("../util-server");
let successMessage = "Sent Successfully.";
class Splunk extends NotificationProvider {
name = "Splunk";
/**
* @inheritdoc
*/
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
try {
if (heartbeatJSON == null) {
const title = "Uptime Kuma Alert";
const monitor = {
type: "ping",
url: "Uptime Kuma Test Button",
};
return this.postNotification(notification, title, msg, monitor, "trigger");
}
if (heartbeatJSON.status === UP) {
const title = "Uptime Kuma Monitor ✅ Up";
return this.postNotification(notification, title, heartbeatJSON.msg, monitorJSON, "recovery");
}
if (heartbeatJSON.status === DOWN) {
const title = "Uptime Kuma Monitor 🔴 Down";
return this.postNotification(notification, title, heartbeatJSON.msg, monitorJSON, "trigger");
}
} catch (error) {
this.throwGeneralAxiosError(error);
}
}
/**
* Check if result is successful, result code should be in range 2xx
* @param {Object} result Axios response object
* @throws {Error} The status code is not in range 2xx
*/
checkResult(result) {
if (result.status == null) {
throw new Error("Splunk notification failed with invalid response!");
}
if (result.status < 200 || result.status >= 300) {
throw new Error("Splunk notification failed with status code " + result.status);
}
}
/**
* Send the message
* @param {BeanModel} notification Message title
* @param {string} title Message title
* @param {string} body Message
* @param {Object} monitorInfo Monitor details (For Up/Down only)
* @param {?string} eventAction Action event for PagerDuty (trigger, acknowledge, resolve)
* @returns {string}
*/
async postNotification(notification, title, body, monitorInfo, eventAction = "trigger") {
let monitorUrl;
if (monitorInfo.type === "port") {
monitorUrl = monitorInfo.hostname;
if (monitorInfo.port) {
monitorUrl += ":" + monitorInfo.port;
}
} else if (monitorInfo.hostname != null) {
monitorUrl = monitorInfo.hostname;
} else {
monitorUrl = monitorInfo.url;
}
if (eventAction === "recovery") {
if (notification.splunkAutoResolve === "0") {
return "No action required";
}
eventAction = notification.splunkAutoResolve;
} else {
eventAction = notification.splunkSeverity;
}
const options = {
method: "POST",
url: notification.splunkRestURL,
headers: { "Content-Type": "application/json" },
data: {
message_type: eventAction,
state_message: `[${title}] [${monitorUrl}] ${body}`,
entity_display_name: "Uptime Kuma Alert: " + monitorInfo.name,
routing_key: notification.pagerdutyIntegrationKey,
entity_id: "Uptime Kuma/" + monitorInfo.id,
}
};
const baseURL = await setting("primaryBaseURL");
if (baseURL && monitorInfo) {
options.client = "Uptime Kuma";
options.client_url = baseURL + getMonitorRelativeURL(monitorInfo.id);
}
let result = await axios.request(options);
this.checkResult(result);
if (result.statusText != null) {
return "Splunk notification succeed: " + result.statusText;
}
return successMessage;
}
}
module.exports = Splunk;

@ -40,6 +40,7 @@ const Stackfield = require("./notification-providers/stackfield");
const Teams = require("./notification-providers/teams"); const Teams = require("./notification-providers/teams");
const TechulusPush = require("./notification-providers/techulus-push"); const TechulusPush = require("./notification-providers/techulus-push");
const Telegram = require("./notification-providers/telegram"); const Telegram = require("./notification-providers/telegram");
const Splunk = require("./notification-providers/splunk");
const Webhook = require("./notification-providers/webhook"); const Webhook = require("./notification-providers/webhook");
const WeCom = require("./notification-providers/wecom"); const WeCom = require("./notification-providers/wecom");
const GoAlert = require("./notification-providers/goalert"); const GoAlert = require("./notification-providers/goalert");
@ -100,6 +101,7 @@ class Notification {
new Teams(), new Teams(),
new TechulusPush(), new TechulusPush(),
new Telegram(), new Telegram(),
new Splunk(),
new Webhook(), new Webhook(),
new WeCom(), new WeCom(),
new GoAlert(), new GoAlert(),

@ -99,6 +99,7 @@ class Prometheus {
} }
} }
/** Remove monitor from prometheus */
remove() { remove() {
try { try {
monitorCertDaysRemaining.remove(this.monitorLabelValues); monitorCertDaysRemaining.remove(this.monitorLabelValues);

@ -6,10 +6,10 @@ class UptimeCacheList {
static list = {}; static list = {};
/** /**
* * Get the uptime for a specific period
* @param monitorID * @param {number} monitorID
* @param duration * @param {number} duration
* @return number * @return {number}
*/ */
static getUptime(monitorID, duration) { static getUptime(monitorID, duration) {
if (UptimeCacheList.list[monitorID] && UptimeCacheList.list[monitorID][duration]) { if (UptimeCacheList.list[monitorID] && UptimeCacheList.list[monitorID][duration]) {
@ -20,6 +20,12 @@ class UptimeCacheList {
} }
} }
/**
* Add uptime for specified monitor
* @param {number} monitorID
* @param {number} duration
* @param {number} uptime Uptime to add
*/
static addUptime(monitorID, duration, uptime) { static addUptime(monitorID, duration, uptime) {
log.debug("UptimeCacheList", "addUptime: " + monitorID + " " + duration); log.debug("UptimeCacheList", "addUptime: " + monitorID + " " + duration);
if (!UptimeCacheList.list[monitorID]) { if (!UptimeCacheList.list[monitorID]) {
@ -28,6 +34,10 @@ class UptimeCacheList {
UptimeCacheList.list[monitorID][duration] = uptime; UptimeCacheList.list[monitorID][duration] = uptime;
} }
/**
* Clear cache for specified monitor
* @param {number} monitorID
*/
static clearCache(monitorID) { static clearCache(monitorID) {
log.debug("UptimeCacheList", "clearCache: " + monitorID); log.debug("UptimeCacheList", "clearCache: " + monitorID);
delete UptimeCacheList.list[monitorID]; delete UptimeCacheList.list[monitorID];

@ -86,6 +86,7 @@ class UptimeKumaServer {
this.io = new Server(this.httpServer); this.io = new Server(this.httpServer);
} }
/** Initialise app after the database has been set up */
async initAfterDatabaseReady() { async initAfterDatabaseReady() {
await CacheableDnsHttpAgent.update(); await CacheableDnsHttpAgent.update();
@ -98,6 +99,11 @@ class UptimeKumaServer {
this.generateMaintenanceTimeslotsInterval = setInterval(this.generateMaintenanceTimeslots, 60 * 1000); this.generateMaintenanceTimeslotsInterval = setInterval(this.generateMaintenanceTimeslots, 60 * 1000);
} }
/**
* Send list of monitors to client
* @param {Socket} socket
* @returns {Object} List of monitors
*/
async sendMonitorList(socket) { async sendMonitorList(socket) {
let list = await this.getMonitorJSONList(socket.userID); let list = await this.getMonitorJSONList(socket.userID);
this.io.to(socket.userID).emit("monitorList", list); this.io.to(socket.userID).emit("monitorList", list);
@ -134,6 +140,11 @@ class UptimeKumaServer {
return await this.sendMaintenanceListByUserID(socket.userID); return await this.sendMaintenanceListByUserID(socket.userID);
} }
/**
* Send list of maintenances to user
* @param {number} userID
* @returns {Object}
*/
async sendMaintenanceListByUserID(userID) { async sendMaintenanceListByUserID(userID) {
let list = await this.getMaintenanceJSONList(userID); let list = await this.getMaintenanceJSONList(userID);
this.io.to(userID).emit("maintenanceList", list); this.io.to(userID).emit("maintenanceList", list);
@ -185,6 +196,11 @@ class UptimeKumaServer {
errorLogStream.end(); errorLogStream.end();
} }
/**
* Get the IP of the client connected to the socket
* @param {Socket} socket
* @returns {string}
*/
async getClientIP(socket) { async getClientIP(socket) {
let clientIP = socket.client.conn.remoteAddress; let clientIP = socket.client.conn.remoteAddress;
@ -203,6 +219,12 @@ class UptimeKumaServer {
} }
} }
/**
* Attempt to get the current server timezone
* If this fails, fall back to environment variables and then make a
* guess.
* @returns {string}
*/
async getTimezone() { async getTimezone() {
let timezone = await Settings.get("serverTimezone"); let timezone = await Settings.get("serverTimezone");
if (timezone) { if (timezone) {
@ -214,16 +236,25 @@ class UptimeKumaServer {
} }
} }
/**
* Get the current offset
* @returns {string}
*/
getTimezoneOffset() { getTimezoneOffset() {
return dayjs().format("Z"); return dayjs().format("Z");
} }
/**
* Set the current server timezone and environment variables
* @param {string} timezone
*/
async setTimezone(timezone) { async setTimezone(timezone) {
await Settings.set("serverTimezone", timezone, "general"); await Settings.set("serverTimezone", timezone, "general");
process.env.TZ = timezone; process.env.TZ = timezone;
dayjs.tz.setDefault(timezone); dayjs.tz.setDefault(timezone);
} }
/** Load the timeslots for maintenance */
async generateMaintenanceTimeslots() { async generateMaintenanceTimeslots() {
let list = await R.find("maintenance_timeslot", " generated_next = 0 AND start_date <= DATETIME('now') "); let list = await R.find("maintenance_timeslot", " generated_next = 0 AND start_date <= DATETIME('now') ");
@ -237,6 +268,7 @@ class UptimeKumaServer {
} }
/** Stop the server */
async stop() { async stop() {
clearTimeout(this.generateMaintenanceTimeslotsInterval); clearTimeout(this.generateMaintenanceTimeslotsInterval);
} }

@ -14,6 +14,7 @@ const mssql = require("mssql");
const { Client } = require("pg"); const { Client } = require("pg");
const postgresConParse = require("pg-connection-string").parse; const postgresConParse = require("pg-connection-string").parse;
const mysql = require("mysql2"); const mysql = require("mysql2");
const { MongoClient } = require("mongodb");
const { NtlmClient } = require("axios-ntlm"); const { NtlmClient } = require("axios-ntlm");
const { Settings } = require("./settings"); const { Settings } = require("./settings");
const grpc = require("@grpc/grpc-js"); const grpc = require("@grpc/grpc-js");
@ -104,7 +105,7 @@ exports.pingAsync = function (hostname, ipv6 = false) {
ping.promise.probe(hostname, { ping.promise.probe(hostname, {
v6: ipv6, v6: ipv6,
min_reply: 1, min_reply: 1,
timeout: 10, deadline: 10,
}).then((res) => { }).then((res) => {
// If ping failed, it will set field to unknown // If ping failed, it will set field to unknown
if (res.alive) { if (res.alive) {
@ -136,7 +137,7 @@ exports.mqttAsync = function (hostname, topic, okMessage, options = {}) {
const { port, username, password, interval = 20 } = options; const { port, username, password, interval = 20 } = options;
// Adds MQTT protocol to the hostname if not already present // Adds MQTT protocol to the hostname if not already present
if (!/^(?:http|mqtt)s?:\/\//.test(hostname)) { if (!/^(?:http|mqtt|ws)s?:\/\//.test(hostname)) {
hostname = "mqtt://" + hostname; hostname = "mqtt://" + hostname;
} }
@ -146,10 +147,11 @@ exports.mqttAsync = function (hostname, topic, okMessage, options = {}) {
reject(new Error("Timeout")); reject(new Error("Timeout"));
}, interval * 1000 * 0.8); }, interval * 1000 * 0.8);
log.debug("mqtt", "MQTT connecting"); const mqttUrl = `${hostname}:${port}`;
let client = mqtt.connect(hostname, { log.debug("mqtt", `MQTT connecting to ${mqttUrl}`);
port,
let client = mqtt.connect(mqttUrl, {
username, username,
password password
}); });
@ -281,18 +283,23 @@ exports.postgresQuery = function (connectionString, query) {
const client = new Client({ connectionString }); const client = new Client({ connectionString });
client.connect(); client.connect((err) => {
if (err) {
return client.query(query)
.then(res => {
resolve(res);
})
.catch(err => {
reject(err); reject(err);
}) client.end();
.finally(() => { } else {
// Connected here
client.query(query, (err, res) => {
if (err) {
reject(err);
} else {
resolve(res);
}
client.end(); client.end();
}); });
}
});
}); });
}; };
@ -318,6 +325,23 @@ exports.mysqlQuery = function (connectionString, query) {
}); });
}; };
/**
* Connect to and Ping a MongoDB database
* @param {string} connectionString The database connection string
* @returns {Promise<(string[]|Object[]|Object)>}
*/
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 * Query radius server
* @param {string} hostname Hostname of radius server * @param {string} hostname Hostname of radius server

@ -91,11 +91,16 @@ export default {
}, },
methods: { methods: {
/** Confirm deletion of docker host */
deleteConfirm() { deleteConfirm() {
this.modal.hide(); this.modal.hide();
this.$refs.confirmDelete.show(); this.$refs.confirmDelete.show();
}, },
/**
* Show specified docker host
* @param {number} dockerHostID
*/
show(dockerHostID) { show(dockerHostID) {
if (dockerHostID) { if (dockerHostID) {
let found = false; let found = false;
@ -126,6 +131,7 @@ export default {
this.modal.show(); this.modal.show();
}, },
/** Add docker host */
submit() { submit() {
this.processing = true; this.processing = true;
this.$root.getSocket().emit("addDockerHost", this.dockerHost, this.id, (res) => { this.$root.getSocket().emit("addDockerHost", this.dockerHost, this.id, (res) => {
@ -144,6 +150,7 @@ export default {
}); });
}, },
/** Test the docker host */
test() { test() {
this.processing = true; this.processing = true;
this.$root.getSocket().emit("testDockerHost", this.dockerHost, (res) => { this.$root.getSocket().emit("testDockerHost", this.dockerHost, (res) => {
@ -152,6 +159,7 @@ export default {
}); });
}, },
/** Delete this docker host */
deleteDockerHost() { deleteDockerHost() {
this.processing = true; this.processing = true;
this.$root.getSocket().emit("deleteDockerHost", this.id, (res) => { this.$root.getSocket().emit("deleteDockerHost", this.id, (res) => {

@ -3,6 +3,8 @@
</template> </template>
<script> <script>
import { DOWN, MAINTENANCE, PENDING, UP } from "../util.ts";
export default { export default {
props: { props: {
/** Monitor this represents */ /** Monitor this represents */
@ -24,7 +26,6 @@ export default {
computed: { computed: {
uptime() { uptime() {
if (this.type === "maintenance") { if (this.type === "maintenance") {
return this.$t("statusMaintenance"); return this.$t("statusMaintenance");
} }
@ -39,19 +40,19 @@ export default {
}, },
color() { color() {
if (this.type === "maintenance" || this.monitor.maintenance) { if (this.lastHeartBeat.status === MAINTENANCE) {
return "maintenance"; return "maintenance";
} }
if (this.lastHeartBeat.status === 0) { if (this.lastHeartBeat.status === DOWN) {
return "danger"; return "danger";
} }
if (this.lastHeartBeat.status === 1) { if (this.lastHeartBeat.status === UP) {
return "primary"; return "primary";
} }
if (this.lastHeartBeat.status === 2) { if (this.lastHeartBeat.status === PENDING) {
return "warning"; return "warning";
} }

@ -26,6 +26,10 @@
<label for="promosms-sender-name" class="form-label">{{ $t("promosmsSMSSender") }}</label> <label for="promosms-sender-name" class="form-label">{{ $t("promosmsSMSSender") }}</label>
<input id="promosms-sender-name" v-model="$parent.notification.promosmsSenderName" type="text" minlength="3" maxlength="11" class="form-control"> <input id="promosms-sender-name" v-model="$parent.notification.promosmsSenderName" type="text" minlength="3" maxlength="11" class="form-control">
</div> </div>
<div class="form-check form-switch">
<input id="promosms-allow-long" v-model="$parent.notification.promosmsAllowLongSMS" type="checkbox" class="form-check-input">
<label for="promosms-allow-long" class="form-label">{{ $t("promosmsAllowLongSMS") }}</label>
</div>
</template> </template>
<script> <script>

@ -0,0 +1,32 @@
<template>
<div class="mb-3">
<label for="splunk-rest-url" class="form-label">{{ $t("Splunk Rest URL") }}</label>
<HiddenInput id="splunk-rest-url" v-model="$parent.notification.splunkRestURL" :required="true" autocomplete="false"></HiddenInput>
</div>
<div class="mb-3">
<label for="splunk-severity" class="form-label">{{ $t("Severity") }}</label>
<select id="splunk-severity" v-model="$parent.notification.splunkSeverity" class="form-select">
<option value="INFO">{{ $t("info") }}</option>
<option value="WARNING">{{ $t("warning") }}</option>
<option value="CRITICAL" selected="selected">{{ $t("critical") }}</option>
</select>
</div>
<div class="mb-3">
<label for="splunk-resolve" class="form-label">{{ $t("Auto resolve or acknowledged") }}</label>
<select id="splunk-resolve" v-model="$parent.notification.splunkAutoResolve" class="form-select">
<option value="0" selected="selected">{{ $t("do nothing") }}</option>
<option value="ACKNOWLEDGEMENT">{{ $t("auto acknowledged") }}</option>
<option value="RECOVERY">{{ $t("auto resolve") }}</option>
</select>
</div>
</template>
<script>
import HiddenInput from "../HiddenInput.vue";
export default {
components: {
HiddenInput,
},
};
</script>

@ -42,6 +42,11 @@ export default {
HiddenInput, HiddenInput,
}, },
methods: { methods: {
/**
* Get the URL for telegram updates
* @param {string} [mode=masked] Should the token be masked?
* @returns {string} formatted URL
*/
telegramGetUpdatesURL(mode = "masked") { telegramGetUpdatesURL(mode = "masked") {
let token = `<${this.$t("YOUR BOT TOKEN HERE")}>`; let token = `<${this.$t("YOUR BOT TOKEN HERE")}>`;
@ -55,6 +60,8 @@ export default {
return `https://api.telegram.org/bot${token}/getUpdates`; return `https://api.telegram.org/bot${token}/getUpdates`;
}, },
/** Get the telegram chat ID */
async autoGetTelegramChatID() { async autoGetTelegramChatID() {
try { try {
let res = await axios.get(this.telegramGetUpdatesURL("withToken")); let res = await axios.get(this.telegramGetUpdatesURL("withToken"));

@ -44,6 +44,7 @@ import Webhook from "./Webhook.vue";
import WeCom from "./WeCom.vue"; import WeCom from "./WeCom.vue";
import GoAlert from "./GoAlert.vue"; import GoAlert from "./GoAlert.vue";
import ZohoCliq from "./ZohoCliq.vue"; import ZohoCliq from "./ZohoCliq.vue";
import Splunk from "./Splunk.vue";
/** /**
* Manage all notification form. * Manage all notification form.
@ -92,6 +93,7 @@ const NotificationFormList = {
"stackfield": Stackfield, "stackfield": Stackfield,
"teams": Teams, "teams": Teams,
"telegram": Telegram, "telegram": Telegram,
"Splunk": Splunk,
"webhook": Webhook, "webhook": Webhook,
"WeCom": WeCom, "WeCom": WeCom,
"GoAlert": GoAlert, "GoAlert": GoAlert,

@ -191,6 +191,7 @@ export default {
location.reload(); location.reload();
}, },
/** Show confirmation dialog for disable auth */
confirmDisableAuth() { confirmDisableAuth() {
this.$refs.confirmDisableAuth.show(); this.$refs.confirmDisableAuth.show();
}, },

@ -44,6 +44,7 @@ import {
faWrench, faWrench,
faHeartbeat, faHeartbeat,
faFilter, faFilter,
faInfoCircle,
} from "@fortawesome/free-solid-svg-icons"; } from "@fortawesome/free-solid-svg-icons";
library.add( library.add(
@ -88,6 +89,7 @@ library.add(
faWrench, faWrench,
faHeartbeat, faHeartbeat,
faFilter, faFilter,
faInfoCircle,
); );
export { FontAwesomeIcon }; export { FontAwesomeIcon };

@ -322,6 +322,7 @@ export default {
promosmsTypeSpeed: "SMS SPEED - Highest priority in system. Very quick and reliable but costly (about twice of SMS FULL price).", promosmsTypeSpeed: "SMS SPEED - Highest priority in system. Very quick and reliable but costly (about twice of SMS FULL price).",
promosmsPhoneNumber: "Phone number (for Polish recipient You can skip area codes)", promosmsPhoneNumber: "Phone number (for Polish recipient You can skip area codes)",
promosmsSMSSender: "SMS Sender Name : Pre-registred name or one of defaults: InfoSMS, SMS Info, MaxSMS, INFO, SMS", promosmsSMSSender: "SMS Sender Name : Pre-registred name or one of defaults: InfoSMS, SMS Info, MaxSMS, INFO, SMS",
promosmsAllowLongSMS: "Allow long SMS",
"Feishu WebHookUrl": "Feishu WebHookURL", "Feishu WebHookUrl": "Feishu WebHookURL",
matrixHomeserverURL: "Homeserver URL (with http(s):// and optionally port)", matrixHomeserverURL: "Homeserver URL (with http(s):// and optionally port)",
"Internal Room Id": "Internal Room ID", "Internal Room Id": "Internal Room ID",

@ -284,6 +284,7 @@ export default {
promosmsTypeSpeed: "SMS SPEED - wysyłka priorytetowa, ma wszystkie zalety SMS FULL", promosmsTypeSpeed: "SMS SPEED - wysyłka priorytetowa, ma wszystkie zalety SMS FULL",
promosmsPhoneNumber: "Numer odbiorcy", promosmsPhoneNumber: "Numer odbiorcy",
promosmsSMSSender: "Nadawca SMS (wcześniej zatwierdzone nazwy z panelu PromoSMS)", promosmsSMSSender: "Nadawca SMS (wcześniej zatwierdzone nazwy z panelu PromoSMS)",
promosmsAllowLongSMS: "Zezwól na długie SMSy",
"Primary Base URL": "Główny URL", "Primary Base URL": "Główny URL",
"Push URL": "Push URL", "Push URL": "Push URL",
needPushEvery: "Powinieneś wywoływać ten URL co {0} sekund", needPushEvery: "Powinieneś wywoływać ten URL co {0} sekund",

@ -63,6 +63,12 @@
</router-link> </router-link>
</li> </li>
<li>
<a href="https://github.com/louislam/uptime-kuma/wiki" class="dropdown-item" target="_blank">
<font-awesome-icon icon="info-circle" /> {{ $t("Help") }}
</a>
</li>
<li v-if="$root.loggedIn && $root.socket.token !== 'autoLogin'"> <li v-if="$root.loggedIn && $root.socket.token !== 'autoLogin'">
<button class="dropdown-item" @click="$root.logout"> <button class="dropdown-item" @click="$root.logout">
<font-awesome-icon icon="sign-out-alt" /> <font-awesome-icon icon="sign-out-alt" />

@ -12,6 +12,11 @@ export default {
}, },
methods: { methods: {
/**
* Convert value to UTC
* @param {string | number | Date | dayjs.Dayjs} value
* @returns {dayjs.Dayjs}
*/
toUTC(value) { toUTC(value) {
return dayjs.tz(value, this.timezone).utc().format(); return dayjs.tz(value, this.timezone).utc().format();
}, },
@ -34,6 +39,11 @@ export default {
return this.datetimeFormat(value, "YYYY-MM-DD HH:mm:ss"); return this.datetimeFormat(value, "YYYY-MM-DD HH:mm:ss");
}, },
/**
* Get time for maintenance
* @param {string | number | Date | dayjs.Dayjs} value
* @returns {string}
*/
datetimeMaintenance(value) { datetimeMaintenance(value) {
const inputDate = new Date(value); const inputDate = new Date(value);
const now = new Date(Date.now()); const now = new Date(Date.now());

@ -3,6 +3,7 @@ import { useToast } from "vue-toastification";
import jwtDecode from "jwt-decode"; import jwtDecode from "jwt-decode";
import Favico from "favico.js"; import Favico from "favico.js";
import dayjs from "dayjs"; import dayjs from "dayjs";
import { DOWN, MAINTENANCE, PENDING, UP } from "../util.ts";
const toast = useToast(); const toast = useToast();
let socket; let socket;
@ -454,6 +455,10 @@ export default {
socket.emit("getMonitorList", callback); socket.emit("getMonitorList", callback);
}, },
/**
* Get list of maintenances
* @param {socketCB} callback
*/
getMaintenanceList(callback) { getMaintenanceList(callback) {
if (! callback) { if (! callback) {
callback = () => { }; callback = () => { };
@ -470,22 +475,49 @@ export default {
socket.emit("add", monitor, callback); socket.emit("add", monitor, callback);
}, },
/**
* Adds a maintenace
* @param {Object} maintenance
* @param {socketCB} callback
*/
addMaintenance(maintenance, callback) { addMaintenance(maintenance, callback) {
socket.emit("addMaintenance", maintenance, callback); socket.emit("addMaintenance", maintenance, callback);
}, },
/**
* Add monitors to maintenance
* @param {number} maintenanceID
* @param {number[]} monitors
* @param {socketCB} callback
*/
addMonitorMaintenance(maintenanceID, monitors, callback) { addMonitorMaintenance(maintenanceID, monitors, callback) {
socket.emit("addMonitorMaintenance", maintenanceID, monitors, callback); socket.emit("addMonitorMaintenance", maintenanceID, monitors, callback);
}, },
/**
* Add status page to maintenance
* @param {number} maintenanceID
* @param {number} statusPages
* @param {socketCB} callback
*/
addMaintenanceStatusPage(maintenanceID, statusPages, callback) { addMaintenanceStatusPage(maintenanceID, statusPages, callback) {
socket.emit("addMaintenanceStatusPage", maintenanceID, statusPages, callback); socket.emit("addMaintenanceStatusPage", maintenanceID, statusPages, callback);
}, },
/**
* Get monitors affected by maintenance
* @param {number} maintenanceID
* @param {socketCB} callback
*/
getMonitorMaintenance(maintenanceID, callback) { getMonitorMaintenance(maintenanceID, callback) {
socket.emit("getMonitorMaintenance", maintenanceID, callback); socket.emit("getMonitorMaintenance", maintenanceID, callback);
}, },
/**
* Get status pages where maintenance is shown
* @param {number} maintenanceID
* @param {socketCB} callback
*/
getMaintenanceStatusPage(maintenanceID, callback) { getMaintenanceStatusPage(maintenanceID, callback) {
socket.emit("getMaintenanceStatusPage", maintenanceID, callback); socket.emit("getMaintenanceStatusPage", maintenanceID, callback);
}, },
@ -499,6 +531,11 @@ export default {
socket.emit("deleteMonitor", monitorID, callback); socket.emit("deleteMonitor", monitorID, callback);
}, },
/**
* Delete specified maintenance
* @param {number} maintenanceID
* @param {socketCB} callback
*/
deleteMaintenance(maintenanceID, callback) { deleteMaintenance(maintenanceID, callback) {
socket.emit("deleteMaintenance", maintenanceID, callback); socket.emit("deleteMaintenance", maintenanceID, callback);
}, },
@ -590,28 +627,28 @@ export default {
for (let monitorID in this.lastHeartbeatList) { for (let monitorID in this.lastHeartbeatList) {
let lastHeartBeat = this.lastHeartbeatList[monitorID]; let lastHeartBeat = this.lastHeartbeatList[monitorID];
if (this.monitorList[monitorID] && this.monitorList[monitorID].maintenance) { if (! lastHeartBeat) {
result[monitorID] = {
text: this.$t("statusMaintenance"),
color: "maintenance",
};
} else if (! lastHeartBeat) {
result[monitorID] = unknown; result[monitorID] = unknown;
} else if (lastHeartBeat.status === 1) { } else if (lastHeartBeat.status === UP) {
result[monitorID] = { result[monitorID] = {
text: this.$t("Up"), text: this.$t("Up"),
color: "primary", color: "primary",
}; };
} else if (lastHeartBeat.status === 0) { } else if (lastHeartBeat.status === DOWN) {
result[monitorID] = { result[monitorID] = {
text: this.$t("Down"), text: this.$t("Down"),
color: "danger", color: "danger",
}; };
} else if (lastHeartBeat.status === 2) { } else if (lastHeartBeat.status === PENDING) {
result[monitorID] = { result[monitorID] = {
text: this.$t("Pending"), text: this.$t("Pending"),
color: "warning", color: "warning",
}; };
} else if (lastHeartBeat.status === MAINTENANCE) {
result[monitorID] = {
text: this.$t("statusMaintenance"),
color: "maintenance",
};
} else { } else {
result[monitorID] = unknown; result[monitorID] = unknown;
} }
@ -633,17 +670,17 @@ export default {
let beat = this.$root.lastHeartbeatList[monitorID]; let beat = this.$root.lastHeartbeatList[monitorID];
let monitor = this.$root.monitorList[monitorID]; let monitor = this.$root.monitorList[monitorID];
if (monitor && monitor.maintenance) { if (monitor && ! monitor.active) {
result.maintenance++;
} else if (monitor && ! monitor.active) {
result.pause++; result.pause++;
} else if (beat) { } else if (beat) {
if (beat.status === 1) { if (beat.status === UP) {
result.up++; result.up++;
} else if (beat.status === 0) { } else if (beat.status === DOWN) {
result.down++; result.down++;
} else if (beat.status === 2) { } else if (beat.status === PENDING) {
result.up++; result.up++;
} else if (beat.status === MAINTENANCE) {
result.maintenance++;
} else { } else {
result.unknown++; result.unknown++;
} }

@ -356,6 +356,7 @@ export default {
}); });
}, },
methods: { methods: {
/** Initialise page */
init() { init() {
this.affectedMonitors = []; this.affectedMonitors = [];
this.selectedStatusPages = []; this.selectedStatusPages = [];
@ -414,6 +415,7 @@ export default {
} }
}, },
/** Create new maintenance */
async submit() { async submit() {
this.processing = true; this.processing = true;
@ -458,6 +460,11 @@ export default {
} }
}, },
/**
* Add monitor to maintenance
* @param {number} maintenanceID
* @param {socketCB} callback
*/
async addMonitorMaintenance(maintenanceID, callback) { async addMonitorMaintenance(maintenanceID, callback) {
await this.$root.addMonitorMaintenance(maintenanceID, this.affectedMonitors, async (res) => { await this.$root.addMonitorMaintenance(maintenanceID, this.affectedMonitors, async (res) => {
if (!res.ok) { if (!res.ok) {
@ -470,6 +477,11 @@ export default {
}); });
}, },
/**
* Add status page to maintenance
* @param {number} maintenanceID
* @param {socketCB} callback
*/
async addMaintenanceStatusPage(maintenanceID, callback) { async addMaintenanceStatusPage(maintenanceID, callback) {
await this.$root.addMaintenanceStatusPage(maintenanceID, (this.showOnAllPages) ? this.selectedStatusPagesOptions : this.selectedStatusPages, async (res) => { await this.$root.addMaintenanceStatusPage(maintenanceID, (this.showOnAllPages) ? this.selectedStatusPagesOptions : this.selectedStatusPages, async (res) => {
if (!res.ok) { if (!res.ok) {

@ -57,6 +57,9 @@
<option value="mysql"> <option value="mysql">
MySQL/MariaDB MySQL/MariaDB
</option> </option>
<option value="mongodb">
MongoDB
</option>
<option value="radius"> <option value="radius">
Radius Radius
</option> </option>
@ -108,7 +111,7 @@
<!-- TCP Port / Ping / DNS / Steam / MQTT / Radius only --> <!-- TCP Port / Ping / DNS / Steam / MQTT / Radius only -->
<div v-if="monitor.type === 'port' || monitor.type === 'ping' || monitor.type === 'dns' || monitor.type === 'steam' || monitor.type === 'mqtt' || monitor.type === 'radius'" class="my-3"> <div v-if="monitor.type === 'port' || monitor.type === 'ping' || monitor.type === 'dns' || monitor.type === 'steam' || monitor.type === 'mqtt' || monitor.type === 'radius'" class="my-3">
<label for="hostname" class="form-label">{{ $t("Hostname") }}</label> <label for="hostname" class="form-label">{{ $t("Hostname") }}</label>
<input id="hostname" v-model="monitor.hostname" type="text" class="form-control" :pattern="`${ipRegexPattern}|${hostnameRegexPattern}`" required> <input id="hostname" v-model="monitor.hostname" type="text" class="form-control" :pattern="`${monitor.type === 'mqtt' ? mqttIpOrHostnameRegexPattern : ipOrHostnameRegexPattern}`" required>
</div> </div>
<!-- Port --> <!-- Port -->
@ -277,6 +280,18 @@
<input id="redisConnectionString" v-model="monitor.databaseConnectionString" type="text" class="form-control" placeholder="redis://user:password@host:port"> <input id="redisConnectionString" v-model="monitor.databaseConnectionString" type="text" class="form-control" placeholder="redis://user:password@host:port">
</div> </div>
</template> </template>
<!-- MongoDB -->
<template v-if="monitor.type === 'mongodb'">
<div class="my-3">
<label for="sqlConnectionString" class="form-label">{{ $t("Connection String") }}</label>
<template v-if="monitor.type === 'mongodb'">
<input id="sqlConnectionString" v-model="monitor.databaseConnectionString" type="text" class="form-control" placeholder="mongodb://username:password@host:port/database">
</template>
</div>
</template>
<!-- Interval --> <!-- Interval -->
<div class="my-3"> <div class="my-3">
<label for="interval" class="form-label">{{ $t("Heartbeat Interval") }} ({{ $t("checkEverySecond", [ monitor.interval ]) }})</label> <label for="interval" class="form-label">{{ $t("Heartbeat Interval") }} ({{ $t("checkEverySecond", [ monitor.interval ]) }})</label>
@ -594,6 +609,7 @@ import DockerHostDialog from "../components/DockerHostDialog.vue";
import ProxyDialog from "../components/ProxyDialog.vue"; import ProxyDialog from "../components/ProxyDialog.vue";
import TagsManager from "../components/TagsManager.vue"; import TagsManager from "../components/TagsManager.vue";
import { genSecret, isDev, MAX_INTERVAL_SECOND, MIN_INTERVAL_SECOND } from "../util.ts"; import { genSecret, isDev, MAX_INTERVAL_SECOND, MIN_INTERVAL_SECOND } from "../util.ts";
import { hostNameRegexPattern } from "../util-frontend";
const toast = useToast(); const toast = useToast();
@ -618,11 +634,8 @@ export default {
}, },
acceptedStatusCodeOptions: [], acceptedStatusCodeOptions: [],
dnsresolvetypeOptions: [], dnsresolvetypeOptions: [],
ipOrHostnameRegexPattern: hostNameRegexPattern(),
// Source: https://digitalfortress.tech/tips/top-15-commonly-used-regex/ mqttIpOrHostnameRegexPattern: hostNameRegexPattern(true)
ipRegexPattern: "((^\\s*((([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))\\s*$)|(^\\s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:)))(%.+)?\\s*$))",
// Source: https://stackoverflow.com/questions/106179/regular-expression-to-match-dns-hostname-or-ip-address
hostnameRegexPattern: "^(([a-zA-Z0-9_]|[a-zA-Z0-9_][a-zA-Z0-9\\-_]*[a-zA-Z0-9_])\\.)*([A-Za-z0-9_]|[A-Za-z0-9_][A-Za-z0-9\\-_]*[A-Za-z0-9_])$"
}; };
}, },

@ -65,6 +65,7 @@ export default {
this.init(); this.init();
}, },
methods: { methods: {
/** Initialise page */
init() { init() {
this.$root.getSocket().emit("getMonitorMaintenance", this.$route.params.id, (res) => { this.$root.getSocket().emit("getMonitorMaintenance", this.$route.params.id, (res) => {
if (res.ok) { if (res.ok) {
@ -83,10 +84,12 @@ export default {
}); });
}, },
/** Confirm deletion */
deleteDialog() { deleteDialog() {
this.$refs.confirmDelete.show(); this.$refs.confirmDelete.show();
}, },
/** Delete maintenance after showing confirmation */
deleteMaintenance() { deleteMaintenance() {
this.$root.deleteMaintenance(this.maintenance.id, (res) => { this.$root.deleteMaintenance(this.maintenance.id, (res) => {
if (res.ok) { if (res.ok) {

@ -133,15 +133,25 @@ export default {
} }
}, },
/**
* Get maintenance URL
* @param {number} id
* @returns {string} Relative URL
*/
maintenanceURL(id) { maintenanceURL(id) {
return getMaintenanceRelativeURL(id); return getMaintenanceRelativeURL(id);
}, },
/**
* Show delete confirmation
* @param {number} maintenanceID
*/
deleteDialog(maintenanceID) { deleteDialog(maintenanceID) {
this.selectedMaintenanceID = maintenanceID; this.selectedMaintenanceID = maintenanceID;
this.$refs.confirmDelete.show(); this.$refs.confirmDelete.show();
}, },
/** Delete maintenance after showing confirmation dialog */
deleteMaintenance() { deleteMaintenance() {
this.$root.deleteMaintenance(this.selectedMaintenanceID, (res) => { this.$root.deleteMaintenance(this.selectedMaintenanceID, (res) => {
if (res.ok) { if (res.ok) {

@ -79,6 +79,22 @@ export function getResBaseURL() {
} }
} }
/**
*
* @param {} mqtt wheather or not the regex should take into account the fact that it is an mqtt uri
* @returns RegExp The requested regex
*/
export function hostNameRegexPattern(mqtt = false) {
// mqtt, mqtts, ws and wss schemes accepted by mqtt.js (https://github.com/mqttjs/MQTT.js/#connect)
const mqttSchemeRegexPattern = "((mqtt|ws)s?:\\/\\/)?";
// Source: https://digitalfortress.tech/tips/top-15-commonly-used-regex/
const ipRegexPattern = `((^\\s*${mqtt ? mqttSchemeRegexPattern : ""}((([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))\\s*$)|(^\\s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:)))(%.+)?\\s*$))`;
// Source: https://stackoverflow.com/questions/106179/regular-expression-to-match-dns-hostname-or-ip-address
const hostNameRegexPattern = `^${mqtt ? mqttSchemeRegexPattern : ""}([a-zA-Z0-9])?(([a-zA-Z0-9_]|[a-zA-Z0-9_][a-zA-Z0-9\\-_]*[a-zA-Z0-9_])\\.)*([A-Za-z0-9_]|[A-Za-z0-9_][A-Za-z0-9\\-_]*[A-Za-z0-9_])$`;
return `${ipRegexPattern}|${hostNameRegexPattern}`;
}
/** /**
* Get the tag color options * Get the tag color options
* Shared between components * Shared between components

@ -315,6 +315,11 @@ function getMonitorRelativeURL(id) {
return "/dashboard/" + id; return "/dashboard/" + id;
} }
exports.getMonitorRelativeURL = getMonitorRelativeURL; exports.getMonitorRelativeURL = getMonitorRelativeURL;
/**
* Get relative path for maintenance
* @param id ID of maintenance
* @returns Formatted relative path
*/
function getMaintenanceRelativeURL(id) { function getMaintenanceRelativeURL(id) {
return "/maintenance/" + id; return "/maintenance/" + id;
} }
@ -361,6 +366,11 @@ function parseTimeFromTimeObject(obj) {
return result; return result;
} }
exports.parseTimeFromTimeObject = parseTimeFromTimeObject; exports.parseTimeFromTimeObject = parseTimeFromTimeObject;
/**
* Convert ISO date to UTC
* @param input Date
* @returns ISO Date time
*/
function isoToUTCDateTime(input) { function isoToUTCDateTime(input) {
return dayjs(input).utc().format(exports.SQL_DATETIME_FORMAT); return dayjs(input).utc().format(exports.SQL_DATETIME_FORMAT);
} }
@ -379,6 +389,12 @@ function utcToLocal(input, format = exports.SQL_DATETIME_FORMAT) {
return dayjs.utc(input).local().format(format); return dayjs.utc(input).local().format(format);
} }
exports.utcToLocal = utcToLocal; exports.utcToLocal = utcToLocal;
/**
* Convert local datetime to UTC
* @param input Local date
* @param format Format to return
* @returns Date in requested format
*/
function localToUTC(input, format = exports.SQL_DATETIME_FORMAT) { function localToUTC(input, format = exports.SQL_DATETIME_FORMAT) {
return dayjs(input).utc().format(format); return dayjs(input).utc().format(format);
} }

@ -352,6 +352,11 @@ export function getMonitorRelativeURL(id: string) {
return "/dashboard/" + id; return "/dashboard/" + id;
} }
/**
* Get relative path for maintenance
* @param id ID of maintenance
* @returns Formatted relative path
*/
export function getMaintenanceRelativeURL(id: string) { export function getMaintenanceRelativeURL(id: string) {
return "/maintenance/" + id; return "/maintenance/" + id;
} }
@ -405,7 +410,11 @@ export function parseTimeFromTimeObject(obj : any) {
return result; return result;
} }
/**
* Convert ISO date to UTC
* @param input Date
* @returns ISO Date time
*/
export function isoToUTCDateTime(input : string) { export function isoToUTCDateTime(input : string) {
return dayjs(input).utc().format(SQL_DATETIME_FORMAT); return dayjs(input).utc().format(SQL_DATETIME_FORMAT);
} }
@ -424,6 +433,12 @@ export function utcToLocal(input : string, format = SQL_DATETIME_FORMAT) {
return dayjs.utc(input).local().format(format); return dayjs.utc(input).local().format(format);
} }
/**
* Convert local datetime to UTC
* @param input Local date
* @param format Format to return
* @returns Date in requested format
*/
export function localToUTC(input : string, format = SQL_DATETIME_FORMAT) { export function localToUTC(input : string, format = SQL_DATETIME_FORMAT) {
return dayjs(input).utc().format(format); return dayjs(input).utc().format(format);
} }

@ -0,0 +1,33 @@
import { hostNameRegexPattern } from "../../../src/util-frontend";
describe("Test util-frontend.js", () => {
describe("hostNameRegexPattern()", () => {
it('should return a valid regex for non mqtt hostnames', () => {
const regex = new RegExp(hostNameRegexPattern(false));
expect(regex.test("www.test.com")).to.be.true;
expect(regex.test("127.0.0.1")).to.be.true;
expect(regex.test("192.168.1.156")).to.be.true;
["mqtt", "mqtts", "ws", "wss"].forEach(schema => {
expect(regex.test(`${schema}://www.test.com`)).to.be.false;
expect(regex.test(`${schema}://127.0.0.1`)).to.be.false;
});
});
it('should return a valid regex for mqtt hostnames', () => {
const hostnameString = hostNameRegexPattern(false);
console.log('*********', hostnameString, '***********');
const regex = new RegExp(hostNameRegexPattern(true));
expect(regex.test("www.test.com")).to.be.true;
expect(regex.test("127.0.0.1")).to.be.true;
expect(regex.test("192.168.1.156")).to.be.true;
["mqtt", "mqtts", "ws", "wss"].forEach(schema => {
expect(regex.test(`${schema}://www.test.com`)).to.be.true;
expect(regex.test(`${schema}://127.0.0.1`)).to.be.true;
});
});
});
});
Loading…
Cancel
Save