mirror of
https://github.com/louislam/uptime-kuma.git
synced 2026-02-03 18:27:45 -06: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:
parent
66c8bac506
commit
bad679ee47
17
db/knex_migrations/2025-12-17-0000-add-globalping-monitor.js
Normal file
17
db/knex_migrations/2025-12-17-0000-add-globalping-monitor.js
Normal file
@ -0,0 +1,17 @@
|
||||
exports.up = function (knex) {
|
||||
// Add new columns
|
||||
return knex.schema.alterTable("monitor", function (table) {
|
||||
table.string("subtype", 10).nullable();
|
||||
table.string("location", 255).nullable();
|
||||
table.string("protocol", 20).nullable();
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function (knex) {
|
||||
// Drop columns
|
||||
return knex.schema.alterTable("monitor", function (table) {
|
||||
table.dropColumn("subtype");
|
||||
table.dropColumn("location");
|
||||
table.dropColumn("protocol");
|
||||
});
|
||||
};
|
||||
21
package-lock.json
generated
21
package-lock.json
generated
@ -35,6 +35,7 @@
|
||||
"feed": "^4.2.2",
|
||||
"form-data": "~4.0.0",
|
||||
"gamedig": "^5.0.1",
|
||||
"globalping": "^0.2.0",
|
||||
"html-escaper": "^3.0.3",
|
||||
"http-cookie-agent": "~5.0.4",
|
||||
"http-graceful-shutdown": "~3.1.7",
|
||||
@ -3555,6 +3556,18 @@
|
||||
"@hapi/hoek": "^9.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@hey-api/client-fetch": {
|
||||
"version": "0.10.2",
|
||||
"resolved": "https://registry.npmjs.org/@hey-api/client-fetch/-/client-fetch-0.10.2.tgz",
|
||||
"integrity": "sha512-AGiFYDx+y8VT1wlQ3EbzzZtfU8EfV+hLLRTtr8Y/tjYZaxIECwJagVZf24YzNbtEBXONFV50bwcU1wLVGXe1ow==",
|
||||
"deprecated": "Starting with v0.73.0, this package is bundled directly inside @hey-api/openapi-ts.",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/hey-api"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@hey-api/openapi-ts": "< 2"
|
||||
}
|
||||
},
|
||||
"node_modules/@humanwhocodes/config-array": {
|
||||
"version": "0.9.5",
|
||||
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.9.5.tgz",
|
||||
@ -11373,6 +11386,14 @@
|
||||
"which": "bin/which"
|
||||
}
|
||||
},
|
||||
"node_modules/globalping": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/globalping/-/globalping-0.2.0.tgz",
|
||||
"integrity": "sha512-rxyvqXF/oz5yBGJHR58TEsCkcbc4WQVmdKC+uZ8LXfIt4KwTdTvbzkfwhhjuyUmuQr8U/b8m/ZicdrA8ZIroKw==",
|
||||
"dependencies": {
|
||||
"@hey-api/client-fetch": "^0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/globals": {
|
||||
"version": "13.24.0",
|
||||
"resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz",
|
||||
|
||||
@ -96,6 +96,7 @@
|
||||
"express-static-gzip": "~2.1.7",
|
||||
"feed": "^4.2.2",
|
||||
"form-data": "~4.0.0",
|
||||
"globalping": "^0.2.0",
|
||||
"gamedig": "^5.0.1",
|
||||
"html-escaper": "^3.0.3",
|
||||
"http-cookie-agent": "~5.0.4",
|
||||
|
||||
@ -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
server/monitor-types/globalping.js
Normal file
482
server/monitor-types/globalping.js
Normal 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"];
|
||||
|
||||
@ -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
|
||||
|
||||
@ -135,6 +135,21 @@
|
||||
</i18n-t>
|
||||
</div>
|
||||
|
||||
<!-- Globalping API Token -->
|
||||
<div class="mb-4">
|
||||
<label class="form-label" for="globalpingApiToken">
|
||||
{{ $t("Globalping API Token") }}
|
||||
</label>
|
||||
<HiddenInput
|
||||
id="globalpingApiToken"
|
||||
v-model="settings.globalpingApiToken"
|
||||
autocomplete="new-password"
|
||||
/>
|
||||
<i18n-t keypath="globalpingApiTokenDescription" tag="div" class="form-text">
|
||||
<a href="https://dash.globalping.io" target="_blank">https://dash.globalping.io</a>
|
||||
</i18n-t>
|
||||
</div>
|
||||
|
||||
<!-- DNS Cache (nscd) -->
|
||||
<div v-if="$root.info.isContainer" class="mb-4">
|
||||
<label class="form-label">
|
||||
|
||||
@ -1319,6 +1319,21 @@
|
||||
"Send UP silently": "Send UP silently",
|
||||
"Send DOWN silently": "Send DOWN silently",
|
||||
"Installing a Nextcloud Talk bot requires administrative access to the server.": "Installing a Nextcloud Talk bot requires administrative access to the server.",
|
||||
"Globalping - Access global monitoring probes": "Globalping - Access global monitoring probes",
|
||||
"GlobalpingDescription": "Globalping provides access to thousands of community hosted probes to run network tests and measurements. A limit of 250 tests per hour is set for all anonymous users. To double the limit to 500 per hour please save your token in {accountSettings}.",
|
||||
"Globalping API Token": "Globalping API Token",
|
||||
"globalpingApiTokenDescription": "Get your Globalping API Token at {0}.",
|
||||
"GlobalpingHostname": "A publicly reachable measurement target. Typically a hostname or IPv4/IPv6 address, depending on the measurement type.",
|
||||
"GlobalpingLocation": "The location field accepts continents, countries, regions, cities, ASNs, ISPs, or cloud regions. You can combine filters with {plus} (e.g {amazonPlusGermany} or {comcastPlusCalifornia}). If latency is an important metric, use filters to narrow down the location to a small region to avoid spikes. {fullDocs}.",
|
||||
"GlobalpingLocationDocs": "Full location input documentation",
|
||||
"GlobalpingIpFamilyInfo": "The IP version to use. Only allowed if the target is a hostname.",
|
||||
"GlobalpingResolverInfo": "IPv4/IPv6 address or a fully Qualified Domain Name (FQDN). Defaults to the probe's local network resolver. You can change the resolver server anytime.",
|
||||
"Resolver Server": "Resolver Server",
|
||||
"Protocol": "Protocol",
|
||||
"account settings": "account settings",
|
||||
"Location": "Location",
|
||||
"Monitor Subtype": "Monitor Subtype",
|
||||
"Check for": "Check for",
|
||||
"Number of retry attempts if webhook fails": "Number of retry attempts (every 60-180 seconds) if the webhook fails.",
|
||||
"Maximum Retries": "Maximum Retries",
|
||||
"Template ID": "Template ID",
|
||||
|
||||
@ -35,6 +35,16 @@
|
||||
</a>
|
||||
<span v-if="monitor.type === 'port'">TCP Port {{ monitor.hostname }}:{{ monitor.port }}</span>
|
||||
<span v-if="monitor.type === 'ping'">Ping: {{ monitor.hostname }}</span>
|
||||
<span v-if="monitor.type === 'globalping'">
|
||||
<a v-if="monitor.subtype === 'http'" :href="monitor.url" target="_blank" rel="noopener noreferrer">
|
||||
{{ filterPassword(monitor.url) }}
|
||||
</a>
|
||||
<span v-if="monitor.hostname">{{ monitor.hostname }}</span>
|
||||
<br />
|
||||
<span>{{ $t("Location") }}:</span>
|
||||
<span class="keyword">{{ monitor.location }}</span>
|
||||
<br />
|
||||
</span>
|
||||
<span v-if="monitor.type === 'keyword'">
|
||||
<br />
|
||||
<span>{{ $t("Keyword") }}:</span>
|
||||
|
||||
@ -8,6 +8,17 @@
|
||||
<div class="col-md-6">
|
||||
<h2 class="mb-2">{{ $t("General") }}</h2>
|
||||
|
||||
<i18n-t
|
||||
v-if="monitor.type === 'globalping'"
|
||||
keypath="GlobalpingDescription"
|
||||
tag="p"
|
||||
class="form-text"
|
||||
>
|
||||
<template #accountSettings>
|
||||
<router-link to="/settings/general">{{ $t("account settings") }}</router-link>
|
||||
</template>
|
||||
</i18n-t>
|
||||
|
||||
<div class="my-3">
|
||||
<label for="type" class="form-label">{{ $t("Monitor Type") }}</label>
|
||||
<select
|
||||
@ -55,6 +66,9 @@
|
||||
</optgroup>
|
||||
|
||||
<optgroup :label="$t('Specific Monitor Type')">
|
||||
<option value="globalping">
|
||||
{{ $t("Globalping - Access global monitoring probes") }}
|
||||
</option>
|
||||
<option value="steam">
|
||||
{{ $t("Steam Game Server") }}
|
||||
</option>
|
||||
@ -98,6 +112,19 @@
|
||||
{{ $t("tailscalePingWarning") }}
|
||||
</div>
|
||||
|
||||
<div v-if="monitor.type === 'globalping'" class="my-3">
|
||||
<label for="subtype" class="form-label">{{ $t("Monitor Subtype") }}</label>
|
||||
<select
|
||||
id="subtype"
|
||||
v-model="monitor.subtype"
|
||||
class="form-select"
|
||||
data-testid="monitor-subtype-select"
|
||||
>
|
||||
<option value="ping">Ping</option>
|
||||
<option value="http">HTTP(s)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div v-if="monitor.type === 'sip-options'" class="alert alert-warning" role="alert">
|
||||
{{ $t("sipsakPingWarning") }}
|
||||
</div>
|
||||
@ -442,6 +469,116 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Globalping -->
|
||||
<template v-if="monitor.type === 'globalping'">
|
||||
<!-- Hostname -->
|
||||
<div v-if="monitor.subtype === 'ping'" class="my-3">
|
||||
<label for="hostname" class="form-label">{{ $t("Hostname") }}</label>
|
||||
<input
|
||||
id="hostname"
|
||||
v-model="monitor.hostname"
|
||||
type="text"
|
||||
class="form-control"
|
||||
:pattern="ipOrHostnameRegexPattern"
|
||||
required
|
||||
data-testid="hostname-input"
|
||||
/>
|
||||
<div class="form-text">
|
||||
{{ $t("GlobalpingHostname") }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="monitor.subtype === 'http'" class="my-3">
|
||||
<label for="url" class="form-label">{{ $t("URL") }}</label>
|
||||
<input
|
||||
id="url"
|
||||
v-model="monitor.url"
|
||||
type="url"
|
||||
class="form-control"
|
||||
pattern="https?://.+"
|
||||
required
|
||||
data-testid="url-input"
|
||||
/>
|
||||
<div class="form-text">
|
||||
{{ $t("GlobalpingHostname") }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Location -->
|
||||
<div class="my-3">
|
||||
<label for="location" class="form-label">{{ $t("Location") }}</label>
|
||||
<input
|
||||
id="location"
|
||||
v-model="monitor.location"
|
||||
type="text"
|
||||
class="form-control"
|
||||
required
|
||||
/>
|
||||
<i18n-t keypath="GlobalpingLocation" tag="div" class="form-text">
|
||||
<template #plus>
|
||||
<code>+</code>
|
||||
</template>
|
||||
<template #amazonPlusGermany>
|
||||
<code>amazon+germany</code>
|
||||
</template>
|
||||
<template #comcastPlusCalifornia>
|
||||
<code>comcast+california</code>
|
||||
</template>
|
||||
<template #fullDocs>
|
||||
<a
|
||||
href="https://github.com/jsdelivr/globalping?tab=readme-ov-file#basic-location-targeting-"
|
||||
target="_blank"
|
||||
>
|
||||
{{ $t("GlobalpingLocationDocs") }}
|
||||
</a>
|
||||
</template>
|
||||
</i18n-t>
|
||||
</div>
|
||||
|
||||
<!-- IP Family -->
|
||||
<div class="my-3">
|
||||
<label for="ipFamily" class="form-label">{{ $t("Ip Family") }}</label>
|
||||
<select id="ipFamily" v-model="monitor.ipFamily" class="form-select">
|
||||
<option :value="null">{{ $t("auto-select") }}</option>
|
||||
<option value="ipv4">IPv4</option>
|
||||
<option value="ipv6">IPv6</option>
|
||||
</select>
|
||||
<div class="form-text">
|
||||
{{ $t("GlobalpingIpFamilyInfo") }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="monitor.subtype === 'http'" class="my-3">
|
||||
<label for="dns_resolve_server" class="form-label">
|
||||
{{ $t("Resolver Server") }}
|
||||
</label>
|
||||
<input
|
||||
id="dns_resolve_server"
|
||||
v-model="monitor.dns_resolve_server"
|
||||
type="text"
|
||||
class="form-control"
|
||||
/>
|
||||
<div class="form-text">
|
||||
{{ $t("GlobalpingResolverInfo") }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Protocol -->
|
||||
<div class="my-3">
|
||||
<label for="protocol" class="form-label">{{ $t("Protocol") }}</label>
|
||||
<select id="protocol" v-model="monitor.protocol" class="form-select" required>
|
||||
<template v-if="monitor.subtype === 'ping'">
|
||||
<option value="ICMP">ICMP</option>
|
||||
<option value="TCP">TCP</option>
|
||||
</template>
|
||||
<template v-else-if="monitor.subtype === 'http'">
|
||||
<option :value="null">{{ $t("auto-select") }}</option>
|
||||
<option value="HTTP2">HTTP2</option>
|
||||
</template>
|
||||
</select>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Port -->
|
||||
<!-- For TCP Port / Steam / MQTT / Radius Type / SNMP / SIP Options -->
|
||||
<div
|
||||
@ -453,7 +590,10 @@
|
||||
monitor.type === 'radius' ||
|
||||
monitor.type === 'smtp' ||
|
||||
monitor.type === 'snmp' ||
|
||||
monitor.type === 'sip-options'
|
||||
monitor.type === 'sip-options' ||
|
||||
(monitor.type === 'globalping' &&
|
||||
monitor.subtype === 'ping' &&
|
||||
monitor.protocol === 'TCP')
|
||||
"
|
||||
class="my-3"
|
||||
>
|
||||
@ -1260,7 +1400,9 @@
|
||||
monitor.type === 'http' ||
|
||||
monitor.type === 'keyword' ||
|
||||
monitor.type === 'json-query' ||
|
||||
(monitor.type === 'port' && ['starttls', 'secure'].includes(monitor.smtpSecurity))
|
||||
(monitor.type === 'port' &&
|
||||
['starttls', 'secure'].includes(monitor.smtpSecurity)) ||
|
||||
(monitor.type === 'globalping' && monitor.subtype === 'http')
|
||||
"
|
||||
class="my-3 form-check"
|
||||
:title="monitor.ignoreTls ? $t('ignoredTLSError') : ''"
|
||||
@ -1348,7 +1490,8 @@
|
||||
monitor.type === 'http' ||
|
||||
monitor.type === 'keyword' ||
|
||||
monitor.type === 'json-query' ||
|
||||
monitor.type === 'redis'
|
||||
monitor.type === 'redis' ||
|
||||
(monitor.type === 'globalping' && monitor.subtype === 'http')
|
||||
"
|
||||
class="my-3 form-check"
|
||||
>
|
||||
@ -1368,7 +1511,8 @@
|
||||
v-if="
|
||||
monitor.type === 'http' ||
|
||||
monitor.type === 'keyword' ||
|
||||
monitor.type === 'json-query'
|
||||
monitor.type === 'json-query' ||
|
||||
(monitor.type === 'globalping' && monitor.subtype === 'http')
|
||||
"
|
||||
class="my-3 form-check"
|
||||
>
|
||||
@ -1427,7 +1571,13 @@
|
||||
</div>
|
||||
|
||||
<!-- Max Packets / Count -->
|
||||
<div v-if="monitor.type === 'ping'" class="my-3">
|
||||
<div
|
||||
v-if="
|
||||
monitor.type === 'ping' ||
|
||||
(monitor.type === 'globalping' && monitor.subtype === 'ping')
|
||||
"
|
||||
class="my-3"
|
||||
>
|
||||
<label for="ping-count" class="form-label">{{ $t("pingCountLabel") }}</label>
|
||||
<input
|
||||
id="ping-count"
|
||||
@ -1688,6 +1838,31 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Globalping Accepted Status Codes -->
|
||||
<div v-if="monitor.type === 'globalping' && monitor.subtype === 'http'" class="my-3">
|
||||
<label for="acceptedStatusCodes" class="form-label">
|
||||
{{ $t("Accepted Status Codes") }}
|
||||
</label>
|
||||
|
||||
<VueMultiselect
|
||||
id="acceptedStatusCodes"
|
||||
v-model="monitor.accepted_statuscodes"
|
||||
:options="acceptedStatusCodeOptions"
|
||||
:multiple="true"
|
||||
:close-on-select="false"
|
||||
:clear-on-select="false"
|
||||
:preserve-search="true"
|
||||
:placeholder="$t('Pick Accepted Status Codes...')"
|
||||
:preselect-first="false"
|
||||
:max-height="600"
|
||||
:taggable="true"
|
||||
></VueMultiselect>
|
||||
|
||||
<div class="form-text">
|
||||
{{ $t("acceptedStatusCodesDescription") }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Parent Monitor -->
|
||||
<div class="my-3">
|
||||
<label for="monitorGroupSelector" class="form-label">{{ $t("Monitor Group") }}</label>
|
||||
@ -2156,6 +2331,281 @@
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<!-- Globalping HTTP Options -->
|
||||
<template v-if="monitor.type === 'globalping' && monitor.subtype === 'http'">
|
||||
<h2 class="mt-5 mb-2">{{ $t("HTTP Options") }}</h2>
|
||||
|
||||
<!-- Method -->
|
||||
<div class="my-3">
|
||||
<label for="method" class="form-label">{{ $t("Method") }}</label>
|
||||
<select id="method" v-model="monitor.method" class="form-select">
|
||||
<option value="HEAD">HEAD</option>
|
||||
<option value="GET">GET</option>
|
||||
<option value="OPTIONS">OPTIONS</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Headers -->
|
||||
<div class="my-3">
|
||||
<label for="headers" class="form-label">{{ $t("Headers") }}</label>
|
||||
<textarea
|
||||
id="headers"
|
||||
v-model="monitor.headers"
|
||||
class="form-control"
|
||||
:placeholder="headersPlaceholder"
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<!-- HTTP Auth -->
|
||||
<h4 class="mt-5 mb-2">{{ $t("Authentication") }}</h4>
|
||||
|
||||
<!-- Method -->
|
||||
<div class="my-3">
|
||||
<label for="method" class="form-label">{{ $t("Method") }}</label>
|
||||
<select id="method" v-model="monitor.authMethod" class="form-select">
|
||||
<option :value="null">
|
||||
{{ $t("None") }}
|
||||
</option>
|
||||
<option value="basic">
|
||||
{{ $t("HTTP Basic Auth") }}
|
||||
</option>
|
||||
<option value="oauth2-cc">
|
||||
{{ $t("OAuth2: Client Credentials") }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<template v-if="monitor.authMethod === 'basic'">
|
||||
<div class="my-3">
|
||||
<label for="basicauth-user" class="form-label">{{ $t("Username") }}</label>
|
||||
<input
|
||||
id="basicauth-user"
|
||||
v-model="monitor.basic_auth_user"
|
||||
type="text"
|
||||
class="form-control"
|
||||
:placeholder="$t('Username')"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="my-3">
|
||||
<label for="basicauth-pass" class="form-label">{{ $t("Password") }}</label>
|
||||
<input
|
||||
id="basicauth-pass"
|
||||
v-model="monitor.basic_auth_pass"
|
||||
type="password"
|
||||
autocomplete="new-password"
|
||||
class="form-control"
|
||||
:placeholder="$t('Password')"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="monitor.authMethod === 'oauth2-cc'">
|
||||
<div class="my-3">
|
||||
<label for="oauth_auth_method" class="form-label">
|
||||
{{ $t("Authentication Method") }}
|
||||
</label>
|
||||
<select
|
||||
id="oauth_auth_method"
|
||||
v-model="monitor.oauth_auth_method"
|
||||
class="form-select"
|
||||
>
|
||||
<option value="client_secret_basic">
|
||||
{{ $t("Authorization Header") }}
|
||||
</option>
|
||||
<option value="client_secret_post">
|
||||
{{ $t("Form Data Body") }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="my-3">
|
||||
<label for="oauth_token_url" class="form-label">
|
||||
{{ $t("OAuth Token URL") }}
|
||||
</label>
|
||||
<input
|
||||
id="oauth_token_url"
|
||||
v-model="monitor.oauth_token_url"
|
||||
type="text"
|
||||
class="form-control"
|
||||
:placeholder="$t('OAuth Token URL')"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div class="my-3">
|
||||
<label for="oauth_client_id" class="form-label">{{ $t("Client ID") }}</label>
|
||||
<input
|
||||
id="oauth_client_id"
|
||||
v-model="monitor.oauth_client_id"
|
||||
type="text"
|
||||
class="form-control"
|
||||
:placeholder="$t('Client ID')"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<template
|
||||
v-if="
|
||||
monitor.oauth_auth_method === 'client_secret_post' ||
|
||||
monitor.oauth_auth_method === 'client_secret_basic'
|
||||
"
|
||||
>
|
||||
<div class="my-3">
|
||||
<label for="oauth_client_secret" class="form-label">
|
||||
{{ $t("Client Secret") }}
|
||||
</label>
|
||||
<input
|
||||
id="oauth_client_secret"
|
||||
v-model="monitor.oauth_client_secret"
|
||||
type="password"
|
||||
class="form-control"
|
||||
:placeholder="$t('Client Secret')"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div class="my-3">
|
||||
<label for="oauth_scopes" class="form-label">{{ $t("OAuth Scope") }}</label>
|
||||
<input
|
||||
id="oauth_scopes"
|
||||
v-model="monitor.oauth_scopes"
|
||||
type="text"
|
||||
class="form-control"
|
||||
:placeholder="$t('Optional: Space separated list of scopes')"
|
||||
/>
|
||||
</div>
|
||||
<div class="my-3">
|
||||
<label for="oauth_audience" class="form-label">
|
||||
{{ $t("OAuth Audience") }}
|
||||
</label>
|
||||
<input
|
||||
id="oauth_audience"
|
||||
v-model="monitor.oauth_audience"
|
||||
type="text"
|
||||
class="form-control"
|
||||
:placeholder="$t('Optional: The audience to request the JWT for')"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<!-- Response -->
|
||||
<h2 class="mt-5 mb-2">{{ $t("Response") }}</h2>
|
||||
<div class="my-3">
|
||||
<label for="checkfor" class="form-label">{{ $t("Check for") }}</label>
|
||||
<select id="checkfor" v-model="monitor.responsecheck" class="form-select">
|
||||
<option :value="null">
|
||||
{{ $t("None") }}
|
||||
</option>
|
||||
<option value="keyword">
|
||||
{{ $t("Keyword") }}
|
||||
</option>
|
||||
<option value="json-query">
|
||||
{{ $t("Json Query Expression") }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Keyword -->
|
||||
<template v-if="monitor.responsecheck === 'keyword'">
|
||||
<div class="my-3">
|
||||
<label for="keyword" class="form-label">{{ $t("Keyword") }}</label>
|
||||
<input
|
||||
id="keyword"
|
||||
v-model="monitor.keyword"
|
||||
type="text"
|
||||
class="form-control"
|
||||
/>
|
||||
<div class="form-text">
|
||||
{{ $t("keywordDescription") }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Invert keyword -->
|
||||
<div class="my-3 form-check">
|
||||
<input
|
||||
id="invert-keyword"
|
||||
v-model="monitor.invertKeyword"
|
||||
class="form-check-input"
|
||||
type="checkbox"
|
||||
/>
|
||||
<label class="form-check-label" for="invert-keyword">
|
||||
{{ $t("Invert Keyword") }}
|
||||
</label>
|
||||
<div class="form-text">
|
||||
{{ $t("invertKeywordDescription") }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Json Query -->
|
||||
<template v-if="monitor.responsecheck === 'json-query'">
|
||||
<div class="my-3">
|
||||
<div class="my-2">
|
||||
<label for="jsonPath" class="form-label mb-0">
|
||||
{{ $t("Json Query Expression") }}
|
||||
</label>
|
||||
<i18n-t tag="div" class="form-text mb-2" keypath="jsonQueryDescription">
|
||||
<a href="https://jsonata.org/">jsonata.org</a>
|
||||
<a href="https://try.jsonata.org/">{{ $t("playground") }}</a>
|
||||
</i18n-t>
|
||||
<input
|
||||
id="jsonPath"
|
||||
v-model="monitor.jsonPath"
|
||||
type="text"
|
||||
class="form-control"
|
||||
placeholder="$"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="d-flex align-items-start">
|
||||
<div class="me-2">
|
||||
<label for="json_path_operator" class="form-label">
|
||||
{{ $t("Condition") }}
|
||||
</label>
|
||||
<select
|
||||
id="json_path_operator"
|
||||
v-model="monitor.jsonPathOperator"
|
||||
class="form-select me-3"
|
||||
required
|
||||
>
|
||||
<option value=">">></option>
|
||||
<option value=">=">>=</option>
|
||||
<option value="<"><</option>
|
||||
<option value="<="><=</option>
|
||||
<option value="!=">!=</option>
|
||||
<option value="==">==</option>
|
||||
<option value="contains">contains</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex-grow-1">
|
||||
<label for="expectedValue" class="form-label">
|
||||
{{ $t("Expected Value") }}
|
||||
</label>
|
||||
<input
|
||||
v-if="
|
||||
monitor.jsonPathOperator !== 'contains' &&
|
||||
monitor.jsonPathOperator !== '==' &&
|
||||
monitor.jsonPathOperator !== '!='
|
||||
"
|
||||
id="expectedValue"
|
||||
v-model="monitor.expectedValue"
|
||||
type="number"
|
||||
class="form-control"
|
||||
required
|
||||
step=".01"
|
||||
/>
|
||||
<input
|
||||
v-else
|
||||
id="expectedValue"
|
||||
v-model="monitor.expectedValue"
|
||||
type="text"
|
||||
class="form-control"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<!-- gRPC Options -->
|
||||
<template v-if="monitor.type === 'grpc-keyword'">
|
||||
<!-- Proto service enable TLS -->
|
||||
@ -2298,6 +2748,8 @@ const monitorDefaults = {
|
||||
url: "https://",
|
||||
wsSubprotocol: "",
|
||||
method: "GET",
|
||||
protocol: null,
|
||||
location: "world",
|
||||
ipFamily: null,
|
||||
interval: 60,
|
||||
humanReadableInterval: timeDurationFormatter.secondsToHumanReadableFormat(60),
|
||||
@ -2316,7 +2768,7 @@ const monitorDefaults = {
|
||||
saveErrorResponse: true,
|
||||
responseMaxLength: 1024,
|
||||
dns_resolve_type: "A",
|
||||
dns_resolve_server: "1.1.1.1",
|
||||
dns_resolve_server: "",
|
||||
docker_container: "",
|
||||
docker_host: null,
|
||||
proxyId: null,
|
||||
@ -2754,6 +3206,14 @@ message HealthCheckResponse {
|
||||
},
|
||||
|
||||
"monitor.type"(newType, oldType) {
|
||||
if (newType === "globalping" && !this.monitor.subtype) {
|
||||
this.monitor.subtype = "ping";
|
||||
}
|
||||
|
||||
if (newType === "dns" && !this.monitor.dns_resolve_server) {
|
||||
this.monitor.dns_resolve_server = "1.1.1.1";
|
||||
}
|
||||
|
||||
if (oldType && this.monitor.type === "websocket-upgrade") {
|
||||
this.monitor.url = "wss://";
|
||||
this.monitor.accepted_statuscodes = ["1000"];
|
||||
@ -2774,6 +3234,8 @@ message HealthCheckResponse {
|
||||
this.monitor.port = "1812";
|
||||
} else if (this.monitor.type === "snmp") {
|
||||
this.monitor.port = "161";
|
||||
} else if (this.monitor.type === "globalping" && this.monitor.subtype === "ping") {
|
||||
this.monitor.port = "80";
|
||||
} else {
|
||||
this.monitor.port = undefined;
|
||||
}
|
||||
@ -2840,6 +3302,43 @@ message HealthCheckResponse {
|
||||
}
|
||||
},
|
||||
|
||||
"monitor.subtype"(newSubtype, oldSubtype) {
|
||||
if (!oldSubtype && !this.monitor.protocol) {
|
||||
if (newSubtype === "ping") {
|
||||
this.monitor.protocol = "ICMP";
|
||||
} else if (newSubtype === "http") {
|
||||
this.monitor.protocol = null;
|
||||
}
|
||||
}
|
||||
if (newSubtype !== oldSubtype) {
|
||||
if (newSubtype === "ping") {
|
||||
this.monitor.protocol = "ICMP";
|
||||
this.monitor.port = "80";
|
||||
} else if (newSubtype === "http") {
|
||||
this.monitor.protocol = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (newSubtype === "http") {
|
||||
if (this.monitor.keyword) {
|
||||
this.monitor.responsecheck = "keyword";
|
||||
} else if (this.monitor.expectedValue) {
|
||||
this.monitor.responsecheck = "json-query";
|
||||
} else {
|
||||
this.monitor.responsecheck = null;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"monitor.responsecheck"(newSubtype) {
|
||||
if (newSubtype !== "keyword") {
|
||||
this.monitor.keyword = null;
|
||||
}
|
||||
if (newSubtype !== "json-query") {
|
||||
this.monitor.expectedValue = null;
|
||||
}
|
||||
},
|
||||
|
||||
currentGameObject(newGameObject, previousGameObject) {
|
||||
if (!this.monitor.port || (previousGameObject && previousGameObject.options.port === this.monitor.port)) {
|
||||
this.monitor.port = newGameObject.options.port;
|
||||
@ -2945,6 +3444,16 @@ message HealthCheckResponse {
|
||||
this.monitor.tags = undefined;
|
||||
}
|
||||
|
||||
if (this.monitor.type === "globalping" && this.monitor.subtype === "http") {
|
||||
if (this.monitor.keyword) {
|
||||
this.monitor.responsecheck = "keyword";
|
||||
} else if (this.monitor.expectedValue) {
|
||||
this.monitor.responsecheck = "json-query";
|
||||
} else {
|
||||
this.monitor.responsecheck = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Handling for monitors that are created before 1.7.0
|
||||
if (this.monitor.retryInterval === 0) {
|
||||
this.monitor.retryInterval = this.monitor.interval;
|
||||
|
||||
868
test/backend-test/test-globalping.js
Normal file
868
test/backend-test/test-globalping.js
Normal file
@ -0,0 +1,868 @@
|
||||
const { describe, test, mock } = require("node:test");
|
||||
const assert = require("node:assert");
|
||||
const { encodeBase64 } = require("../../server/util-server");
|
||||
const { UP, DOWN, PENDING } = require("../../src/util");
|
||||
|
||||
describe("GlobalpingMonitorType", () => {
|
||||
const { GlobalpingMonitorType } = require("../../server/monitor-types/globalping");
|
||||
|
||||
describe("ping", () => {
|
||||
test("should handle successful ping", async () => {
|
||||
const monitorType = new GlobalpingMonitorType("test-agent/1.0");
|
||||
const mockClient = createGlobalpingClientMock();
|
||||
const createResponse = createMockResponse({
|
||||
id: "2g8T7V3OwXG3JV6Y10011zF2v",
|
||||
});
|
||||
const measurement = createPingMeasurement();
|
||||
const awaitResponse = createMockResponse(measurement);
|
||||
|
||||
mockClient.createMeasurement.mock.mockImplementation(() => createResponse);
|
||||
mockClient.awaitMeasurement.mock.mockImplementation(() => awaitResponse);
|
||||
|
||||
const monitor = {
|
||||
hostname: "example.com",
|
||||
location: "North America",
|
||||
ping_count: 3,
|
||||
protocol: "ICMP",
|
||||
ipFamily: "ipv4",
|
||||
};
|
||||
|
||||
const heartbeat = {
|
||||
status: PENDING,
|
||||
msg: "",
|
||||
ping: 0,
|
||||
};
|
||||
|
||||
await monitorType.ping(mockClient, monitor, heartbeat, true);
|
||||
|
||||
assert.strictEqual(mockClient.createMeasurement.mock.calls.length, 1);
|
||||
assert.deepStrictEqual(mockClient.createMeasurement.mock.calls[0].arguments[0], {
|
||||
type: "ping",
|
||||
target: "example.com",
|
||||
inProgressUpdates: false,
|
||||
limit: 1,
|
||||
locations: [{ magic: "North America" }],
|
||||
measurementOptions: {
|
||||
packets: 3,
|
||||
protocol: "ICMP",
|
||||
ipVersion: 4,
|
||||
},
|
||||
});
|
||||
|
||||
assert.deepStrictEqual(heartbeat, {
|
||||
status: UP,
|
||||
msg: "Ashburn (VA), US, NA, Amazon.com (AS14618), (aws-us-east-1) : OK",
|
||||
ping: 2.169,
|
||||
});
|
||||
});
|
||||
|
||||
test("should handle failed ping with status failed", async () => {
|
||||
const monitorType = new GlobalpingMonitorType("test-agent/1.0");
|
||||
const mockClient = createGlobalpingClientMock();
|
||||
const createResponse = createMockResponse({
|
||||
id: "2g8T7V3OwXG3JV6Y10011zF2v",
|
||||
});
|
||||
const measurement = createPingMeasurement();
|
||||
measurement.results[0].result.status = "failed";
|
||||
measurement.results[0].result.rawOutput = "Host unreachable";
|
||||
const awaitResponse = createMockResponse(measurement);
|
||||
|
||||
mockClient.createMeasurement.mock.mockImplementation(() => createResponse);
|
||||
mockClient.awaitMeasurement.mock.mockImplementation(() => awaitResponse);
|
||||
|
||||
const monitor = {
|
||||
hostname: "unreachable.example.com",
|
||||
location: "Europe",
|
||||
ping_count: 3,
|
||||
protocol: "ICMP",
|
||||
};
|
||||
|
||||
const heartbeat = {
|
||||
status: PENDING,
|
||||
msg: "",
|
||||
};
|
||||
|
||||
await monitorType.ping(mockClient, monitor, heartbeat, true);
|
||||
|
||||
assert.deepStrictEqual(heartbeat, {
|
||||
status: DOWN,
|
||||
msg: "Ashburn (VA), US, NA, Amazon.com (AS14618), (aws-us-east-1) : Failed: Host unreachable",
|
||||
});
|
||||
});
|
||||
|
||||
test("should handle API error on create measurement", async () => {
|
||||
const monitorType = new GlobalpingMonitorType("test-agent/1.0");
|
||||
const mockClient = createGlobalpingClientMock();
|
||||
const createResponse = createMockResponse({
|
||||
error: {
|
||||
type: "validation_error",
|
||||
message: "Invalid target",
|
||||
params: { target: "example.com" },
|
||||
},
|
||||
});
|
||||
createResponse.ok = false;
|
||||
createResponse.response.status = 400;
|
||||
|
||||
mockClient.createMeasurement.mock.mockImplementation(() => createResponse);
|
||||
|
||||
const monitor = {
|
||||
hostname: "example.com",
|
||||
location: "North America",
|
||||
ping_count: 3,
|
||||
protocol: "ICMP",
|
||||
};
|
||||
|
||||
const heartbeat = {
|
||||
status: PENDING,
|
||||
msg: "",
|
||||
};
|
||||
|
||||
await assert.rejects(monitorType.ping(mockClient, monitor, heartbeat, true), (error) => {
|
||||
assert.deepStrictEqual(
|
||||
error,
|
||||
new Error("Failed to create measurement: validation_error Invalid target.\ntarget: example.com")
|
||||
);
|
||||
return true;
|
||||
});
|
||||
});
|
||||
|
||||
test("should handle API error on await measurement", async () => {
|
||||
const monitorType = new GlobalpingMonitorType("test-agent/1.0");
|
||||
const mockClient = createGlobalpingClientMock();
|
||||
const createResponse = createMockResponse({
|
||||
id: "2g8T7V3OwXG3JV6Y10011zF2v",
|
||||
});
|
||||
const awaitResponse = createMockResponse({
|
||||
error: {
|
||||
type: "internal_error",
|
||||
message: "Server error",
|
||||
},
|
||||
});
|
||||
awaitResponse.ok = false;
|
||||
awaitResponse.response.status = 400;
|
||||
|
||||
mockClient.createMeasurement.mock.mockImplementation(() => createResponse);
|
||||
mockClient.awaitMeasurement.mock.mockImplementation(() => awaitResponse);
|
||||
|
||||
const monitor = {
|
||||
hostname: "example.com",
|
||||
location: "North America",
|
||||
ping_count: 3,
|
||||
protocol: "ICMP",
|
||||
};
|
||||
|
||||
const heartbeat = {
|
||||
status: PENDING,
|
||||
msg: "",
|
||||
};
|
||||
|
||||
await assert.rejects(monitorType.ping(mockClient, monitor, heartbeat, true), (error) => {
|
||||
assert.deepStrictEqual(
|
||||
error,
|
||||
new Error("Failed to fetch measurement (2g8T7V3OwXG3JV6Y10011zF2v): internal_error Server error.")
|
||||
);
|
||||
return true;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("http", () => {
|
||||
test("should handle successful HTTP request", async () => {
|
||||
const monitorType = new GlobalpingMonitorType("test-agent/1.0");
|
||||
const mockClient = createGlobalpingClientMock();
|
||||
const createResponse = createMockResponse({
|
||||
id: "2g8T7V3OwXG3JV6Y10011zF2v",
|
||||
});
|
||||
const measurement = createHttpMeasurement();
|
||||
const awaitResponse = createMockResponse(measurement);
|
||||
|
||||
mockClient.createMeasurement.mock.mockImplementation(() => createResponse);
|
||||
mockClient.awaitMeasurement.mock.mockImplementation(() => awaitResponse);
|
||||
|
||||
const monitor = {
|
||||
url: "https://example.com:444/api/test?test=1",
|
||||
location: "North America",
|
||||
method: "GET",
|
||||
accepted_statuscodes_json: JSON.stringify(["200-299", "300-399"]),
|
||||
headers: '{"Test-Header": "Test-Value"}',
|
||||
ipFamily: "ipv4",
|
||||
dns_resolve_server: "8.8.8.8",
|
||||
auth_method: "basic",
|
||||
basic_auth_user: "username",
|
||||
basic_auth_pass: "password",
|
||||
};
|
||||
|
||||
const heartbeat = {
|
||||
status: PENDING,
|
||||
msg: "",
|
||||
ping: 0,
|
||||
};
|
||||
|
||||
await monitorType.http(mockClient, monitor, heartbeat, true);
|
||||
|
||||
assert.strictEqual(mockClient.createMeasurement.mock.calls.length, 1);
|
||||
const expectedToken = encodeBase64(monitor.basic_auth_user, monitor.basic_auth_pass);
|
||||
assert.deepStrictEqual(mockClient.createMeasurement.mock.calls[0].arguments[0], {
|
||||
type: "http",
|
||||
target: "example.com",
|
||||
inProgressUpdates: false,
|
||||
limit: 1,
|
||||
locations: [{ magic: "North America" }],
|
||||
measurementOptions: {
|
||||
request: {
|
||||
host: "example.com",
|
||||
path: "/api/test",
|
||||
query: "test=1",
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Test-Header": "Test-Value",
|
||||
Authorization: `Basic ${expectedToken}`,
|
||||
},
|
||||
},
|
||||
port: 444,
|
||||
protocol: "HTTPS",
|
||||
ipVersion: 4,
|
||||
resolver: "8.8.8.8",
|
||||
},
|
||||
});
|
||||
|
||||
assert.deepStrictEqual(heartbeat, {
|
||||
status: UP,
|
||||
msg: "New York (NY), US, NA, MASSIVEGRID (AS49683) : OK",
|
||||
ping: 1440,
|
||||
});
|
||||
});
|
||||
|
||||
test("should handle failed HTTP request", async () => {
|
||||
const monitorType = new GlobalpingMonitorType("test-agent/1.0");
|
||||
const mockClient = createGlobalpingClientMock();
|
||||
const createResponse = createMockResponse({
|
||||
id: "2g8T7V3OwXG3JV6Y10011zF2v",
|
||||
});
|
||||
const measurement = createHttpMeasurement();
|
||||
measurement.results[0].result.status = "failed";
|
||||
measurement.results[0].result.rawOutput = "Host unreachable";
|
||||
const awaitResponse = createMockResponse(measurement);
|
||||
|
||||
mockClient.createMeasurement.mock.mockImplementation(() => createResponse);
|
||||
mockClient.awaitMeasurement.mock.mockImplementation(() => awaitResponse);
|
||||
|
||||
const monitor = {
|
||||
url: "https://example.com",
|
||||
location: "North America",
|
||||
method: "GET",
|
||||
accepted_statuscodes_json: JSON.stringify(["200-299", "300-399"]),
|
||||
headers: null,
|
||||
};
|
||||
|
||||
const heartbeat = {
|
||||
status: PENDING,
|
||||
msg: "",
|
||||
};
|
||||
|
||||
await monitorType.http(mockClient, monitor, heartbeat, true);
|
||||
|
||||
assert.deepStrictEqual(heartbeat, {
|
||||
status: DOWN,
|
||||
msg: "New York (NY), US, NA, MASSIVEGRID (AS49683) : Failed: Host unreachable",
|
||||
});
|
||||
});
|
||||
|
||||
test("should handle API error on create measurement", async () => {
|
||||
const monitorType = new GlobalpingMonitorType("test-agent/1.0");
|
||||
const mockClient = createGlobalpingClientMock();
|
||||
const createResponse = createMockResponse({
|
||||
error: {
|
||||
type: "validation_error",
|
||||
message: "Invalid target",
|
||||
params: { target: "example.com" },
|
||||
},
|
||||
});
|
||||
createResponse.ok = false;
|
||||
createResponse.response.status = 400;
|
||||
|
||||
mockClient.createMeasurement.mock.mockImplementation(() => createResponse);
|
||||
|
||||
const monitor = {
|
||||
url: "https://example.com",
|
||||
location: "North America",
|
||||
method: "GET",
|
||||
accepted_statuscodes_json: JSON.stringify(["200-299", "300-399"]),
|
||||
headers: null,
|
||||
};
|
||||
|
||||
const heartbeat = {
|
||||
status: PENDING,
|
||||
msg: "",
|
||||
};
|
||||
|
||||
await assert.rejects(monitorType.http(mockClient, monitor, heartbeat, true), (error) => {
|
||||
assert.deepStrictEqual(
|
||||
error,
|
||||
new Error("Failed to create measurement: validation_error Invalid target.\ntarget: example.com")
|
||||
);
|
||||
return true;
|
||||
});
|
||||
});
|
||||
|
||||
test("should handle API error on await measurement", async () => {
|
||||
const monitorType = new GlobalpingMonitorType("test-agent/1.0");
|
||||
const mockClient = createGlobalpingClientMock();
|
||||
const createResponse = createMockResponse({
|
||||
id: "2g8T7V3OwXG3JV6Y10011zF2v",
|
||||
});
|
||||
const awaitResponse = createMockResponse({
|
||||
error: {
|
||||
type: "internal_error",
|
||||
message: "Server error",
|
||||
},
|
||||
});
|
||||
awaitResponse.ok = false;
|
||||
awaitResponse.response.status = 400;
|
||||
|
||||
mockClient.createMeasurement.mock.mockImplementation(() => createResponse);
|
||||
mockClient.awaitMeasurement.mock.mockImplementation(() => awaitResponse);
|
||||
|
||||
const monitor = {
|
||||
url: "https://example.com",
|
||||
location: "North America",
|
||||
method: "GET",
|
||||
accepted_statuscodes_json: JSON.stringify(["200-299", "300-399"]),
|
||||
headers: null,
|
||||
};
|
||||
|
||||
const heartbeat = {
|
||||
status: PENDING,
|
||||
msg: "",
|
||||
};
|
||||
|
||||
await assert.rejects(monitorType.http(mockClient, monitor, heartbeat, true), (error) => {
|
||||
assert.deepStrictEqual(
|
||||
error,
|
||||
new Error("Failed to fetch measurement (2g8T7V3OwXG3JV6Y10011zF2v): internal_error Server error.")
|
||||
);
|
||||
return true;
|
||||
});
|
||||
});
|
||||
|
||||
test("should handle invalid status code", async () => {
|
||||
const monitorType = new GlobalpingMonitorType("test-agent/1.0");
|
||||
const mockClient = createGlobalpingClientMock();
|
||||
const createResponse = createMockResponse({
|
||||
id: "2g8T7V3OwXG3JV6Y10011zF2v",
|
||||
});
|
||||
const measurement = createHttpMeasurement();
|
||||
measurement.results[0].result.rawOutput = "RAW OUTPUT";
|
||||
const awaitResponse = createMockResponse(measurement);
|
||||
|
||||
mockClient.createMeasurement.mock.mockImplementation(() => createResponse);
|
||||
mockClient.awaitMeasurement.mock.mockImplementation(() => awaitResponse);
|
||||
|
||||
const monitor = {
|
||||
url: "https://example.com/api/test",
|
||||
location: "North America",
|
||||
method: "GET",
|
||||
accepted_statuscodes_json: JSON.stringify(["200-299"]),
|
||||
headers: null,
|
||||
};
|
||||
|
||||
const heartbeat = {
|
||||
status: PENDING,
|
||||
msg: "",
|
||||
ping: 0,
|
||||
};
|
||||
|
||||
await monitorType.http(mockClient, monitor, heartbeat, true);
|
||||
|
||||
assert.deepStrictEqual(heartbeat, {
|
||||
status: DOWN,
|
||||
msg: "New York (NY), US, NA, MASSIVEGRID (AS49683) : Status code 301 not accepted. Output: RAW OUTPUT",
|
||||
ping: 1440,
|
||||
});
|
||||
});
|
||||
|
||||
test("should handle keyword check (keyword present)", async () => {
|
||||
const monitorType = new GlobalpingMonitorType("test-agent/1.0");
|
||||
const mockClient = createGlobalpingClientMock();
|
||||
const createResponse = createMockResponse({
|
||||
id: "2g8T7V3OwXG3JV6Y10011zF2v",
|
||||
});
|
||||
const measurement = createHttpMeasurement();
|
||||
measurement.results[0].result.rawOutput = "Response body with KEYWORD word";
|
||||
const awaitResponse = createMockResponse(measurement);
|
||||
|
||||
mockClient.createMeasurement.mock.mockImplementation(() => createResponse);
|
||||
mockClient.awaitMeasurement.mock.mockImplementation(() => awaitResponse);
|
||||
|
||||
const monitor = {
|
||||
url: "https://example.com",
|
||||
location: "North America",
|
||||
protocol: "HTTPS",
|
||||
accepted_statuscodes_json: JSON.stringify(["300-399"]),
|
||||
keyword: "KEYWORD",
|
||||
invertKeyword: false,
|
||||
};
|
||||
|
||||
const heartbeat = {
|
||||
status: PENDING,
|
||||
msg: "",
|
||||
};
|
||||
|
||||
await monitorType.http(mockClient, monitor, heartbeat, true);
|
||||
|
||||
assert.deepStrictEqual(heartbeat, {
|
||||
status: UP,
|
||||
msg: "New York (NY), US, NA, MASSIVEGRID (AS49683) : 301 - Moved Permanently, keyword is found",
|
||||
ping: 1440,
|
||||
});
|
||||
});
|
||||
|
||||
test("should handle keyword check (keyword not present)", async () => {
|
||||
const monitorType = new GlobalpingMonitorType("test-agent/1.0");
|
||||
const mockClient = createGlobalpingClientMock();
|
||||
const createResponse = createMockResponse({
|
||||
id: "2g8T7V3OwXG3JV6Y10011zF2v",
|
||||
});
|
||||
const measurement = createHttpMeasurement();
|
||||
measurement.results[0].result.rawOutput = "Response body with KEYWORD word";
|
||||
const awaitResponse = createMockResponse(measurement);
|
||||
|
||||
mockClient.createMeasurement.mock.mockImplementation(() => createResponse);
|
||||
mockClient.awaitMeasurement.mock.mockImplementation(() => awaitResponse);
|
||||
|
||||
const monitor = {
|
||||
url: "https://example.com",
|
||||
location: "North America",
|
||||
protocol: "HTTPS",
|
||||
accepted_statuscodes_json: JSON.stringify(["300-399"]),
|
||||
keyword: "MISSING_KEYWORD",
|
||||
invertKeyword: false,
|
||||
};
|
||||
|
||||
const heartbeat = {
|
||||
status: PENDING,
|
||||
msg: "",
|
||||
};
|
||||
|
||||
await assert.rejects(monitorType.http(mockClient, monitor, heartbeat, true), (error) => {
|
||||
assert.deepStrictEqual(
|
||||
error,
|
||||
new Error(
|
||||
"New York (NY), US, NA, MASSIVEGRID (AS49683) : 301 - Moved Permanently, but keyword is not in [Response body with KEYWORD word]"
|
||||
)
|
||||
);
|
||||
return true;
|
||||
});
|
||||
});
|
||||
|
||||
test("should handle inverted keyword check", async () => {
|
||||
const monitorType = new GlobalpingMonitorType("test-agent/1.0");
|
||||
const mockClient = createGlobalpingClientMock();
|
||||
const createResponse = createMockResponse({
|
||||
id: "2g8T7V3OwXG3JV6Y10011zF2v",
|
||||
});
|
||||
const measurement = createHttpMeasurement();
|
||||
measurement.results[0].result.rawOutput = "Response body with KEYWORD word";
|
||||
const awaitResponse = createMockResponse(measurement);
|
||||
|
||||
mockClient.createMeasurement.mock.mockImplementation(() => createResponse);
|
||||
mockClient.awaitMeasurement.mock.mockImplementation(() => awaitResponse);
|
||||
|
||||
const monitor = {
|
||||
url: "https://example.com",
|
||||
location: "North America",
|
||||
protocol: "HTTPS",
|
||||
accepted_statuscodes_json: JSON.stringify(["300-399"]),
|
||||
keyword: "ERROR",
|
||||
invertKeyword: true,
|
||||
};
|
||||
|
||||
const heartbeat = {
|
||||
status: PENDING,
|
||||
msg: "",
|
||||
};
|
||||
|
||||
await monitorType.http(mockClient, monitor, heartbeat, true);
|
||||
|
||||
assert.deepStrictEqual(heartbeat, {
|
||||
status: UP,
|
||||
msg: "New York (NY), US, NA, MASSIVEGRID (AS49683) : 301 - Moved Permanently, keyword not found",
|
||||
ping: 1440,
|
||||
});
|
||||
});
|
||||
|
||||
test("should handle JSON query check (valid)", async () => {
|
||||
const monitorType = new GlobalpingMonitorType("test-agent/1.0");
|
||||
const mockClient = createGlobalpingClientMock();
|
||||
const createResponse = createMockResponse({
|
||||
id: "2g8T7V3OwXG3JV6Y10011zF2v",
|
||||
});
|
||||
const measurement = createHttpMeasurement();
|
||||
measurement.results[0].result.rawOutput = JSON.stringify({
|
||||
status: "success",
|
||||
value: 42,
|
||||
});
|
||||
const awaitResponse = createMockResponse(measurement);
|
||||
|
||||
mockClient.createMeasurement.mock.mockImplementation(() => createResponse);
|
||||
mockClient.awaitMeasurement.mock.mockImplementation(() => awaitResponse);
|
||||
|
||||
const monitor = {
|
||||
url: "https://api.example.com/status",
|
||||
location: "North America",
|
||||
protocol: "HTTPS",
|
||||
accepted_statuscodes_json: JSON.stringify(["300-399"]),
|
||||
jsonPath: "$.status",
|
||||
jsonPathOperator: "==",
|
||||
expectedValue: "success",
|
||||
};
|
||||
|
||||
const heartbeat = {
|
||||
status: PENDING,
|
||||
msg: "",
|
||||
};
|
||||
|
||||
await monitorType.http(mockClient, monitor, heartbeat, true);
|
||||
|
||||
assert.deepStrictEqual(heartbeat, {
|
||||
status: UP,
|
||||
msg: "New York (NY), US, NA, MASSIVEGRID (AS49683) : JSON query passes (comparing success == success)",
|
||||
ping: 1440,
|
||||
});
|
||||
});
|
||||
|
||||
test("should handle JSON query check (invalid)", async () => {
|
||||
const monitorType = new GlobalpingMonitorType("test-agent/1.0");
|
||||
const mockClient = createGlobalpingClientMock();
|
||||
const createResponse = createMockResponse({
|
||||
id: "2g8T7V3OwXG3JV6Y10011zF2v",
|
||||
});
|
||||
const measurement = createHttpMeasurement();
|
||||
measurement.results[0].result.rawOutput = JSON.stringify({
|
||||
status: "failed",
|
||||
value: 42,
|
||||
});
|
||||
const awaitResponse = createMockResponse(measurement);
|
||||
|
||||
mockClient.createMeasurement.mock.mockImplementation(() => createResponse);
|
||||
mockClient.awaitMeasurement.mock.mockImplementation(() => awaitResponse);
|
||||
|
||||
const monitor = {
|
||||
url: "https://api.example.com/status",
|
||||
location: "North America",
|
||||
protocol: "HTTPS",
|
||||
accepted_statuscodes_json: JSON.stringify(["300-399"]),
|
||||
jsonPath: "$.status",
|
||||
jsonPathOperator: "==",
|
||||
expectedValue: "success",
|
||||
};
|
||||
|
||||
const heartbeat = {
|
||||
status: PENDING,
|
||||
msg: "",
|
||||
};
|
||||
|
||||
await assert.rejects(monitorType.http(mockClient, monitor, heartbeat, true), (error) => {
|
||||
assert.deepStrictEqual(
|
||||
error,
|
||||
new Error(
|
||||
"New York (NY), US, NA, MASSIVEGRID (AS49683) : JSON query does not pass (comparing failed == success)"
|
||||
)
|
||||
);
|
||||
return true;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("helper methods", () => {
|
||||
test("formatProbeLocation should format location correctly", () => {
|
||||
const monitorType = new GlobalpingMonitorType("test-agent/1.0");
|
||||
|
||||
const probe = {
|
||||
city: "New York",
|
||||
state: "NY",
|
||||
country: "US",
|
||||
continent: "NA",
|
||||
network: "Amazon.com",
|
||||
asn: 14618,
|
||||
tags: ["aws-us-east-1", "datacenter"],
|
||||
};
|
||||
|
||||
const result = monitorType.formatProbeLocation(probe);
|
||||
|
||||
assert.strictEqual(result, "New York (NY), US, NA, Amazon.com (AS14618), (aws-us-east-1)");
|
||||
});
|
||||
|
||||
test("formatProbeLocation should handle missing state", () => {
|
||||
const monitorType = new GlobalpingMonitorType("test-agent/1.0");
|
||||
|
||||
const probe = {
|
||||
city: "London",
|
||||
state: null,
|
||||
country: "GB",
|
||||
continent: "EU",
|
||||
network: "Example Network",
|
||||
asn: 12345,
|
||||
tags: [],
|
||||
};
|
||||
|
||||
const result = monitorType.formatProbeLocation(probe);
|
||||
|
||||
assert.strictEqual(result, "London, GB, EU, Example Network (AS12345)");
|
||||
});
|
||||
|
||||
test("formatResponse should combine location and text", () => {
|
||||
const monitorType = new GlobalpingMonitorType("test-agent/1.0");
|
||||
|
||||
const probe = {
|
||||
city: "Tokyo",
|
||||
state: null,
|
||||
country: "JP",
|
||||
continent: "AS",
|
||||
network: "Example ISP",
|
||||
asn: 54321,
|
||||
tags: [],
|
||||
};
|
||||
|
||||
const result = monitorType.formatResponse(probe, "Test message");
|
||||
|
||||
assert.strictEqual(result, "Tokyo, JP, AS, Example ISP (AS54321) : Test message");
|
||||
});
|
||||
|
||||
test("formatApiError should format error with params", () => {
|
||||
const monitorType = new GlobalpingMonitorType("test-agent/1.0");
|
||||
|
||||
const error = {
|
||||
type: "validation_error",
|
||||
message: "Invalid request",
|
||||
params: {
|
||||
field: "target",
|
||||
value: "invalid",
|
||||
},
|
||||
};
|
||||
|
||||
const result = monitorType.formatApiError(error);
|
||||
|
||||
assert.strictEqual(result, "validation_error Invalid request.\nfield: target\nvalue: invalid");
|
||||
});
|
||||
|
||||
test("formatApiError should format error without params", () => {
|
||||
const monitorType = new GlobalpingMonitorType("test-agent/1.0");
|
||||
|
||||
const error = {
|
||||
type: "internal_error",
|
||||
message: "Server error",
|
||||
};
|
||||
|
||||
const result = monitorType.formatApiError(error);
|
||||
|
||||
assert.strictEqual(result, "internal_error Server error.");
|
||||
});
|
||||
|
||||
test("formatTooManyRequestsError with API token", () => {
|
||||
const monitorType = new GlobalpingMonitorType("test-agent/1.0");
|
||||
|
||||
const result = monitorType.formatTooManyRequestsError(true);
|
||||
|
||||
assert.strictEqual(
|
||||
result,
|
||||
"You have run out of credits. Get higher limits by sponsoring us or hosting probes. Learn more at https://dash.globalping.io?view=add-credits."
|
||||
);
|
||||
});
|
||||
|
||||
test("formatTooManyRequestsError without API token", () => {
|
||||
const monitorType = new GlobalpingMonitorType("test-agent/1.0");
|
||||
|
||||
const result = monitorType.formatTooManyRequestsError(false);
|
||||
|
||||
assert.strictEqual(
|
||||
result,
|
||||
"You have run out of credits. Get higher limits by creating an account. Sign up at https://dash.globalping.io?view=add-credits."
|
||||
);
|
||||
});
|
||||
|
||||
test("getBasicAuthHeader should return empty for non-basic auth", () => {
|
||||
const monitorType = new GlobalpingMonitorType("test-agent/1.0");
|
||||
|
||||
const monitor = {
|
||||
auth_method: "none",
|
||||
};
|
||||
|
||||
const result = monitorType.getBasicAuthHeader(monitor);
|
||||
|
||||
assert.deepStrictEqual(result, {});
|
||||
});
|
||||
|
||||
test("getBasicAuthHeader should return Authorization header", () => {
|
||||
const monitorType = new GlobalpingMonitorType("test-agent/1.0");
|
||||
|
||||
const monitor = {
|
||||
auth_method: "basic",
|
||||
basic_auth_user: "testuser",
|
||||
basic_auth_pass: "testpass",
|
||||
};
|
||||
|
||||
const result = monitorType.getBasicAuthHeader(monitor);
|
||||
|
||||
const expectedToken = encodeBase64(monitor.basic_auth_user, monitor.basic_auth_pass);
|
||||
|
||||
assert.strictEqual(result.Authorization, `Basic ${expectedToken}`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Reusable mock factory for Globalping client
|
||||
* @returns {object} Mocked Globalping client
|
||||
*/
|
||||
function createGlobalpingClientMock() {
|
||||
return {
|
||||
createMeasurement: mock.fn(),
|
||||
awaitMeasurement: mock.fn(),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Reusable mock factory for Globalping response
|
||||
* @param {object} data Response data
|
||||
* @returns {object} Mocked Globalping response
|
||||
*/
|
||||
function createMockResponse(data) {
|
||||
return {
|
||||
ok: true,
|
||||
response: {
|
||||
status: 200,
|
||||
},
|
||||
data,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a successful ping measurement response
|
||||
* @returns {object} Mock measurement response
|
||||
*/
|
||||
function createPingMeasurement() {
|
||||
return {
|
||||
id: "2g8T7V3OwXG3JV6Y10011zF2v",
|
||||
type: "ping",
|
||||
status: "finished",
|
||||
createdAt: "2025-11-05T08:25:33.173Z",
|
||||
updatedAt: "2025-11-05T08:25:34.750Z",
|
||||
target: "google.com",
|
||||
probesCount: 1,
|
||||
locations: [{ magic: "us-east-1" }],
|
||||
results: [
|
||||
{
|
||||
probe: {
|
||||
continent: "NA",
|
||||
region: "Northern America",
|
||||
country: "US",
|
||||
state: "VA",
|
||||
city: "Ashburn",
|
||||
asn: 14618,
|
||||
longitude: -77.49,
|
||||
latitude: 39.04,
|
||||
network: "Amazon.com",
|
||||
tags: [
|
||||
"aws-us-east-1",
|
||||
"aws",
|
||||
"datacenter-network",
|
||||
"u-cloudlookingglass:aws-us-east-1-use1-az6",
|
||||
"u-cloudlookingglass:aws-us-east-1-use1-az6-net",
|
||||
],
|
||||
resolvers: ["private"],
|
||||
},
|
||||
result: {
|
||||
status: "finished",
|
||||
rawOutput:
|
||||
"PING (142.251.16.100) 56(84) bytes of data.\n64 bytes from bl-in-f100.1e100.net (142.251.16.100): icmp_seq=1 ttl=106 time=2.07 ms\n64 bytes from bl-in-f100.1e100.net (142.251.16.100): icmp_seq=2 ttl=106 time=2.08 ms\n64 bytes from bl-in-f100.1e100.net (142.251.16.100): icmp_seq=3 ttl=106 time=2.35 ms\n\n--- ping statistics ---\n3 packets transmitted, 3 received, 0% packet loss, time 1002ms\nrtt min/avg/max/mdev = 2.073/2.169/2.351/0.128 ms",
|
||||
resolvedAddress: "142.251.16.100",
|
||||
resolvedHostname: "bl-in-f100.1e100.net",
|
||||
timings: [
|
||||
{ ttl: 106, rtt: 2.07 },
|
||||
{ ttl: 106, rtt: 2.08 },
|
||||
{ ttl: 106, rtt: 2.35 },
|
||||
],
|
||||
stats: {
|
||||
min: 2.073,
|
||||
max: 2.351,
|
||||
avg: 2.169,
|
||||
total: 3,
|
||||
loss: 0,
|
||||
rcv: 3,
|
||||
drop: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a successful HTTP measurement response
|
||||
* @returns {object} Mock measurement response
|
||||
*/
|
||||
function createHttpMeasurement() {
|
||||
return {
|
||||
id: "2m6DeD067jeT6licX0011zF2x",
|
||||
type: "http",
|
||||
status: "finished",
|
||||
createdAt: "2025-11-05T08:27:29.034Z",
|
||||
updatedAt: "2025-11-05T08:27:30.718Z",
|
||||
target: "google.com",
|
||||
probesCount: 1,
|
||||
locations: [{ magic: "New York" }],
|
||||
results: [
|
||||
{
|
||||
probe: {
|
||||
continent: "NA",
|
||||
region: "Northern America",
|
||||
country: "US",
|
||||
state: "NY",
|
||||
city: "New York",
|
||||
asn: 49683,
|
||||
longitude: -74.01,
|
||||
latitude: 40.71,
|
||||
network: "MASSIVEGRID",
|
||||
tags: ["datacenter-network", "u-gbzret4d"],
|
||||
resolvers: ["private"],
|
||||
},
|
||||
result: {
|
||||
status: "finished",
|
||||
resolvedAddress: "209.85.201.101",
|
||||
headers: {
|
||||
location: "https://www.google.com/",
|
||||
"content-type": "text/html; charset=UTF-8",
|
||||
"content-security-policy-report-only":
|
||||
"object-src 'none';base-uri 'self';script-src 'nonce-Eft2LKpM01f69RvQoV6QJA' 'strict-dynamic' 'report-sample' 'unsafe-eval' 'unsafe-inline' https: http:;report-uri https://csp.withgoogle.com/csp/gws/other-hp",
|
||||
date: "Wed, 05 Nov 2025 08:27:30 GMT",
|
||||
expires: "Fri, 05 Dec 2025 08:27:30 GMT",
|
||||
"cache-control": "public, max-age=2592000",
|
||||
server: "gws",
|
||||
"content-length": "220",
|
||||
"x-xss-protection": "0",
|
||||
"x-frame-options": "SAMEORIGIN",
|
||||
"alt-svc": 'h3=":443"; ma=2592000,h3-29=":443"; ma=2592000',
|
||||
connection: "close",
|
||||
},
|
||||
rawHeaders:
|
||||
"Location: https://www.google.com/\nContent-Type: text/html; charset=UTF-8\nContent-Security-Policy-Report-Only: object-src 'none';base-uri 'self';script-src 'nonce-Eft2LKpM01f69RvQoV6QJA' 'strict-dynamic' 'report-sample' 'unsafe-eval' 'unsafe-inline' https: http:;report-uri https://csp.withgoogle.com/csp/gws/other-hp\nDate: Wed, 05 Nov 2025 08:27:30 GMT\nExpires: Fri, 05 Dec 2025 08:27:30 GMT\nCache-Control: public, max-age=2592000\nServer: gws\nContent-Length: 220\nX-XSS-Protection: 0\nX-Frame-Options: SAMEORIGIN\nAlt-Svc: h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000\nConnection: close",
|
||||
rawBody: null,
|
||||
rawOutput:
|
||||
"HTTP/1.1 301\nLocation: https://www.google.com/\nContent-Type: text/html; charset=UTF-8\nContent-Security-Policy-Report-Only: object-src 'none';base-uri 'self';script-src 'nonce-Eft2LKpM01f69RvQoV6QJA' 'strict-dynamic' 'report-sample' 'unsafe-eval' 'unsafe-inline' https: http:;report-uri https://csp.withgoogle.com/csp/gws/other-hp\nDate: Wed, 05 Nov 2025 08:27:30 GMT\nExpires: Fri, 05 Dec 2025 08:27:30 GMT\nCache-Control: public, max-age=2592000\nServer: gws\nContent-Length: 220\nX-XSS-Protection: 0\nX-Frame-Options: SAMEORIGIN\nAlt-Svc: h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000\nConnection: close",
|
||||
truncated: false,
|
||||
statusCode: 301,
|
||||
statusCodeName: "Moved Permanently",
|
||||
timings: {
|
||||
total: 1440,
|
||||
download: 1,
|
||||
firstByte: 1391,
|
||||
dns: 9,
|
||||
tls: 22,
|
||||
tcp: 16,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user