diff --git a/db/knex_migrations/2025-06-03-0000-add-ip-family.js b/db/knex_migrations/2025-06-03-0000-add-ip-family.js new file mode 100644 index 000000000..a3bcdc613 --- /dev/null +++ b/db/knex_migrations/2025-06-03-0000-add-ip-family.js @@ -0,0 +1,13 @@ +exports.up = function (knex) { + return knex.schema + .alterTable("monitor", function (table) { + table.boolean("ip_family").defaultTo(null); + }); +}; + +exports.down = function (knex) { + return knex.schema + .alterTable("monitor", function (table) { + table.dropColumn("ip_family"); + }); +}; diff --git a/db/knex_migrations/2025-06-11-0000-add-manual-monitor.js b/db/knex_migrations/2025-06-11-0000-add-manual-monitor.js new file mode 100644 index 000000000..16d307eb5 --- /dev/null +++ b/db/knex_migrations/2025-06-11-0000-add-manual-monitor.js @@ -0,0 +1,12 @@ +exports.up = function (knex) { + return knex.schema + .alterTable("monitor", function (table) { + table.string("manual_status").defaultTo(null); + }); +}; + +exports.down = function (knex) { + return knex.schema.alterTable("monitor", function (table) { + table.dropColumn("manual_status"); + }); +}; diff --git a/server/model/maintenance.js b/server/model/maintenance.js index 828c1f757..7ae88abd0 100644 --- a/server/model/maintenance.js +++ b/server/model/maintenance.js @@ -158,12 +158,22 @@ class Maintenance extends BeanModel { bean.active = obj.active; if (obj.dateRange[0]) { + const parsedDate = new Date(obj.dateRange[0]); + if (isNaN(parsedDate.getTime()) || parsedDate.getFullYear() > 9999) { + throw new Error("Invalid start date"); + } + bean.start_date = obj.dateRange[0]; } else { bean.start_date = null; } if (obj.dateRange[1]) { + const parsedDate = new Date(obj.dateRange[1]); + if (isNaN(parsedDate.getTime()) || parsedDate.getFullYear() > 9999) { + throw new Error("Invalid end date"); + } + bean.end_date = obj.dateRange[1]; } else { bean.end_date = null; @@ -235,7 +245,7 @@ class Maintenance extends BeanModel { try { this.beanMeta.status = "scheduled"; - let startEvent = (customDuration = 0) => { + let startEvent = async (customDuration = 0) => { log.info("maintenance", "Maintenance id: " + this.id + " is under maintenance now"); this.beanMeta.status = "under-maintenance"; diff --git a/server/model/monitor.js b/server/model/monitor.js index 741fb940e..c9844a55d 100644 --- a/server/model/monitor.js +++ b/server/model/monitor.js @@ -160,6 +160,7 @@ class Monitor extends BeanModel { smtpSecurity: this.smtpSecurity, rabbitmqNodes: JSON.parse(this.rabbitmqNodes), conditions: JSON.parse(this.conditions), + ipFamily: this.ipFamily, // ping advanced options ping_numeric: this.isPingNumeric(), @@ -426,10 +427,26 @@ class Monitor extends BeanModel { } } + let agentFamily = undefined; + if (this.ipFamily === "ipv4") { + agentFamily = 4; + } + if (this.ipFamily === "ipv6") { + agentFamily = 6; + } + const httpsAgentOptions = { maxCachedSessions: 0, // Use Custom agent to disable session reuse (https://github.com/nodejs/node/issues/3940) rejectUnauthorized: !this.getIgnoreTls(), secureOptions: crypto.constants.SSL_OP_LEGACY_SERVER_CONNECT, + autoSelectFamily: true, + ...(agentFamily ? { family: agentFamily } : {}) + }; + + const httpAgentOptions = { + maxCachedSessions: 0, + autoSelectFamily: true, + ...(agentFamily ? { family: agentFamily } : {}) }; log.debug("monitor", `[${this.name}] Prepare Options for axios`); @@ -491,6 +508,7 @@ class Monitor extends BeanModel { if (proxy && proxy.active) { const { httpAgent, httpsAgent } = Proxy.createAgents(proxy, { httpsAgentOptions: httpsAgentOptions, + httpAgentOptions: httpAgentOptions, }); options.proxy = false; @@ -499,6 +517,10 @@ class Monitor extends BeanModel { } } + if (!options.httpAgent) { + options.httpAgent = new http.Agent(httpAgentOptions); + } + if (!options.httpsAgent) { let jar = new CookieJar(); let httpsCookieAgentOptions = { diff --git a/server/model/status_page.js b/server/model/status_page.js index 38f548ebb..2f3511ec5 100644 --- a/server/model/status_page.js +++ b/server/model/status_page.js @@ -120,8 +120,8 @@ class StatusPage extends BeanModel { const head = $("head"); - if (statusPage.googleAnalyticsTagId) { - let escapedGoogleAnalyticsScript = googleAnalytics.getGoogleAnalyticsScript(statusPage.googleAnalyticsTagId); + if (statusPage.google_analytics_tag_id) { + let escapedGoogleAnalyticsScript = googleAnalytics.getGoogleAnalyticsScript(statusPage.google_analytics_tag_id); head.append($(escapedGoogleAnalyticsScript)); } diff --git a/server/modules/axios-ntlm/lib/ntlmClient.js b/server/modules/axios-ntlm/lib/ntlmClient.js index 682de5f9a..9dab32553 100644 --- a/server/modules/axios-ntlm/lib/ntlmClient.js +++ b/server/modules/axios-ntlm/lib/ntlmClient.js @@ -89,6 +89,9 @@ function NtlmClient(credentials, AxiosConfig) { switch (_b.label) { case 0: error = err.response; + // The header may look like this: `Negotiate, NTLM, Basic realm="itsahiddenrealm.example.net"`Add commentMore actions + // so extract the 'NTLM' part first + const ntlmheader = error.headers['www-authenticate'].split(',').find(_ => _.match(/ *NTLM/))?.trim() || ''; if (!(error && error.status === 401 && error.headers['www-authenticate'] && error.headers['www-authenticate'].includes('NTLM'))) return [3 /*break*/, 3]; @@ -96,12 +99,12 @@ function NtlmClient(credentials, AxiosConfig) { // include the Negotiate option when responding with the T2 message // There is nore we could do to ensure we are processing correctly, // but this is the easiest option for now - if (error.headers['www-authenticate'].length < 50) { + if (ntlmheader.length < 50) { t1Msg = ntlm.createType1Message(credentials.workstation, credentials.domain); error.config.headers["Authorization"] = t1Msg; } else { - t2Msg = ntlm.decodeType2Message((error.headers['www-authenticate'].match(/^NTLM\s+(.+?)(,|\s+|$)/) || [])[1]); + t2Msg = ntlm.decodeType2Message((ntlmheader.match(/^NTLM\s+(.+?)(,|\s+|$)/) || [])[1]); t3Msg = ntlm.createType3Message(t2Msg, credentials.username, credentials.password, credentials.workstation, credentials.domain); error.config.headers["X-retry"] = "false"; error.config.headers["Authorization"] = t3Msg; diff --git a/server/monitor-types/manual.js b/server/monitor-types/manual.js new file mode 100644 index 000000000..e587b7409 --- /dev/null +++ b/server/monitor-types/manual.js @@ -0,0 +1,36 @@ +const { MonitorType } = require("./monitor-type"); +const { UP, DOWN, PENDING } = require("../../src/util"); + +class ManualMonitorType extends MonitorType { + name = "Manual"; + type = "manual"; + description = "A monitor that allows manual control of the status"; + supportsConditions = false; + conditionVariables = []; + + /** + * @inheritdoc + */ + async check(monitor, heartbeat) { + if (monitor.manual_status !== null) { + heartbeat.status = monitor.manual_status; + switch (monitor.manual_status) { + case UP: + heartbeat.msg = "Up"; + break; + case DOWN: + heartbeat.msg = "Down"; + break; + default: + heartbeat.msg = "Pending"; + } + } else { + heartbeat.status = PENDING; + heartbeat.msg = "Manual monitoring - No status set"; + } + } +} + +module.exports = { + ManualMonitorType +}; diff --git a/server/notification-providers/ntfy.js b/server/notification-providers/ntfy.js index ad1d39f8f..e44e7e868 100644 --- a/server/notification-providers/ntfy.js +++ b/server/notification-providers/ntfy.js @@ -41,8 +41,8 @@ class Ntfy extends NotificationProvider { if (heartbeatJSON.status === DOWN) { tags = [ "red_circle" ]; status = "Down"; - // if priority is not 5, increase priority for down alerts - priority = priority === 5 ? priority : priority + 1; + // defaults to max(priority + 1, 5) + priority = notification.ntfyPriorityDown || (priority === 5 ? priority : priority + 1); } else if (heartbeatJSON["status"] === UP) { tags = [ "green_circle" ]; status = "Up"; diff --git a/server/prometheus.js b/server/prometheus.js index f26125d2c..485dfe53a 100644 --- a/server/prometheus.js +++ b/server/prometheus.js @@ -2,6 +2,7 @@ const PrometheusClient = require("prom-client"); const { log } = require("../src/util"); const commonLabels = [ + "monitor_id", "monitor_name", "monitor_type", "monitor_url", @@ -40,6 +41,7 @@ class Prometheus { */ constructor(monitor) { this.monitorLabelValues = { + monitor_id: monitor.id, monitor_name: monitor.name, monitor_type: monitor.type, monitor_url: monitor.url, diff --git a/server/server.js b/server/server.js index cba02174d..e328ff470 100644 --- a/server/server.js +++ b/server/server.js @@ -792,6 +792,7 @@ let needSetup = false; bean.url = monitor.url; bean.method = monitor.method; bean.body = monitor.body; + bean.ipFamily = monitor.ipFamily; bean.headers = monitor.headers; bean.basic_auth_user = monitor.basic_auth_user; bean.basic_auth_pass = monitor.basic_auth_pass; @@ -875,6 +876,7 @@ let needSetup = false; bean.rabbitmqUsername = monitor.rabbitmqUsername; bean.rabbitmqPassword = monitor.rabbitmqPassword; bean.conditions = JSON.stringify(monitor.conditions); + bean.manual_status = monitor.manual_status; // ping advanced options bean.ping_numeric = monitor.ping_numeric; diff --git a/server/uptime-kuma-server.js b/server/uptime-kuma-server.js index 1f75b72cc..a04e6bd49 100644 --- a/server/uptime-kuma-server.js +++ b/server/uptime-kuma-server.js @@ -118,6 +118,7 @@ class UptimeKumaServer { UptimeKumaServer.monitorTypeList["snmp"] = new SNMPMonitorType(); UptimeKumaServer.monitorTypeList["mongodb"] = new MongodbMonitorType(); UptimeKumaServer.monitorTypeList["rabbitmq"] = new RabbitMqMonitorType(); + UptimeKumaServer.monitorTypeList["manual"] = new ManualMonitorType(); // Allow all CORS origins (polling) in development let cors = undefined; @@ -558,4 +559,5 @@ const { GroupMonitorType } = require("./monitor-types/group"); const { SNMPMonitorType } = require("./monitor-types/snmp"); const { MongodbMonitorType } = require("./monitor-types/mongodb"); const { RabbitMqMonitorType } = require("./monitor-types/rabbitmq"); +const { ManualMonitorType } = require("./monitor-types/manual"); const Monitor = require("./model/monitor"); diff --git a/src/components/TagsManager.vue b/src/components/TagsManager.vue index aa8f93a83..e7e370a4c 100644 --- a/src/components/TagsManager.vue +++ b/src/components/TagsManager.vue @@ -4,7 +4,7 @@
@@ -20,10 +20,20 @@ {{ $t("Add") }}
-