mirror of
https://github.com/koichixD/uptime-kuma.git
synced 2026-09-04 17:23:21 +00:00
Merge branch 'master' into feature/umami-analytics-status-page
This commit is contained in:
+14
-18
@@ -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
@@ -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";
|
||||
|
||||
@@ -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;
|
||||
@@ -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
@@ -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?
|
||||
|
||||
@@ -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":
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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() : {};
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user