mirror of
https://github.com/koichixD/uptime-kuma.git
synced 2026-09-04 17:23:21 +00:00
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:
co-authored by
Frank Elsinga
autofix-ci[bot]
parent
66c8bac506
commit
bad679ee47
+10
-73
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user