Merge branch 'master' into feature/umami-analytics-status-page

This commit is contained in:
Frank Elsinga
2025-12-25 02:20:35 +01:00
committed by GitHub
72 changed files with 6739 additions and 2022 deletions
+14 -18
View File
@@ -142,27 +142,23 @@ async function sendAPIKeyList(socket) {
* @returns {Promise<void>}
*/
async function sendInfo(socket, hideVersion = false) {
let version;
let latestVersion;
let isContainer;
let dbType;
if (!hideVersion) {
version = checkVersion.version;
latestVersion = checkVersion.latestVersion;
isContainer = (process.env.UPTIME_KUMA_IS_CONTAINER === "1");
dbType = Database.dbConfig.type;
}
socket.emit("info", {
version,
latestVersion,
isContainer,
dbType,
const info = {
primaryBaseURL: await setting("primaryBaseURL"),
serverTimezone: await server.getTimezone(),
serverTimezoneOffset: server.getTimezoneOffset(),
});
};
if (!hideVersion) {
info.version = checkVersion.version;
info.latestVersion = checkVersion.latestVersion;
info.isContainer = (process.env.UPTIME_KUMA_IS_CONTAINER === "1");
info.dbType = Database.dbConfig.type;
info.runtime = {
platform: process.platform, // linux or win32
arch: process.arch, // x86 or arm
};
}
socket.emit("info", info);
}
/**
+21 -9
View File
@@ -223,9 +223,24 @@ class Database {
let config = {};
let parsedMaxPoolConnections = parseInt(process.env.UPTIME_KUMA_DB_POOL_MAX_CONNECTIONS);
if (!process.env.UPTIME_KUMA_DB_POOL_MAX_CONNECTIONS) {
parsedMaxPoolConnections = 10;
} else if (Number.isNaN(parsedMaxPoolConnections)) {
log.warn("db", "Max database connections defaulted to 10 because UPTIME_KUMA_DB_POOL_MAX_CONNECTIONS was invalid.");
parsedMaxPoolConnections = 10;
} else if (parsedMaxPoolConnections < 1) {
log.warn("db", "Max database connections defaulted to 10 because UPTIME_KUMA_DB_POOL_MAX_CONNECTIONS was less than 1.");
parsedMaxPoolConnections = 10;
} else if (parsedMaxPoolConnections > 100) {
log.warn("db", "Max database connections capped to 100 because Mysql/Mariadb connections are heavy. consider using a proxy like ProxySQL or MaxScale.");
parsedMaxPoolConnections = 100;
}
let mariadbPoolConfig = {
min: 0,
max: 10,
max: parsedMaxPoolConnections,
idleTimeoutMillis: 30000,
};
@@ -811,9 +826,7 @@ class Database {
await Settings.set("migrateAggregateTableState", "migrating");
let progressPercent = 0;
let part = 100 / monitors.length;
let i = 1;
for (let monitor of monitors) {
for (const [ i, monitor ] of monitors.entries()) {
// Get a list of unique dates from the heartbeat table, using raw sql
let dates = await R.getAll(`
SELECT DISTINCT DATE(time) AS date
@@ -824,7 +837,7 @@ class Database {
monitor.monitor_id
]);
for (let date of dates) {
for (const [ dateIndex, date ] of dates.entries()) {
// New Uptime Calculator
let calculator = new UptimeCalculator();
calculator.monitorID = monitor.monitor_id;
@@ -840,7 +853,7 @@ class Database {
`, [ monitor.monitor_id, date.date ]);
if (heartbeats.length > 0) {
msg = `[DON'T STOP] Migrating monitor data ${monitor.monitor_id} - ${date.date} [${progressPercent.toFixed(2)}%][${i}/${monitors.length}]`;
msg = `[DON'T STOP] Migrating monitor ${monitor.monitor_id}s' (${i + 1} of ${monitors.length} total) data - ${date.date} - total migration progress ${progressPercent.toFixed(2)}%`;
log.info("db", msg);
migrationServer?.update(msg);
}
@@ -849,15 +862,14 @@ class Database {
await calculator.update(heartbeat.status, parseFloat(heartbeat.ping), dayjs(heartbeat.time));
}
progressPercent += (Math.round(part / dates.length * 100) / 100);
// Calculate progress: (current_monitor_index + relative_date_progress) / total_monitors
progressPercent = (i + (dateIndex + 1) / dates.length) / monitors.length * 100;
// Lazy to fix the floating point issue, it is acceptable since it is just a progress bar
if (progressPercent > 100) {
progressPercent = 100;
}
}
i++;
}
msg = "Clearing non-important heartbeats";
+270
View File
@@ -0,0 +1,270 @@
const { BeanModel } = require("redbean-node/dist/bean-model");
const { R } = require("redbean-node");
const { log } = require("../../src/util");
const { parse: parseTld } = require("tldts");
const { getDaysRemaining, getDaysBetween, setting, setSetting } = require("../util-server");
const { Notification } = require("../notification");
const { default: NodeFetchCache, MemoryCache } = require("node-fetch-cache");
const TABLE = "domain_expiry";
const urlTypes = [ "websocket-upgrade", "http", "keyword", "json-query", "real-browser" ];
const excludeTypes = [ "docker", "group", "push", "manual", "rabbitmq", "redis" ];
const cachedFetch = process.env.NODE_ENV ? NodeFetchCache.create({
// cache for 8h
cache: new MemoryCache({ ttl: 1000 * 60 * 60 * 8 })
}) : fetch;
/**
* Find the RDAP server for a given TLD
* @param {string} tld TLD
* @returns {Promise<string>} First RDAP server found
*/
async function getRdapServer(tld) {
let rdapList;
try {
const res = await cachedFetch("https://data.iana.org/rdap/dns.json");
rdapList = await res.json();
} catch (error) {
log.debug("rdap", error);
return null;
}
for (const service of rdapList["services"]) {
const [ tlds, urls ] = service;
if (tlds.includes(tld)) {
return urls[0];
}
}
return null;
}
/**
* Request RDAP server to retrieve the expiry date of a domain
* @param {string} domain Domain to retrieve the expiry date from
* @returns {Promise<(Date|null)>} Expiry date from RDAP server
*/
async function getRdapDomainExpiryDate(domain) {
const tld = DomainExpiry.parseTld(domain).publicSuffix;
const rdapServer = await getRdapServer(tld);
if (rdapServer === null) {
log.warn("rdap", `No RDAP server found, TLD ${tld} not supported.`);
return null;
}
const url = `${rdapServer}domain/${domain}`;
let rdapInfos;
try {
const res = await fetch(url);
if (res.status !== 200) {
return null;
}
rdapInfos = await res.json();
} catch {
log.warn("rdap", "Not able to get expiry date from RDAP");
return null;
}
if (rdapInfos["events"] === undefined) {
return null;
}
for (const event of rdapInfos["events"]) {
if (event["eventAction"] === "expiration") {
return new Date(event["eventDate"]);
}
}
return null;
}
/**
* Send a certificate notification when domain expires in less than target days
* @param {string} domain Domain we monitor
* @param {number} daysRemaining Number of days remaining on certificate
* @param {number} targetDays Number of days to alert after
* @param {LooseObject<any>[]} notificationList List of notification providers
* @returns {Promise<void>}
*/
async function sendDomainNotificationByTargetDays(domain, daysRemaining, targetDays, notificationList) {
let sent = false;
log.debug("domain", `Send domain expiry notification for ${targetDays} deadline.`);
for (let notification of notificationList) {
try {
log.debug("domain", `Sending to ${notification.name}`);
await Notification.send(
JSON.parse(notification.config),
`Domain name ${domain} will expire in ${daysRemaining} days`
);
sent = true;
} catch (e) {
log.error("domain", `Cannot send domain notification to ${notification.name}`);
log.error("domain", e);
}
}
return sent;
}
class DomainExpiry extends BeanModel {
/**
* @param {string} domain Domain name
* @returns {Promise<DomainExpiry>} Domain bean
*/
static async findByName(domain) {
return R.findOne(TABLE, "domain = ?", [ domain ]);
}
/**
* @param {string} domain Domain name
* @returns {DomainExpiry} Domain bean
*/
static createByName(domain) {
const d = R.dispense(TABLE);
d.domain = domain;
return d;
}
static parseTld = parseTld;
/**
* @returns {(object)} parsed domain components
*/
parseName() {
return parseTld(this.domain);
}
/**
* @returns {(null|object)} parsed domain tld
*/
get tld() {
return this.parseName().publicSuffix;
}
/**
* @param {Monitor} monitor Monitor object
* @returns {Promise<DomainExpiry>} Domain expiry bean
*/
static async forMonitor(monitor) {
const m = monitor;
if (excludeTypes.includes(m.type) || m.type?.match(/sql$/)) {
return false;
}
const tld = parseTld(urlTypes.includes(m.type) ? m.url : m.type === "grpc-keyword" ? m.grpcUrl : m.hostname);
const rdap = await getRdapServer(tld.publicSuffix);
if (!rdap) {
log.warn("domain", `${tld.publicSuffix} is not supported. File a bug report if you believe it should be.`);
return false;
}
const existing = await DomainExpiry.findByName(tld.domain);
if (existing) {
return existing;
}
if (tld.domain) {
return await DomainExpiry.createByName(tld.domain);
}
}
/**
* @returns {number} number of days remaining before expiry
*/
get daysRemaining() {
return getDaysRemaining(new Date(), new Date(this.expiry));
}
/**
* @returns {(Date|null)} Expiry date from RDAP
*/
getExpiryDate() {
return getRdapDomainExpiryDate(this.domain);
}
/**
* @param {(Monitor)} monitor Monitor object
* @returns {Promise<void>}
*/
static async checkExpiry(monitor) {
let bean = await DomainExpiry.forMonitor(monitor);
let expiryDate;
if (bean?.lastCheck && getDaysBetween(new Date(bean.lastCheck), new Date()) < 1) {
log.debug("domain", `Domain expiry already checked recently for ${bean.domain}, won't re-check.`);
return bean.expiry;
} else if (bean) {
expiryDate = await bean.getExpiryDate();
if (new Date(expiryDate) > new Date(bean.expiry)) {
bean.lastExpiryNotificationSent = null;
}
bean.expiry = expiryDate;
bean.lastCheck = new Date();
await R.store(bean);
}
if (expiryDate === null) {
return;
}
return expiryDate;
}
/**
* @param {Monitor} monitor Monitor instance
* @param {LooseObject<any>[]} notificationList notification List
* @returns {Promise<void>}
*/
static async sendNotifications(monitor, notificationList) {
const domain = await DomainExpiry.forMonitor(monitor);
const name = domain.domain;
if (!notificationList.length > 0) {
// fail fast. If no notification is set, all the following checks can be skipped.
log.debug("domain", "No notification, no need to send domain notification");
return;
}
const daysRemaining = getDaysRemaining(new Date(), domain.expiry);
const lastSent = domain.lastExpiryNotificationSent;
log.debug("domain", `${name} expires in ${daysRemaining} days`);
let notifyDays = await setting("domainExpiryNotifyDays");
if (notifyDays == null || !Array.isArray(notifyDays)) {
// Reset Default
await setSetting("domainExpiryNotifyDays", [ 7, 14, 21 ], "general");
notifyDays = [ 7, 14, 21 ];
}
if (Array.isArray(notifyDays)) {
// Asc sort to avoid sending multiple notifications if daysRemaining is below multiple targetDays
notifyDays.sort((a, b) => a - b);
for (const targetDays of notifyDays) {
if (daysRemaining > targetDays) {
log.debug(
"domain",
`No need to send domain notification for ${name} (${daysRemaining} days valid) on ${targetDays} deadline.`
);
continue;
} else if (lastSent && lastSent <= targetDays) {
log.debug(
"domain",
`Notification for ${name} on ${targetDays} deadline sent already, no need to send again.`
);
continue;
}
const sent = await sendDomainNotificationByTargetDays(
name,
daysRemaining,
targetDays,
notificationList
);
if (sent) {
domain.lastExpiryNotificationSent = targetDays;
await R.store(domain);
return targetDays;
}
}
}
}
}
module.exports = DomainExpiry;
+2 -2
View File
@@ -1,5 +1,5 @@
const { BeanModel } = require("redbean-node/dist/bean-model");
const { parseTimeObject, parseTimeFromTimeObject, log } = require("../../src/util");
const { parseTimeObject, parseTimeFromTimeObject, log, SQL_DATETIME_FORMAT } = require("../../src/util");
const { R } = require("redbean-node");
const dayjs = require("dayjs");
const Cron = require("croner");
@@ -262,7 +262,7 @@ class Maintenance extends BeanModel {
}, duration);
// Set last start date to current time
this.last_start_date = current.toISOString();
this.last_start_date = current.utc().format(SQL_DATETIME_FORMAT);
await R.store(this);
};
+44 -14
View File
@@ -8,8 +8,8 @@ const { log, UP, DOWN, PENDING, MAINTENANCE, flipStatus, MAX_INTERVAL_SECOND, MI
PING_COUNT_MIN, PING_COUNT_MAX, PING_COUNT_DEFAULT,
PING_PER_REQUEST_TIMEOUT_MIN, PING_PER_REQUEST_TIMEOUT_MAX, PING_PER_REQUEST_TIMEOUT_DEFAULT
} = require("../../src/util");
const { ping, checkCertificate, checkStatusCode, getTotalClientInRoom, setting, mssqlQuery, postgresQuery, mysqlQuery, setSetting, httpNtlm, radius,
kafkaProducerAsync, getOidcTokenClientCredentials, rootCertificatesFingerprints, axiosAbortSignal
const { ping, checkCertificate, checkStatusCode, getTotalClientInRoom, setting, mssqlQuery, mysqlQuery, setSetting, httpNtlm, radius,
kafkaProducerAsync, getOidcTokenClientCredentials, rootCertificatesFingerprints, axiosAbortSignal, checkCertificateHostname
} = require("../util-server");
const { R } = require("redbean-node");
const { BeanModel } = require("redbean-node/dist/bean-model");
@@ -28,6 +28,7 @@ const { CookieJar } = require("tough-cookie");
const { HttpsCookieAgent } = require("http-cookie-agent/http");
const https = require("https");
const http = require("http");
const DomainExpiry = require("./domain_expiry");
const rootCertificates = rootCertificatesFingerprints();
@@ -117,6 +118,7 @@ class Monitor extends BeanModel {
keyword: this.keyword,
invertKeyword: this.isInvertKeyword(),
expiryNotification: this.isEnabledExpiryNotification(),
domainExpiryNotification: Boolean(this.domainExpiryNotification),
ignoreTls: this.getIgnoreTls(),
upsideDown: this.isUpsideDown(),
packetSize: this.packetSize,
@@ -565,6 +567,7 @@ class Monitor extends BeanModel {
tlsSocket.once("secureConnect", async () => {
tlsInfo = checkCertificate(tlsSocket);
tlsInfo.valid = tlsSocket.authorized || false;
tlsInfo.hostnameMatchMonitorUrl = checkCertificateHostname(tlsInfo.certInfo.raw, this.getUrl()?.hostname);
await this.handleTlsInfo(tlsInfo);
});
@@ -587,6 +590,7 @@ class Monitor extends BeanModel {
if (tlsSocket) {
tlsInfo = checkCertificate(tlsSocket);
tlsInfo.valid = tlsSocket.authorized || false;
tlsInfo.hostnameMatchMonitorUrl = checkCertificateHostname(tlsInfo.certInfo.raw, this.getUrl()?.hostname);
await this.handleTlsInfo(tlsInfo);
}
@@ -766,12 +770,14 @@ class Monitor extends BeanModel {
let res = await axios.request(options);
if (res.data.State.Running) {
if (res.data.State.Health && res.data.State.Health.Status !== "healthy") {
bean.status = PENDING;
bean.msg = res.data.State.Health.Status;
} else {
if (res.data.State.Health.Status === "healthy") {
bean.status = UP;
bean.msg = res.data.State.Health ? res.data.State.Health.Status : res.data.State.Status;
} else if (res.data.State.Health.Status === "unhealthy") {
throw Error("Container State is unhealthy");
} else {
bean.status = PENDING;
bean.msg = res.data.State.Health.Status;
}
} else {
throw Error("Container State is " + res.data.State.Status);
@@ -781,14 +787,6 @@ class Monitor extends BeanModel {
await mssqlQuery(this.databaseConnectionString, this.databaseQuery || "SELECT 1");
bean.msg = "";
bean.status = UP;
bean.ping = dayjs().valueOf() - startTime;
} else if (this.type === "postgres") {
let startTime = dayjs().valueOf();
await postgresQuery(this.databaseConnectionString, this.databaseQuery || "SELECT 1");
bean.msg = "";
bean.status = UP;
bean.ping = dayjs().valueOf() - startTime;
@@ -938,6 +936,19 @@ class Monitor extends BeanModel {
}
}
if (bean.status !== MAINTENANCE && Boolean(this.domainExpiryNotification)) {
try {
const domainExpiryDate = await DomainExpiry.checkExpiry(this);
if (domainExpiryDate) {
DomainExpiry.sendNotifications(this, await Monitor.getNotificationList(this) || []);
} else {
log.debug("monitor", `Failed getting expiration date for domain ${this.name}`);
}
} catch (error) {
log.warn("monitor", `Failed to get domain expiry for ${this.name} : ${error.message}`);
}
}
if (bean.status === UP) {
log.debug("monitor", `Monitor #${this.id} '${this.name}': Successful Response: ${bean.ping} ms | Interval: ${beatInterval} seconds | Type: ${this.type}`);
} else if (bean.status === PENDING) {
@@ -1205,6 +1216,9 @@ class Monitor extends BeanModel {
// Send Cert Info
await Monitor.sendCertInfo(io, monitorID, userID);
// Send domain info
await Monitor.sendDomainInfo(io, monitorID, userID);
} else {
log.debug("monitor", "No clients in the room, no need to send stats");
}
@@ -1226,6 +1240,22 @@ class Monitor extends BeanModel {
}
}
/**
* Send domain name information to client
* @param {Server} io Socket server instance
* @param {number} monitorID ID of monitor to send
* @param {number} userID ID of user to send to
* @returns {void}
*/
static async sendDomainInfo(io, monitorID, userID) {
const monitor = await R.findOne("monitor", "id = ?", [ monitorID ]);
const domain = await DomainExpiry.forMonitor(monitor);
if (domain?.expiry) {
io.to(userID).emit("domainInfo", monitorID, domain.daysRemaining, new Date(domain.expiry));
}
}
/**
* Has status of monitor changed since last beat?
* @param {boolean} isFirstBeat Is this the first beat of this monitor?
+4 -2
View File
@@ -50,8 +50,10 @@ class DnsMonitorType extends MonitorType {
break;
case "CAA":
dnsMessage = dnsRes[0].issue;
conditionsResult = handleConditions({ record: dnsRes[0].issue });
// .filter(Boolean) was added because some CAA records do not contain an issue key, resulting in a blank list item.
// Hypothetical dnsRes [{ critical: 0, issuewild: 'letsencrypt.org' }, { critical: 0, issue: 'letsencrypt.org' }]
dnsMessage = `Records: ${dnsRes.map(record => record.issue).filter(Boolean).join(" | ")}`;
conditionsResult = dnsRes.some(record => handleConditions({ record: record.issue }));
break;
case "MX":
+83
View File
@@ -0,0 +1,83 @@
const { MonitorType } = require("./monitor-type");
const { log, UP } = require("../../src/util");
const dayjs = require("dayjs");
const postgresConParse = require("pg-connection-string").parse;
const { Client } = require("pg");
class PostgresMonitorType extends MonitorType {
name = "postgres";
/**
* @inheritdoc
*/
async check(monitor, heartbeat, _server) {
let startTime = dayjs().valueOf();
let query = monitor.databaseQuery;
// No query provided by user, use SELECT 1
if (!query || (typeof query === "string" && query.trim() === "")) {
query = "SELECT 1";
}
await this.postgresQuery(monitor.databaseConnectionString, query);
heartbeat.msg = "";
heartbeat.status = UP;
heartbeat.ping = dayjs().valueOf() - startTime;
}
/**
* Run a query on Postgres
* @param {string} connectionString The database connection string
* @param {string} query The query to validate the database with
* @returns {Promise<(string[] | object[] | object)>} Response from
* server
*/
async postgresQuery(connectionString, query) {
return new Promise((resolve, reject) => {
const config = postgresConParse(connectionString);
// Fix #3868, which true/false is not parsed to boolean
if (typeof config.ssl === "string") {
config.ssl = config.ssl === "true";
}
if (config.password === "") {
// See https://github.com/brianc/node-postgres/issues/1927
reject(new Error("Password is undefined."));
return;
}
const client = new Client(config);
client.on("error", (error) => {
log.debug("postgres", "Error caught in the error event handler.");
reject(error);
});
client.connect((err) => {
if (err) {
reject(err);
client.end();
} else {
// Connected here
try {
client.query(query, (err, res) => {
if (err) {
reject(err);
} else {
resolve(res);
}
client.end();
});
} catch (e) {
reject(e);
client.end();
}
}
});
});
}
}
module.exports = {
PostgresMonitorType,
};
+35 -6
View File
@@ -54,12 +54,32 @@ class TCPMonitorType extends MonitorType {
const preTLS = () =>
new Promise((resolve, reject) => {
let timeout;
let dialogTimeout;
let bannerTimeout;
socket_ = net.connect(monitor.port, monitor.hostname);
const onTimeout = () => {
log.debug(this.name, `[${monitor.name}] Pre-TLS connection timed out`);
reject("Connection timed out");
doReject("Connection timed out");
};
const onBannerTimeout = () => {
log.debug(this.name, `[${monitor.name}] Pre-TLS timed out waiting for banner`);
// No banner. Could be a XMPP server?
socket_.write(`<stream:stream to='${monitor.hostname}' xmlns='jabber:client' xmlns:stream='http://etherx.jabber.org/streams' version='1.0'>`);
};
const doResolve = () => {
dialogTimeout && clearTimeout(dialogTimeout);
bannerTimeout && clearTimeout(bannerTimeout);
resolve({ socket: socket_ });
};
const doReject = (error) => {
dialogTimeout && clearTimeout(dialogTimeout);
bannerTimeout && clearTimeout(bannerTimeout);
socket_.end();
reject(error);
};
socket_.on("connect", () => {
@@ -70,10 +90,10 @@ class TCPMonitorType extends MonitorType {
const response = data.toString();
const response_ = response.toLowerCase();
log.debug(this.name, `[${monitor.name}] Pre-TLS response: ${response}`);
clearTimeout(bannerTimeout);
switch (true) {
case response_.includes("start tls") || response_.includes("begin tls"):
timeout && clearTimeout(timeout);
resolve({ socket: socket_ });
doResolve();
break;
case response.startsWith("* OK") || response.match(/CAPABILITY.+STARTTLS/):
socket_.write("a001 STARTTLS\r\n");
@@ -84,8 +104,16 @@ class TCPMonitorType extends MonitorType {
case response.includes("250-STARTTLS"):
socket_.write("STARTTLS\r\n");
break;
case response_.includes("<proceed"):
doResolve();
break;
case response_.includes("<starttls"):
socket_.write("<starttls xmlns=\"urn:ietf:params:xml:ns:xmpp-tls\"/>");
break;
case response_.includes("<stream:stream") || response_.includes("</stream:stream>"):
break;
default:
reject(`Unexpected response: ${response}`);
doReject(`Unexpected response: ${response}`);
}
});
socket_.on("error", error => {
@@ -93,7 +121,8 @@ class TCPMonitorType extends MonitorType {
reject(error);
});
socket_.setTimeout(1000 * TIMEOUT, onTimeout);
timeout = setTimeout(onTimeout, 1000 * TIMEOUT);
dialogTimeout = setTimeout(onTimeout, 1000 * TIMEOUT);
bannerTimeout = setTimeout(onBannerTimeout, 1000 * 1.5);
});
const reuseSocket = monitor.smtpSecurity === "starttls" ? await preTLS() : {};
+23
View File
@@ -843,6 +843,7 @@ let needSetup = false;
bean.invertKeyword = monitor.invertKeyword;
bean.ignoreTls = monitor.ignoreTls;
bean.expiryNotification = monitor.expiryNotification;
bean.domainExpiryNotification = monitor.domainExpiryNotification;
bean.upsideDown = monitor.upsideDown;
bean.packetSize = monitor.packetSize;
bean.maxredirects = monitor.maxredirects;
@@ -981,6 +982,22 @@ let needSetup = false;
}
});
socket.on("checkMointor", async (partial, callback) => {
try {
checkLogin(socket);
const DomainExpiry = require("./model/domain_expiry");
callback({
ok: true,
domain: (await DomainExpiry.forMonitor(partial))?.domain || null
});
} catch (e) {
callback({
ok: false,
msg: e.message,
});
}
});
socket.on("getMonitorBeats", async (monitorID, period, callback) => {
try {
checkLogin(socket);
@@ -1248,6 +1265,8 @@ let needSetup = false;
value,
]);
await server.sendUpdateMonitorIntoList(socket, monitorID);
callback({
ok: true,
msg: "successAdded",
@@ -1272,6 +1291,8 @@ let needSetup = false;
monitorID,
]);
await server.sendUpdateMonitorIntoList(socket, monitorID);
callback({
ok: true,
msg: "successEdited",
@@ -1296,6 +1317,8 @@ let needSetup = false;
value,
]);
await server.sendUpdateMonitorIntoList(socket, monitorID);
callback({
ok: true,
msg: "successDeleted",
+5 -1
View File
@@ -113,6 +113,7 @@ class UptimeKumaServer {
UptimeKumaServer.monitorTypeList["tailscale-ping"] = new TailscalePing();
UptimeKumaServer.monitorTypeList["websocket-upgrade"] = new WebSocketMonitorType();
UptimeKumaServer.monitorTypeList["dns"] = new DnsMonitorType();
UptimeKumaServer.monitorTypeList["postgres"] = new PostgresMonitorType();
UptimeKumaServer.monitorTypeList["mqtt"] = new MqttMonitorType();
UptimeKumaServer.monitorTypeList["smtp"] = new SMTPMonitorType();
UptimeKumaServer.monitorTypeList["group"] = new GroupMonitorType();
@@ -221,7 +222,9 @@ class UptimeKumaServer {
*/
async sendUpdateMonitorIntoList(socket, monitorID) {
let list = await this.getMonitorJSONList(socket.userID, monitorID);
this.io.to(socket.userID).emit("updateMonitorIntoList", list);
if (list && list[monitorID]) {
this.io.to(socket.userID).emit("updateMonitorIntoList", list);
}
}
/**
@@ -558,6 +561,7 @@ const { RealBrowserMonitorType } = require("./monitor-types/real-browser-monitor
const { TailscalePing } = require("./monitor-types/tailscale-ping");
const { WebSocketMonitorType } = require("./monitor-types/websocket-upgrade");
const { DnsMonitorType } = require("./monitor-types/dns");
const { PostgresMonitorType } = require("./monitor-types/postgres");
const { MqttMonitorType } = require("./monitor-types/mqtt");
const { SMTPMonitorType } = require("./monitor-types/smtp");
const { GroupMonitorType } = require("./monitor-types/group");
+27 -61
View File
@@ -11,8 +11,6 @@ const iconv = require("iconv-lite");
const chardet = require("chardet");
const chroma = require("chroma-js");
const mssql = require("mssql");
const { Client } = require("pg");
const postgresConParse = require("pg-connection-string").parse;
const mysql = require("mysql2");
const { NtlmClient } = require("./modules/axios-ntlm/lib/ntlmClient.js");
const { Settings } = require("./settings");
@@ -349,64 +347,6 @@ exports.mssqlQuery = async function (connectionString, query) {
}
};
/**
* Run a query on Postgres
* @param {string} connectionString The database connection string
* @param {string} query The query to validate the database with
* @returns {Promise<(string[] | object[] | object)>} Response from
* server
*/
exports.postgresQuery = function (connectionString, query) {
return new Promise((resolve, reject) => {
const config = postgresConParse(connectionString);
// Fix #3868, which true/false is not parsed to boolean
if (typeof config.ssl === "string") {
config.ssl = config.ssl === "true";
}
if (config.password === "") {
// See https://github.com/brianc/node-postgres/issues/1927
reject(new Error("Password is undefined."));
return;
}
const client = new Client(config);
client.on("error", (error) => {
log.debug("postgres", "Error caught in the error event handler.");
reject(error);
});
client.connect((err) => {
if (err) {
reject(err);
client.end();
} else {
// Connected here
try {
// No query provided by user, use SELECT 1
if (!query || (typeof query === "string" && query.trim() === "")) {
query = "SELECT 1";
}
client.query(query, (err, res) => {
if (err) {
reject(err);
} else {
resolve(res);
}
client.end();
});
} catch (e) {
reject(e);
client.end();
}
}
});
});
};
/**
* Run a query on MySQL/MariaDB
* @param {string} connectionString The database connection string
@@ -543,6 +483,7 @@ exports.setSettings = async function (type, data) {
*/
const getDaysBetween = (validFrom, validTo) =>
Math.round(Math.abs(+validFrom - +validTo) / 8.64e7);
exports.getDaysBetween = getDaysBetween;
/**
* Get days remaining from a time range
@@ -552,11 +493,12 @@ const getDaysBetween = (validFrom, validTo) =>
*/
const getDaysRemaining = (validFrom, validTo) => {
const daysRemaining = getDaysBetween(validFrom, validTo);
if (new Date(validTo).getTime() < new Date().getTime()) {
if (new Date(validTo).getTime() < new Date(validFrom).getTime()) {
return -daysRemaining;
}
return daysRemaining;
};
exports.getDaysRemaining = getDaysRemaining;
/**
* Fix certificate info for display
@@ -636,6 +578,30 @@ exports.checkCertificate = function (socket) {
};
};
/**
* Checks if the certificate is valid for the provided hostname.
* Defaults to true if feature `X509Certificate` is not available, or input is not valid.
* @param {Buffer} certBuffer - The certificate buffer.
* @param {string} hostname - The hostname to compare against.
* @returns {boolean} True if the certificate is valid for the provided hostname, false otherwise.
*/
exports.checkCertificateHostname = function (certBuffer, hostname) {
let X509Certificate;
try {
X509Certificate = require("node:crypto").X509Certificate;
} catch (_) {
// X509Certificate is not available in this version of Node.js
return true;
}
if (!X509Certificate || !certBuffer || !hostname) {
return true;
}
let certObject = new X509Certificate(certBuffer);
return certObject.checkHost(hostname) !== undefined;
};
/**
* Check if the provided status code is within the accepted ranges
* @param {number} status The status code to check