feat: add Globalping support (#6163)

Co-authored-by: Frank Elsinga <frank@elsinga.de>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
Radu Lucuț
2026-01-26 05:39:42 +00:00
committed by GitHub
co-authored by Frank Elsinga autofix-ci[bot]
parent 66c8bac506
commit bad679ee47
14 changed files with 2070 additions and 80 deletions
+10 -73
View File
@@ -33,7 +33,6 @@ const {
checkStatusCode,
getTotalClientInRoom,
setting,
setSetting,
httpNtlm,
radius,
kafkaProducerAsync,
@@ -41,6 +40,8 @@ const {
rootCertificatesFingerprints,
axiosAbortSignal,
checkCertificateHostname,
encodeBase64,
checkCertExpiryNotifications,
} = require("../util-server");
const { R } = require("redbean-node");
const { BeanModel } = require("redbean-node/dist/bean-model");
@@ -141,11 +142,14 @@ class Monitor extends BeanModel {
method: this.method,
hostname: this.hostname,
port: this.port,
location: this.location,
protocol: this.protocol,
maxretries: this.maxretries,
weight: this.weight,
active: preloadData.activeStatus.get(this.id),
forceInactive: preloadData.forceInactive.get(this.id),
type: this.type,
subtype: this.subtype,
timeout: this.timeout,
interval: this.interval,
retryInterval: this.retryInterval,
@@ -289,17 +293,6 @@ class Monitor extends BeanModel {
};
}
/**
* Encode user and password to Base64 encoding
* for HTTP "basic" auth, as per RFC-7617
* @param {string|null} user - The username (nullable if not changed by a user)
* @param {string|null} pass - The password (nullable if not changed by a user)
* @returns {string} Encoded Base64 string
*/
encodeBase64(user, pass) {
return Buffer.from(`${user || ""}:${pass || ""}`).toString("base64");
}
/**
* Is the TLS expiry notification enabled?
* @returns {boolean} Enabled?
@@ -421,6 +414,8 @@ class Monitor extends BeanModel {
let previousBeat = null;
let retries = 0;
this.rootCertificates = rootCertificates;
try {
this.prometheus = new Prometheus(this, await this.getTags());
} catch (e) {
@@ -482,7 +477,7 @@ class Monitor extends BeanModel {
let basicAuthHeader = {};
if (this.auth_method === "basic") {
basicAuthHeader = {
Authorization: "Basic " + this.encodeBase64(this.basic_auth_user, this.basic_auth_pass),
Authorization: "Basic " + encodeBase64(this.basic_auth_user, this.basic_auth_pass),
};
}
@@ -922,7 +917,7 @@ class Monitor extends BeanModel {
);
}
if (!bean.ping) {
if (bean.ping === undefined || bean.ping === null) {
bean.ping = dayjs().valueOf() - startTime;
}
} else if (this.type === "kafka-producer") {
@@ -1572,64 +1567,6 @@ class Monitor extends BeanModel {
return notificationList;
}
/**
* checks certificate chain for expiring certificates
* @param {object} tlsInfoObject Information about certificate
* @returns {Promise<void>}
*/
async checkCertExpiryNotifications(tlsInfoObject) {
if (tlsInfoObject && tlsInfoObject.certInfo && tlsInfoObject.certInfo.daysRemaining) {
const notificationList = await Monitor.getNotificationList(this);
if (!notificationList.length > 0) {
// fail fast. If no notification is set, all the following checks can be skipped.
log.debug("monitor", "No notification, no need to send cert notification");
return;
}
let notifyDays = await setting("tlsExpiryNotifyDays");
if (notifyDays == null || !Array.isArray(notifyDays)) {
// Reset Default
await setSetting("tlsExpiryNotifyDays", [7, 14, 21], "general");
notifyDays = [7, 14, 21];
}
if (Array.isArray(notifyDays)) {
for (const targetDays of notifyDays) {
let certInfo = tlsInfoObject.certInfo;
while (certInfo) {
let subjectCN = certInfo.subject["CN"];
if (rootCertificates.has(certInfo.fingerprint256)) {
log.debug(
"monitor",
`Known root cert: ${certInfo.certType} certificate "${subjectCN}" (${certInfo.daysRemaining} days valid) on ${targetDays} deadline.`
);
break;
} else if (certInfo.daysRemaining > targetDays) {
log.debug(
"monitor",
`No need to send cert notification for ${certInfo.certType} certificate "${subjectCN}" (${certInfo.daysRemaining} days valid) on ${targetDays} deadline.`
);
} else {
log.debug(
"monitor",
`call sendCertNotificationByTargetDays for ${targetDays} deadline on certificate ${subjectCN}.`
);
await this.sendCertNotificationByTargetDays(
subjectCN,
certInfo.certType,
certInfo.daysRemaining,
targetDays,
notificationList
);
}
certInfo = certInfo.issuerCertificate;
}
}
}
}
}
/**
* Send a certificate notification when certificate expires in less
* than target days
@@ -2164,7 +2101,7 @@ class Monitor extends BeanModel {
if (!this.getIgnoreTls() && this.isEnabledExpiryNotification()) {
log.debug("monitor", `[${this.name}] call checkCertExpiryNotifications`);
await this.checkCertExpiryNotifications(tlsInfo);
await checkCertExpiryNotifications(this, tlsInfo);
}
}
}
+482
View File
@@ -0,0 +1,482 @@
const { MonitorType } = require("./monitor-type");
const { Globalping, IpVersion } = require("globalping");
const { Settings } = require("../settings");
const { log, UP, DOWN, evaluateJsonQuery } = require("../../src/util");
const {
checkStatusCode,
getOidcTokenClientCredentials,
encodeBase64,
getDaysRemaining,
checkCertExpiryNotifications,
} = require("../util-server");
const { R } = require("redbean-node");
/**
* Globalping is a free and open-source tool that allows you to run network tests
* and measurements from thousands of community hosted probes around the world.
*
* Library documentation: https://github.com/jsdelivr/globalping-typescript
*
* API documentation: https://globalping.io/docs/api.globalping.io
*/
class GlobalpingMonitorType extends MonitorType {
name = "globalping";
httpUserAgent = "";
/**
* @inheritdoc
*/
constructor(httpUserAgent) {
super();
this.httpUserAgent = httpUserAgent;
}
/**
* @inheritdoc
*/
async check(monitor, heartbeat, _server) {
const apiKey = await Settings.get("globalpingApiToken");
const client = new Globalping({
auth: apiKey,
agent: this.httpUserAgent,
});
const hasAPIToken = !!apiKey;
switch (monitor.subtype) {
case "ping":
await this.ping(client, monitor, heartbeat, hasAPIToken);
break;
case "http":
await this.http(client, monitor, heartbeat, hasAPIToken);
break;
}
}
/**
* Handles ping monitors.
* @param {Client} client - The client object.
* @param {Monitor} monitor - The monitor object.
* @param {Heartbeat} heartbeat - The heartbeat object.
* @param {boolean} hasAPIToken - Whether the monitor has an API token.
* @returns {Promise<void>} A promise that resolves when the ping monitor is handled.
*/
async ping(client, monitor, heartbeat, hasAPIToken) {
const opts = {
type: "ping",
target: monitor.hostname,
inProgressUpdates: false,
limit: 1,
locations: [{ magic: monitor.location }],
measurementOptions: {
packets: monitor.ping_count,
protocol: monitor.protocol,
},
};
if (monitor.protocol === "TCP" && monitor.port) {
opts.measurementOptions.port = monitor.port;
}
if (monitor.ipFamily === "ipv4") {
opts.measurementOptions.ipVersion = IpVersion[4];
} else if (monitor.ipFamily === "ipv6") {
opts.measurementOptions.ipVersion = IpVersion[6];
}
log.debug("monitor", `Globalping create measurement: ${JSON.stringify(opts)}`);
let res = await client.createMeasurement(opts);
if (!res.ok) {
if (Globalping.isHttpStatus(429, res)) {
throw new Error(`Failed to create measurement: ${this.formatTooManyRequestsError(hasAPIToken)}`);
}
throw new Error(`Failed to create measurement: ${this.formatApiError(res.data.error)}`);
}
log.debug("monitor", `Globalping fetch measurement: ${res.data.id}`);
let measurement = await client.awaitMeasurement(res.data.id);
if (!measurement.ok) {
throw new Error(
`Failed to fetch measurement (${res.data.id}): ${this.formatApiError(measurement.data.error)}`
);
}
const probe = measurement.data.results[0].probe;
const result = measurement.data.results[0].result;
if (result.status === "failed") {
heartbeat.msg = this.formatResponse(probe, `Failed: ${result.rawOutput}`);
heartbeat.status = DOWN;
return;
}
if (!result.timings?.length) {
heartbeat.msg = this.formatResponse(probe, `Failed: ${result.rawOutput}`);
heartbeat.status = DOWN;
return;
}
heartbeat.ping = result.stats.avg || 0;
heartbeat.msg = this.formatResponse(probe, "OK");
heartbeat.status = UP;
}
/**
* Handles HTTP monitors.
* @param {Client} client - The client object.
* @param {Monitor} monitor - The monitor object.
* @param {Heartbeat} heartbeat - The heartbeat object.
* @param {boolean} hasAPIToken - Whether the monitor has an API token.
* @returns {Promise<void>} A promise that resolves when the HTTP monitor is handled.
*/
async http(client, monitor, heartbeat, hasAPIToken) {
const url = new URL(monitor.url);
let protocol = url.protocol.replace(":", "").toUpperCase();
if (monitor.protocol === "HTTP2") {
protocol = "HTTP2";
}
const basicAuthHeader = this.getBasicAuthHeader(monitor);
const oauth2AuthHeader = await this.getOauth2AuthHeader(monitor);
const headers = {
...basicAuthHeader,
...oauth2AuthHeader,
...(monitor.headers ? JSON.parse(monitor.headers) : {}),
};
if (monitor.cacheBust) {
const randomFloatString = Math.random().toString(36);
const cacheBust = randomFloatString.substring(2);
url.searchParams.set("uptime_kuma_cachebuster", cacheBust);
}
const opts = {
type: "http",
target: url.hostname,
inProgressUpdates: false,
limit: 1,
locations: [{ magic: monitor.location }],
measurementOptions: {
request: {
host: url.hostname,
path: url.pathname,
query: url.search ? url.search.slice(1) : undefined,
method: monitor.method,
headers,
},
protocol: protocol,
},
};
if (url.port) {
opts.measurementOptions.port = parseInt(url.port);
}
if (monitor.ipFamily === "ipv4") {
opts.measurementOptions.ipVersion = IpVersion[4];
} else if (monitor.ipFamily === "ipv6") {
opts.measurementOptions.ipVersion = IpVersion[6];
}
if (monitor.dns_resolve_server) {
opts.measurementOptions.resolver = monitor.dns_resolve_server;
}
log.debug("monitor", `Globalping create measurement: ${JSON.stringify(opts)}`);
let res = await client.createMeasurement(opts);
if (!res.ok) {
if (Globalping.isHttpStatus(429, res)) {
throw new Error(`Failed to create measurement: ${this.formatTooManyRequestsError(hasAPIToken)}`);
}
throw new Error(`Failed to create measurement: ${this.formatApiError(res.data.error)}`);
}
log.debug("monitor", `Globalping fetch measurement: ${res.data.id}`);
let measurement = await client.awaitMeasurement(res.data.id);
if (!measurement.ok) {
throw new Error(
`Failed to fetch measurement (${res.data.id}): ${this.formatApiError(measurement.data.error)}`
);
}
const probe = measurement.data.results[0].probe;
const result = measurement.data.results[0].result;
if (result.status === "failed") {
heartbeat.msg = this.formatResponse(probe, `Failed: ${result.rawOutput}`);
heartbeat.status = DOWN;
return;
}
heartbeat.ping = result.timings.total || 0;
if (!checkStatusCode(result.statusCode, JSON.parse(monitor.accepted_statuscodes_json))) {
heartbeat.msg = this.formatResponse(
probe,
`Status code ${result.statusCode} not accepted. Output: ${result.rawOutput}`
);
heartbeat.status = DOWN;
return;
}
heartbeat.msg = this.formatResponse(probe, `${result.statusCode} - ${result.statusCodeName}`);
// keyword
if (monitor.keyword) {
await this.handleKeywordForHTTP(monitor, heartbeat, result, probe);
return;
}
// json-query
if (monitor.expectedValue) {
await this.handleJSONQueryForHTTP(monitor, heartbeat, result, probe);
return;
}
await this.handleTLSInfo(monitor, protocol, probe, result.tls);
heartbeat.msg = this.formatResponse(probe, "OK");
heartbeat.status = UP;
}
/**
* Handles keyword for HTTP monitors.
* @param {Monitor} monitor - The monitor object.
* @param {Heartbeat} heartbeat - The heartbeat object.
* @param {Result} result - The result object.
* @param {Probe} probe - The probe object.
* @returns {Promise<void>} A promise that resolves when the keyword is handled.
*/
async handleKeywordForHTTP(monitor, heartbeat, result, probe) {
let data = result.rawOutput;
let keywordFound = data.includes(monitor.keyword);
if (keywordFound === Boolean(monitor.invertKeyword)) {
data = data.replace(/<[^>]*>?|[\n\r]|\s+/gm, " ").trim();
if (data.length > 50) {
data = data.substring(0, 47) + "...";
}
throw new Error(
heartbeat.msg + ", but keyword is " + (keywordFound ? "present" : "not") + " in [" + data + "]"
);
}
heartbeat.msg += ", keyword " + (keywordFound ? "is" : "not") + " found";
heartbeat.status = UP;
}
/**
* Handles JSON query for HTTP monitors.
* @param {Monitor} monitor - The monitor object.
* @param {Heartbeat} heartbeat - The heartbeat object.
* @param {Result} result - The result object.
* @param {Probe} probe - The probe object.
* @returns {Promise<void>} A promise that resolves when the JSON query is handled.
*/
async handleJSONQueryForHTTP(monitor, heartbeat, result, probe) {
const { status, response } = await evaluateJsonQuery(
result.rawOutput,
monitor.jsonPath,
monitor.jsonPathOperator,
monitor.expectedValue
);
if (!status) {
throw new Error(
this.formatResponse(
probe,
`JSON query does not pass (comparing ${response} ${monitor.jsonPathOperator} ${monitor.expectedValue})`
)
);
}
heartbeat.msg = this.formatResponse(
probe,
`JSON query passes (comparing ${response} ${monitor.jsonPathOperator} ${monitor.expectedValue})`
);
heartbeat.status = UP;
}
/**
* Updates the TLS information for a monitor.
* @param {object} monitor - The monitor object.
* @param {string} protocol - The protocol used for the monitor.
* @param {object} probe - The probe object containing location information.
* @param {object} tlsInfo - The TLS information object.
* @returns {Promise<void>}
*/
async handleTLSInfo(monitor, protocol, probe, tlsInfo) {
if (!tlsInfo) {
return;
}
if (!monitor.ignoreTls && protocol === "HTTPS" && !tlsInfo.authorized) {
throw new Error(this.formatResponse(probe, `TLS certificate is not authorized: ${tlsInfo.error}`));
}
let tlsInfoBean = await R.findOne("monitor_tls_info", "monitor_id = ?", [monitor.id]);
if (tlsInfoBean == null) {
tlsInfoBean = R.dispense("monitor_tls_info");
tlsInfoBean.monitor_id = monitor.id;
} else {
try {
let oldCertInfo = JSON.parse(tlsInfoBean.info_json);
if (
oldCertInfo &&
oldCertInfo.certInfo &&
oldCertInfo.certInfo.fingerprint256 !== tlsInfo.fingerprint256
) {
log.debug("monitor", "Resetting sent_history");
await R.exec(
"DELETE FROM notification_sent_history WHERE type = 'certificate' AND monitor_id = ?",
[monitor.id]
);
}
} catch (e) {}
}
const validTo = new Date(tlsInfo.expiresAt);
const certResult = {
valid: tlsInfo.authorized,
certInfo: {
subject: tlsInfo.subject,
issuer: tlsInfo.issuer,
validTo: validTo,
daysRemaining: getDaysRemaining(new Date(), validTo),
fingerprint: tlsInfo.fingerprint256,
fingerprint256: tlsInfo.fingerprint256,
certType: "",
},
};
tlsInfoBean.info_json = JSON.stringify(certResult);
await R.store(tlsInfoBean);
if (monitor.prometheus) {
monitor.prometheus.update(null, certResult);
}
if (!monitor.ignoreTls && monitor.expiryNotification) {
await checkCertExpiryNotifications(monitor, certResult);
}
}
/**
* Generates the OAuth2 authorization header for the monitor if it is enabled.
* @param {object} monitor - The monitor object containing authentication information.
* @returns {Promise<object>} The OAuth2 authorization header.
*/
async getOauth2AuthHeader(monitor) {
if (monitor.auth_method !== "oauth2-cc") {
return {};
}
try {
if (new Date((monitor.oauthAccessToken?.expires_at || 0) * 1000) <= new Date()) {
const oAuthAccessToken = await getOidcTokenClientCredentials(
monitor.oauth_token_url,
monitor.oauth_client_id,
monitor.oauth_client_secret,
monitor.oauth_scopes,
monitor.oauth_audience,
monitor.oauth_auth_method
);
log.debug(
"monitor",
`[${monitor.name}] Obtained oauth access-token. Expires at ${new Date(oAuthAccessToken.expires_at * 1000)}`
);
monitor.oauthAccessToken = oAuthAccessToken;
}
return {
Authorization: monitor.oauthAccessToken.token_type + " " + monitor.oauthAccessToken.access_token,
};
} catch (e) {
throw new Error("The oauth config is invalid. " + e.message);
}
}
/**
* Generates the basic authentication header for a monitor if it is enabled.
* @param {object} monitor - The monitor object.
* @returns {object} The basic authentication header.
*/
getBasicAuthHeader(monitor) {
if (monitor.auth_method !== "basic") {
return {};
}
return {
Authorization: "Basic " + encodeBase64(monitor.basic_auth_user, monitor.basic_auth_pass),
};
}
/**
* Generates a formatted error message for API errors.
* @param {Error} error - The API error object.
* @returns {string} The formatted error message.
*/
formatApiError(error) {
let str = `${error.type} ${error.message}.`;
if (error.params) {
for (const key in error.params) {
str += `\n${key}: ${error.params[key]}`;
}
}
return str;
}
/**
* Generates a formatted error message for too many requests.
* @param {boolean} hasAPIToken - Indicates whether an API token is available.
* @returns {string} The formatted error message.
*/
formatTooManyRequestsError(hasAPIToken) {
const creditsHelpLink = "https://dash.globalping.io?view=add-credits";
if (hasAPIToken) {
return `You have run out of credits. Get higher limits by sponsoring us or hosting probes. Learn more at ${creditsHelpLink}.`;
}
return `You have run out of credits. Get higher limits by creating an account. Sign up at ${creditsHelpLink}.`;
}
/**
* Returns the formatted probe location string. e.g "Ashburn (VA), US, NA, Amazon.com (AS14618), (aws-us-east-1)"
* @param {object} probe - The probe object containing location information.
* @returns {string} The formatted probe location string.
*/
formatProbeLocation(probe) {
let tag = "";
for (const t of probe.tags) {
// If tag ends in a number, it's likely a region code and should be displayed
if (Number.isInteger(Number(t.slice(-1)))) {
tag = t;
break;
}
}
return `${probe.city}${probe.state ? ` (${probe.state})` : ""}, ${probe.country}, ${probe.continent}, ${
probe.network
} (AS${probe.asn})${tag ? `, (${tag})` : ""}`;
}
/**
* Formats the response text with the probe location.
* @param {object} probe - The probe object containing location information.
* @param {string} text - The response text to append.
* @returns {string} The formatted response text.
*/
formatResponse(probe, text) {
return `${this.formatProbeLocation(probe)} : ${text}`;
}
}
module.exports = {
GlobalpingMonitorType,
};
@@ -45,6 +45,15 @@ class NotificationProvider {
return monitorJSON["hostname"] + ":" + monitorJSON["port"];
}
return monitorJSON["hostname"];
case "globalping":
switch (monitorJSON["subtype"]) {
case "ping":
return monitorJSON["hostname"];
case "http":
return monitorJSON["url"];
default:
return "";
}
default:
if (!["https://", "http://", ""].includes(monitorJSON["url"])) {
return monitorJSON["url"];
+4 -1
View File
@@ -745,7 +745,7 @@ let needSetup = false;
* List of frontend-only properties that should not be saved to the database.
* Should clean up before saving to the database.
*/
const frontendOnlyProperties = ["humanReadableInterval"];
const frontendOnlyProperties = ["humanReadableInterval", "responsecheck"];
for (const prop of frontendOnlyProperties) {
if (prop in monitor) {
delete monitor[prop];
@@ -823,6 +823,7 @@ let needSetup = false;
bean.description = monitor.description;
bean.parent = monitor.parent;
bean.type = monitor.type;
bean.subtype = monitor.subtype;
bean.url = monitor.url;
bean.wsIgnoreSecWebsocketAcceptHeader = monitor.wsIgnoreSecWebsocketAcceptHeader;
bean.wsSubprotocol = monitor.wsSubprotocol;
@@ -849,6 +850,8 @@ let needSetup = false;
bean.game = monitor.game;
bean.maxretries = monitor.maxretries;
bean.port = parseInt(monitor.port);
bean.location = monitor.location;
bean.protocol = monitor.protocol;
if (isNaN(bean.port)) {
bean.port = null;
+2
View File
@@ -126,6 +126,7 @@ class UptimeKumaServer {
UptimeKumaServer.monitorTypeList["gamedig"] = new GameDigMonitorType();
UptimeKumaServer.monitorTypeList["port"] = new TCPMonitorType();
UptimeKumaServer.monitorTypeList["manual"] = new ManualMonitorType();
UptimeKumaServer.monitorTypeList["globalping"] = new GlobalpingMonitorType(this.getUserAgent());
UptimeKumaServer.monitorTypeList["redis"] = new RedisMonitorType();
UptimeKumaServer.monitorTypeList["system-service"] = new SystemServiceMonitorType();
UptimeKumaServer.monitorTypeList["sqlserver"] = new MssqlMonitorType();
@@ -576,6 +577,7 @@ const { SIPMonitorType } = require("./monitor-types/sip-options");
const { GameDigMonitorType } = require("./monitor-types/gamedig");
const { TCPMonitorType } = require("./monitor-types/tcp.js");
const { ManualMonitorType } = require("./monitor-types/manual");
const { GlobalpingMonitorType } = require("./monitor-types/globalping");
const { RedisMonitorType } = require("./monitor-types/redis");
const { SystemServiceMonitorType } = require("./monitor-types/system-service");
const { MssqlMonitorType } = require("./monitor-types/mssql");
+101
View File
@@ -414,6 +414,32 @@ exports.setSettings = async function (type, data) {
await Settings.setSettings(type, data);
};
// ssl-checker by @dyaa
//https://github.com/dyaa/ssl-checker/blob/master/src/index.ts
/**
* Get number of days between two dates
* @param {Date} validFrom Start date
* @param {Date} validTo End date
* @returns {number} Number of days
*/
const getDaysBetween = (validFrom, validTo) => Math.round(Math.abs(+validFrom - +validTo) / 8.64e7);
/**
* Get days remaining from a time range
* @param {Date} validFrom Start date
* @param {Date} validTo End date
* @returns {number} Number of days remaining
*/
const getDaysRemaining = (validFrom, validTo) => {
const daysRemaining = getDaysBetween(validFrom, validTo);
if (new Date(validTo).getTime() < new Date().getTime()) {
return -daysRemaining;
}
return daysRemaining;
};
module.exports.getDaysRemaining = getDaysRemaining;
/**
* Fix certificate info for display
* @param {object} info The chain obtained from getPeerCertificate()
@@ -869,6 +895,81 @@ function fsExists(path) {
}
module.exports.fsExists = fsExists;
/**
* Encode user and password to Base64 encoding
* for HTTP "basic" auth, as per RFC-7617
* @param {string|null} user - The username (defaults to empty string if null/undefined)
* @param {string|null} pass - The password (defaults to empty string if null/undefined)
* @returns {string} Encoded Base64 string
*/
function encodeBase64(user, pass) {
return Buffer.from(`${user || ""}:${pass || ""}`).toString("base64");
}
module.exports.encodeBase64 = encodeBase64;
/**
* checks certificate chain for expiring certificates
* @param {object} monitor - The monitor object
* @param {object} tlsInfoObject Information about certificate
* @returns {Promise<void>}
*/
async function checkCertExpiryNotifications(monitor, tlsInfoObject) {
if (!tlsInfoObject || !tlsInfoObject.certInfo || !tlsInfoObject.certInfo.daysRemaining) {
return;
}
let notificationList = await R.getAll(
"SELECT notification.* FROM notification, monitor_notification WHERE monitor_id = ? AND monitor_notification.notification_id = notification.id ",
[monitor.id]
);
if (!notificationList.length > 0) {
// fail fast. If no notification is set, all the following checks can be skipped.
log.debug("monitor", "No notification, no need to send cert notification");
return;
}
let notifyDays = await Settings.get("tlsExpiryNotifyDays");
if (notifyDays == null || !Array.isArray(notifyDays)) {
// Reset Default
await Settings.setSetting("tlsExpiryNotifyDays", [7, 14, 21], "general");
notifyDays = [7, 14, 21];
}
for (const targetDays of notifyDays) {
let certInfo = tlsInfoObject.certInfo;
while (certInfo) {
let subjectCN = certInfo.subject["CN"];
if (monitor.rootCertificates.has(certInfo.fingerprint256)) {
log.debug(
"monitor",
`Known root cert: ${certInfo.certType} certificate "${subjectCN}" (${certInfo.daysRemaining} days valid) on ${targetDays} deadline.`
);
break;
} else if (certInfo.daysRemaining > targetDays) {
log.debug(
"monitor",
`No need to send cert notification for ${certInfo.certType} certificate "${subjectCN}" (${certInfo.daysRemaining} days valid) on ${targetDays} deadline.`
);
} else {
log.debug(
"monitor",
`call sendCertNotificationByTargetDays for ${targetDays} deadline on certificate ${subjectCN}.`
);
await monitor.sendCertNotificationByTargetDays(
subjectCN,
certInfo.certType,
certInfo.daysRemaining,
targetDays,
notificationList
);
}
certInfo = certInfo.issuerCertificate;
}
}
}
module.exports.checkCertExpiryNotifications = checkCertExpiryNotifications;
/**
* By default, command-exists will throw a null error if the command does not exist, which is ugly. The function makes it better.
* Read more: https://github.com/mathisonian/command-exists/issues/22