mirror of
https://github.com/louislam/uptime-kuma.git
synced 2026-03-03 00:47:02 +00:00
feat: add DNS support via Globalping (#6850)
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
const { MonitorType } = require("./monitor-type");
|
const { MonitorType } = require("./monitor-type");
|
||||||
const { Globalping, IpVersion } = require("globalping");
|
const { Globalping, IpVersion } = require("globalping");
|
||||||
const { Settings } = require("../settings");
|
const { Settings } = require("../settings");
|
||||||
const { log, UP, DOWN, evaluateJsonQuery } = require("../../src/util");
|
const { log, UP, evaluateJsonQuery } = require("../../src/util");
|
||||||
const {
|
const {
|
||||||
checkStatusCode,
|
checkStatusCode,
|
||||||
getOidcTokenClientCredentials,
|
getOidcTokenClientCredentials,
|
||||||
@@ -50,6 +50,9 @@ class GlobalpingMonitorType extends MonitorType {
|
|||||||
case "http":
|
case "http":
|
||||||
await this.http(client, monitor, heartbeat, hasAPIToken);
|
await this.http(client, monitor, heartbeat, hasAPIToken);
|
||||||
break;
|
break;
|
||||||
|
case "dns":
|
||||||
|
await this.dns(client, monitor, heartbeat, hasAPIToken, R);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,15 +110,11 @@ class GlobalpingMonitorType extends MonitorType {
|
|||||||
const result = measurement.data.results[0].result;
|
const result = measurement.data.results[0].result;
|
||||||
|
|
||||||
if (result.status === "failed") {
|
if (result.status === "failed") {
|
||||||
heartbeat.msg = this.formatResponse(probe, `Failed: ${result.rawOutput}`);
|
throw new Error(this.formatResponse(probe, `Failed: ${result.rawOutput}`));
|
||||||
heartbeat.status = DOWN;
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!result.timings?.length) {
|
if (!result.timings?.length) {
|
||||||
heartbeat.msg = this.formatResponse(probe, `Failed: ${result.rawOutput}`);
|
throw new Error(this.formatResponse(probe, `Failed: ${result.rawOutput}`));
|
||||||
heartbeat.status = DOWN;
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
heartbeat.ping = result.stats.avg || 0;
|
heartbeat.ping = result.stats.avg || 0;
|
||||||
@@ -208,20 +207,15 @@ class GlobalpingMonitorType extends MonitorType {
|
|||||||
const result = measurement.data.results[0].result;
|
const result = measurement.data.results[0].result;
|
||||||
|
|
||||||
if (result.status === "failed") {
|
if (result.status === "failed") {
|
||||||
heartbeat.msg = this.formatResponse(probe, `Failed: ${result.rawOutput}`);
|
throw new Error(this.formatResponse(probe, `Failed: ${result.rawOutput}`));
|
||||||
heartbeat.status = DOWN;
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
heartbeat.ping = result.timings.total || 0;
|
heartbeat.ping = result.timings.total || 0;
|
||||||
|
|
||||||
if (!checkStatusCode(result.statusCode, JSON.parse(monitor.accepted_statuscodes_json))) {
|
if (!checkStatusCode(result.statusCode, JSON.parse(monitor.accepted_statuscodes_json))) {
|
||||||
heartbeat.msg = this.formatResponse(
|
throw new Error(
|
||||||
probe,
|
this.formatResponse(probe, `Status code ${result.statusCode} not accepted. Output: ${result.rawOutput}`)
|
||||||
`Status code ${result.statusCode} not accepted. Output: ${result.rawOutput}`
|
|
||||||
);
|
);
|
||||||
heartbeat.status = DOWN;
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
heartbeat.msg = this.formatResponse(probe, `${result.statusCode} - ${result.statusCodeName}`);
|
heartbeat.msg = this.formatResponse(probe, `${result.statusCode} - ${result.statusCodeName}`);
|
||||||
@@ -244,13 +238,135 @@ class GlobalpingMonitorType extends MonitorType {
|
|||||||
heartbeat.status = UP;
|
heartbeat.status = UP;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles DNS 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.
|
||||||
|
* @param {R} redbean - The redbean object.
|
||||||
|
* @returns {Promise<void>} A promise that resolves when the HTTP monitor is handled.
|
||||||
|
*/
|
||||||
|
async dns(client, monitor, heartbeat, hasAPIToken, redbean) {
|
||||||
|
const opts = {
|
||||||
|
type: "dns",
|
||||||
|
target: monitor.hostname,
|
||||||
|
inProgressUpdates: false,
|
||||||
|
limit: 1,
|
||||||
|
locations: [{ magic: monitor.location }],
|
||||||
|
measurementOptions: {
|
||||||
|
query: {
|
||||||
|
type: monitor.dns_resolve_type,
|
||||||
|
},
|
||||||
|
port: monitor.port,
|
||||||
|
protocol: monitor.protocol,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
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);
|
||||||
|
log.debug("monitor", `Globalping ${JSON.stringify(res)}`);
|
||||||
|
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") {
|
||||||
|
throw new Error(this.formatResponse(probe, `Failed: ${result.rawOutput}`));
|
||||||
|
}
|
||||||
|
|
||||||
|
let dnsMessage = (result.answers || []).map((answer) => answer.value).join(" | ");
|
||||||
|
const values = (result.answers || []).map((answer) => answer.value);
|
||||||
|
|
||||||
|
let recordMatched = true;
|
||||||
|
|
||||||
|
// keyword
|
||||||
|
if (monitor.keyword) {
|
||||||
|
recordMatched = this.checkDNSRecordValueMatch(monitor, values, monitor.keyword);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (monitor.dns_last_result !== dnsMessage && dnsMessage !== undefined) {
|
||||||
|
await redbean.exec("UPDATE `monitor` SET dns_last_result = ? WHERE id = ? ", [dnsMessage, monitor.id]);
|
||||||
|
}
|
||||||
|
|
||||||
|
heartbeat.ping = result.timings.total || 0;
|
||||||
|
|
||||||
|
if (!dnsMessage) {
|
||||||
|
dnsMessage = `No records found. ${result.statusCodeName}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!recordMatched) {
|
||||||
|
throw new Error(this.formatResponse(probe, "No record matched. " + dnsMessage));
|
||||||
|
}
|
||||||
|
|
||||||
|
heartbeat.msg = this.formatResponse(probe, dnsMessage);
|
||||||
|
heartbeat.status = UP;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles keyword for HTTP monitors.
|
* Handles keyword for HTTP monitors.
|
||||||
* @param {Monitor} monitor - The monitor object.
|
* @param {Monitor} monitor - The monitor object.
|
||||||
|
* @param {Array<string>} values - The values to search for.
|
||||||
|
* @param {string} keyword - The keyword to search for.
|
||||||
|
* @returns {boolean} True if the regex matches, false otherwise.
|
||||||
|
*/
|
||||||
|
checkDNSRecordValueMatch(monitor, values, keyword) {
|
||||||
|
const regex = new RegExp(keyword, "i");
|
||||||
|
|
||||||
|
switch (monitor.dns_resolve_type) {
|
||||||
|
case "A":
|
||||||
|
case "AAAA":
|
||||||
|
case "ANY":
|
||||||
|
case "CNAME":
|
||||||
|
case "DNSKEY":
|
||||||
|
case "DS":
|
||||||
|
case "HTTPS":
|
||||||
|
case "MX":
|
||||||
|
case "NS":
|
||||||
|
case "NSEC":
|
||||||
|
case "PTR":
|
||||||
|
case "RRSIG":
|
||||||
|
case "SOA":
|
||||||
|
case "SRV":
|
||||||
|
case "SVCB":
|
||||||
|
case "TXT":
|
||||||
|
return values.some((record) => regex.test(record));
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles keyword search for HTTP monitors.
|
||||||
|
* @param {Monitor} monitor - The monitor object.
|
||||||
* @param {Heartbeat} heartbeat - The heartbeat object.
|
* @param {Heartbeat} heartbeat - The heartbeat object.
|
||||||
* @param {Result} result - The result object.
|
* @param {Result} result - The result object.
|
||||||
* @param {Probe} probe - The probe object.
|
* @param {Probe} probe - The probe object.
|
||||||
* @returns {Promise<void>} A promise that resolves when the keyword is handled.
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
async handleKeywordForHTTP(monitor, heartbeat, result, probe) {
|
async handleKeywordForHTTP(monitor, heartbeat, result, probe) {
|
||||||
let data = result.rawOutput;
|
let data = result.rawOutput;
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ class NotificationProvider {
|
|||||||
case "globalping":
|
case "globalping":
|
||||||
switch (monitorJSON["subtype"]) {
|
switch (monitorJSON["subtype"]) {
|
||||||
case "ping":
|
case "ping":
|
||||||
|
case "dns":
|
||||||
return monitorJSON["hostname"];
|
return monitorJSON["hostname"];
|
||||||
case "http":
|
case "http":
|
||||||
return monitorJSON["url"];
|
return monitorJSON["url"];
|
||||||
|
|||||||
@@ -745,7 +745,11 @@ let needSetup = false;
|
|||||||
* List of frontend-only properties that should not be saved to the database.
|
* List of frontend-only properties that should not be saved to the database.
|
||||||
* Should clean up before saving to the database.
|
* Should clean up before saving to the database.
|
||||||
*/
|
*/
|
||||||
const frontendOnlyProperties = ["humanReadableInterval", "responsecheck"];
|
const frontendOnlyProperties = [
|
||||||
|
"humanReadableInterval",
|
||||||
|
"globalpingdnsresolvetypeoptions",
|
||||||
|
"responsecheck",
|
||||||
|
];
|
||||||
for (const prop of frontendOnlyProperties) {
|
for (const prop of frontendOnlyProperties) {
|
||||||
if (prop in monitor) {
|
if (prop in monitor) {
|
||||||
delete monitor[prop];
|
delete monitor[prop];
|
||||||
|
|||||||
@@ -1349,7 +1349,7 @@
|
|||||||
"Send DOWN silently": "Send DOWN 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.",
|
"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",
|
"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}.",
|
"GlobalpingMonitorDescription": "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}. Check the {docs} for more information.",
|
||||||
"Globalping API Token": "Globalping API Token",
|
"Globalping API Token": "Globalping API Token",
|
||||||
"globalpingApiTokenDescription": "Get your Globalping API Token at {0}.",
|
"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.",
|
"GlobalpingHostname": "A publicly reachable measurement target. Typically a hostname or IPv4/IPv6 address, depending on the measurement type.",
|
||||||
@@ -1357,6 +1357,8 @@
|
|||||||
"GlobalpingLocationDocs": "Full location input documentation",
|
"GlobalpingLocationDocs": "Full location input documentation",
|
||||||
"GlobalpingIpFamilyInfo": "The IP version to use. Only allowed if the target is a hostname.",
|
"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.",
|
"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.",
|
||||||
|
"RecordMatch": "Record value match",
|
||||||
|
"RegexMatch": "Enter a regex to match the record value",
|
||||||
"Resolver Server": "Resolver Server",
|
"Resolver Server": "Resolver Server",
|
||||||
"Protocol": "Protocol",
|
"Protocol": "Protocol",
|
||||||
"account settings": "account settings",
|
"account settings": "account settings",
|
||||||
|
|||||||
@@ -44,6 +44,12 @@
|
|||||||
<span>{{ $t("Location") }}:</span>
|
<span>{{ $t("Location") }}:</span>
|
||||||
<span class="keyword">{{ monitor.location }}</span>
|
<span class="keyword">{{ monitor.location }}</span>
|
||||||
<br />
|
<br />
|
||||||
|
<span v-if="monitor.subtype === 'dns'">
|
||||||
|
[{{ monitor.dns_resolve_type }}]
|
||||||
|
<br />
|
||||||
|
<span>{{ $t("Last Result") }}:</span>
|
||||||
|
<span class="keyword">{{ monitor.dns_last_result }}</span>
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
<span v-if="monitor.type === 'keyword'">
|
<span v-if="monitor.type === 'keyword'">
|
||||||
<br />
|
<br />
|
||||||
|
|||||||
@@ -10,13 +10,21 @@
|
|||||||
|
|
||||||
<i18n-t
|
<i18n-t
|
||||||
v-if="monitor.type === 'globalping'"
|
v-if="monitor.type === 'globalping'"
|
||||||
keypath="GlobalpingDescription"
|
keypath="GlobalpingMonitorDescription"
|
||||||
tag="p"
|
tag="p"
|
||||||
class="form-text"
|
class="form-text"
|
||||||
>
|
>
|
||||||
<template #accountSettings>
|
<template #accountSettings>
|
||||||
<router-link to="/settings/general">{{ $t("account settings") }}</router-link>
|
<router-link to="/settings/general">{{ $t("account settings") }}</router-link>
|
||||||
</template>
|
</template>
|
||||||
|
<template #docs>
|
||||||
|
<a
|
||||||
|
href="https://github.com/jsdelivr/globalping?tab=readme-ov-file#uptime-monitoring-use-cases"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
{{ $t("documentation") }}
|
||||||
|
</a>
|
||||||
|
</template>
|
||||||
</i18n-t>
|
</i18n-t>
|
||||||
|
|
||||||
<div class="my-3">
|
<div class="my-3">
|
||||||
@@ -122,6 +130,7 @@
|
|||||||
>
|
>
|
||||||
<option value="ping">Ping</option>
|
<option value="ping">Ping</option>
|
||||||
<option value="http">HTTP(s)</option>
|
<option value="http">HTTP(s)</option>
|
||||||
|
<option value="dns">DNS</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -472,7 +481,7 @@
|
|||||||
<!-- Globalping -->
|
<!-- Globalping -->
|
||||||
<template v-if="monitor.type === 'globalping'">
|
<template v-if="monitor.type === 'globalping'">
|
||||||
<!-- Hostname -->
|
<!-- Hostname -->
|
||||||
<div v-if="monitor.subtype === 'ping'" class="my-3">
|
<div v-if="monitor.subtype === 'ping' || monitor.subtype === 'dns'" class="my-3">
|
||||||
<label for="hostname" class="form-label">{{ $t("Hostname") }}</label>
|
<label for="hostname" class="form-label">{{ $t("Hostname") }}</label>
|
||||||
<input
|
<input
|
||||||
id="hostname"
|
id="hostname"
|
||||||
@@ -548,7 +557,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="monitor.subtype === 'http'" class="my-3">
|
<div v-if="monitor.subtype === 'http' || monitor.subtype === 'dns'" class="my-3">
|
||||||
<label for="dns_resolve_server" class="form-label">
|
<label for="dns_resolve_server" class="form-label">
|
||||||
{{ $t("Resolver Server") }}
|
{{ $t("Resolver Server") }}
|
||||||
</label>
|
</label>
|
||||||
@@ -563,6 +572,67 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- DNS -->
|
||||||
|
<template v-if="monitor.subtype === 'dns'">
|
||||||
|
<!-- Port -->
|
||||||
|
<div class="my-3">
|
||||||
|
<label for="port" class="form-label">{{ $t("Port") }}</label>
|
||||||
|
<input
|
||||||
|
id="port"
|
||||||
|
v-model="monitor.port"
|
||||||
|
type="number"
|
||||||
|
class="form-control"
|
||||||
|
required
|
||||||
|
min="0"
|
||||||
|
max="65535"
|
||||||
|
step="1"
|
||||||
|
value="53"
|
||||||
|
/>
|
||||||
|
<div class="form-text">
|
||||||
|
{{ $t("dnsPortDescription") }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="my-3">
|
||||||
|
<label for="dns_resolve_type" class="form-label">
|
||||||
|
{{ $t("Resource Record Type") }}
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<!-- :allow-empty="false" is not working, set a default value instead https://github.com/shentao/vue-multiselect/issues/336 -->
|
||||||
|
<VueMultiselect
|
||||||
|
id="dns_resolve_type"
|
||||||
|
v-model="monitor.dns_resolve_type"
|
||||||
|
:options="globalpingdnsresolvetypeoptions"
|
||||||
|
:multiple="false"
|
||||||
|
:close-on-select="true"
|
||||||
|
:clear-on-select="false"
|
||||||
|
:preserve-search="false"
|
||||||
|
:placeholder="$t('Pick a RR-Type...')"
|
||||||
|
:preselect-first="false"
|
||||||
|
:max-height="500"
|
||||||
|
:taggable="false"
|
||||||
|
data-testid="resolve-type-select"
|
||||||
|
></VueMultiselect>
|
||||||
|
|
||||||
|
<div class="form-text">
|
||||||
|
{{ $t("rrtypeDescription") }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="my-3">
|
||||||
|
<label for="keyword" class="form-label">{{ $t("RecordMatch") }}</label>
|
||||||
|
<input
|
||||||
|
id="keyword"
|
||||||
|
v-model="monitor.keyword"
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
/>
|
||||||
|
<div class="form-text">
|
||||||
|
{{ $t("RegexMatch") }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
<!-- Protocol -->
|
<!-- Protocol -->
|
||||||
<div class="my-3">
|
<div class="my-3">
|
||||||
<label for="protocol" class="form-label">{{ $t("Protocol") }}</label>
|
<label for="protocol" class="form-label">{{ $t("Protocol") }}</label>
|
||||||
@@ -575,6 +645,10 @@
|
|||||||
<option :value="null">{{ $t("auto-select") }}</option>
|
<option :value="null">{{ $t("auto-select") }}</option>
|
||||||
<option value="HTTP2">HTTP2</option>
|
<option value="HTTP2">HTTP2</option>
|
||||||
</template>
|
</template>
|
||||||
|
<template v-else-if="monitor.subtype === 'dns'">
|
||||||
|
<option value="UDP">UDP</option>
|
||||||
|
<option value="TCP">TCP</option>
|
||||||
|
</template>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -2829,6 +2903,7 @@ export default {
|
|||||||
acceptedStatusCodeOptions: [],
|
acceptedStatusCodeOptions: [],
|
||||||
acceptedWebsocketCodeOptions: [],
|
acceptedWebsocketCodeOptions: [],
|
||||||
dnsresolvetypeOptions: [],
|
dnsresolvetypeOptions: [],
|
||||||
|
globalpingdnsresolvetypeoptions: [],
|
||||||
kafkaSaslMechanismOptions: [],
|
kafkaSaslMechanismOptions: [],
|
||||||
gameList: null,
|
gameList: null,
|
||||||
connectionStringTemplates: {
|
connectionStringTemplates: {
|
||||||
@@ -3310,14 +3385,26 @@ message HealthCheckResponse {
|
|||||||
if (!oldSubtype && !this.monitor.protocol) {
|
if (!oldSubtype && !this.monitor.protocol) {
|
||||||
if (newSubtype === "ping") {
|
if (newSubtype === "ping") {
|
||||||
this.monitor.protocol = "ICMP";
|
this.monitor.protocol = "ICMP";
|
||||||
|
} else if (newSubtype === "dns") {
|
||||||
|
this.monitor.protocol = "UDP";
|
||||||
} else if (newSubtype === "http") {
|
} else if (newSubtype === "http") {
|
||||||
this.monitor.protocol = null;
|
this.monitor.protocol = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!oldSubtype && this.monitor.port === undefined) {
|
||||||
|
if (newSubtype === "dns") {
|
||||||
|
this.monitor.port = "53";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (newSubtype !== oldSubtype) {
|
if (newSubtype !== oldSubtype) {
|
||||||
if (newSubtype === "ping") {
|
if (newSubtype === "ping") {
|
||||||
this.monitor.protocol = "ICMP";
|
this.monitor.protocol = "ICMP";
|
||||||
this.monitor.port = "80";
|
this.monitor.port = "80";
|
||||||
|
} else if (newSubtype === "dns") {
|
||||||
|
this.monitor.protocol = "UDP";
|
||||||
|
this.monitor.port = "53";
|
||||||
} else if (newSubtype === "http") {
|
} else if (newSubtype === "http") {
|
||||||
this.monitor.protocol = null;
|
this.monitor.protocol = null;
|
||||||
}
|
}
|
||||||
@@ -3364,6 +3451,24 @@ message HealthCheckResponse {
|
|||||||
let acceptedWebsocketCodeOptions = [];
|
let acceptedWebsocketCodeOptions = [];
|
||||||
|
|
||||||
let dnsresolvetypeOptions = ["A", "AAAA", "CAA", "CNAME", "MX", "NS", "PTR", "SOA", "SRV", "TXT"];
|
let dnsresolvetypeOptions = ["A", "AAAA", "CAA", "CNAME", "MX", "NS", "PTR", "SOA", "SRV", "TXT"];
|
||||||
|
const globalpingdnsresolvetypeoptions = [
|
||||||
|
"A",
|
||||||
|
"AAAA",
|
||||||
|
"ANY",
|
||||||
|
"CNAME",
|
||||||
|
"DNSKEY",
|
||||||
|
"DS",
|
||||||
|
"HTTPS",
|
||||||
|
"MX",
|
||||||
|
"NS",
|
||||||
|
"NSEC",
|
||||||
|
"PTR",
|
||||||
|
"RRSIG",
|
||||||
|
"SOA",
|
||||||
|
"SRV",
|
||||||
|
"SVCB",
|
||||||
|
"TXT",
|
||||||
|
];
|
||||||
|
|
||||||
let kafkaSaslMechanismOptions = ["None", "plain", "scram-sha-256", "scram-sha-512", "aws"];
|
let kafkaSaslMechanismOptions = ["None", "plain", "scram-sha-256", "scram-sha-512", "aws"];
|
||||||
|
|
||||||
@@ -3378,6 +3483,7 @@ message HealthCheckResponse {
|
|||||||
this.acceptedWebsocketCodeOptions = acceptedWebsocketCodeOptions;
|
this.acceptedWebsocketCodeOptions = acceptedWebsocketCodeOptions;
|
||||||
this.acceptedStatusCodeOptions = acceptedStatusCodeOptions;
|
this.acceptedStatusCodeOptions = acceptedStatusCodeOptions;
|
||||||
this.dnsresolvetypeOptions = dnsresolvetypeOptions;
|
this.dnsresolvetypeOptions = dnsresolvetypeOptions;
|
||||||
|
this.globalpingdnsresolvetypeoptions = globalpingdnsresolvetypeoptions;
|
||||||
this.kafkaSaslMechanismOptions = kafkaSaslMechanismOptions;
|
this.kafkaSaslMechanismOptions = kafkaSaslMechanismOptions;
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
const { describe, test, mock } = require("node:test");
|
const { describe, test, mock } = require("node:test");
|
||||||
const assert = require("node:assert");
|
const assert = require("node:assert");
|
||||||
const { encodeBase64 } = require("../../server/util-server");
|
const { encodeBase64 } = require("../../server/util-server");
|
||||||
const { UP, DOWN, PENDING } = require("../../src/util");
|
const { UP, PENDING } = require("../../src/util");
|
||||||
|
|
||||||
describe("GlobalpingMonitorType", () => {
|
describe("GlobalpingMonitorType", () => {
|
||||||
const { GlobalpingMonitorType } = require("../../server/monitor-types/globalping");
|
const { GlobalpingMonitorType } = require("../../server/monitor-types/globalping");
|
||||||
@@ -82,11 +82,12 @@ describe("GlobalpingMonitorType", () => {
|
|||||||
msg: "",
|
msg: "",
|
||||||
};
|
};
|
||||||
|
|
||||||
await monitorType.ping(mockClient, monitor, heartbeat, true);
|
await assert.rejects(monitorType.ping(mockClient, monitor, heartbeat, true), (error) => {
|
||||||
|
assert.deepStrictEqual(
|
||||||
assert.deepStrictEqual(heartbeat, {
|
error,
|
||||||
status: DOWN,
|
new Error("Ashburn (VA), US, NA, Amazon.com (AS14618), (aws-us-east-1) : Failed: Host unreachable")
|
||||||
msg: "Ashburn (VA), US, NA, Amazon.com (AS14618), (aws-us-east-1) : Failed: Host unreachable",
|
);
|
||||||
|
return true;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -260,11 +261,12 @@ describe("GlobalpingMonitorType", () => {
|
|||||||
msg: "",
|
msg: "",
|
||||||
};
|
};
|
||||||
|
|
||||||
await monitorType.http(mockClient, monitor, heartbeat, true);
|
await assert.rejects(monitorType.http(mockClient, monitor, heartbeat, true), (error) => {
|
||||||
|
assert.deepStrictEqual(
|
||||||
assert.deepStrictEqual(heartbeat, {
|
error,
|
||||||
status: DOWN,
|
new Error("New York (NY), US, NA, MASSIVEGRID (AS49683) : Failed: Host unreachable")
|
||||||
msg: "New York (NY), US, NA, MASSIVEGRID (AS49683) : Failed: Host unreachable",
|
);
|
||||||
|
return true;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -372,13 +374,18 @@ describe("GlobalpingMonitorType", () => {
|
|||||||
ping: 0,
|
ping: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
await monitorType.http(mockClient, monitor, heartbeat, true);
|
await assert.rejects(monitorType.http(mockClient, monitor, heartbeat, true), (error) => {
|
||||||
|
assert.deepStrictEqual(
|
||||||
assert.deepStrictEqual(heartbeat, {
|
error,
|
||||||
status: DOWN,
|
new Error(
|
||||||
msg: "New York (NY), US, NA, MASSIVEGRID (AS49683) : Status code 301 not accepted. Output: RAW OUTPUT",
|
"New York (NY), US, NA, MASSIVEGRID (AS49683) : Status code 301 not accepted. Output: RAW OUTPUT"
|
||||||
ping: 1440,
|
)
|
||||||
|
);
|
||||||
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// heartbeat.ping should still be set before the error is thrown
|
||||||
|
assert.strictEqual(heartbeat.ping, 1440);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should handle keyword check (keyword present)", async () => {
|
test("should handle keyword check (keyword present)", async () => {
|
||||||
@@ -574,6 +581,271 @@ describe("GlobalpingMonitorType", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("dns", () => {
|
||||||
|
test("should handle successful dns", async () => {
|
||||||
|
const monitorType = new GlobalpingMonitorType("test-agent/1.0");
|
||||||
|
const mockClient = createGlobalpingClientMock();
|
||||||
|
const createResponse = createMockResponse({
|
||||||
|
id: "2g8T7V3OwXG3JV6Y10011zF2v",
|
||||||
|
});
|
||||||
|
const measurement = createDnsMeasurement();
|
||||||
|
const awaitResponse = createMockResponse(measurement);
|
||||||
|
|
||||||
|
mockClient.createMeasurement.mock.mockImplementation(() => createMockResponse(createResponse));
|
||||||
|
mockClient.awaitMeasurement.mock.mockImplementation(() => awaitResponse);
|
||||||
|
|
||||||
|
const redbeanMock = createRedbeanMock();
|
||||||
|
redbeanMock.exec.mock.mockImplementation(() => Promise.resolve());
|
||||||
|
|
||||||
|
const monitor = {
|
||||||
|
id: "1",
|
||||||
|
hostname: "example.com",
|
||||||
|
location: "us-east-1",
|
||||||
|
dns_resolve_type: "A",
|
||||||
|
port: 53,
|
||||||
|
protocol: "udp",
|
||||||
|
};
|
||||||
|
|
||||||
|
const heartbeat = {
|
||||||
|
status: PENDING,
|
||||||
|
msg: "",
|
||||||
|
ping: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
await monitorType.dns(mockClient, monitor, heartbeat, false, redbeanMock);
|
||||||
|
|
||||||
|
assert.strictEqual(mockClient.createMeasurement.mock.calls.length, 1);
|
||||||
|
assert.deepStrictEqual(mockClient.createMeasurement.mock.calls[0].arguments[0], {
|
||||||
|
type: "dns",
|
||||||
|
target: "example.com",
|
||||||
|
inProgressUpdates: false,
|
||||||
|
limit: 1,
|
||||||
|
locations: [{ magic: "us-east-1" }],
|
||||||
|
measurementOptions: {
|
||||||
|
query: { type: "A" },
|
||||||
|
port: 53,
|
||||||
|
protocol: "udp",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.deepStrictEqual(heartbeat, {
|
||||||
|
status: UP,
|
||||||
|
msg: "New York (NY), US, NA, MASSIVEGRID (AS49683) : 93.184.216.34",
|
||||||
|
ping: 25,
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.strictEqual(redbeanMock.exec.mock.calls.length, 1);
|
||||||
|
assert.deepStrictEqual(redbeanMock.exec.mock.calls[0].arguments, [
|
||||||
|
"UPDATE `monitor` SET dns_last_result = ? WHERE id = ? ",
|
||||||
|
["93.184.216.34", "1"],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should handle failed dns with status failed", async () => {
|
||||||
|
const monitorType = new GlobalpingMonitorType("test-agent/1.0");
|
||||||
|
const mockClient = createGlobalpingClientMock();
|
||||||
|
const createResponse = createMockResponse({
|
||||||
|
id: "2g8T7V3OwXG3JV6Y10011zF2v",
|
||||||
|
});
|
||||||
|
const measurement = createDnsMeasurement();
|
||||||
|
measurement.results[0].result.status = "failed";
|
||||||
|
measurement.results[0].result.rawOutput = "NXDOMAIN";
|
||||||
|
const awaitResponse = createMockResponse(measurement);
|
||||||
|
|
||||||
|
mockClient.createMeasurement.mock.mockImplementation(() => createMockResponse(createResponse));
|
||||||
|
mockClient.awaitMeasurement.mock.mockImplementation(() => awaitResponse);
|
||||||
|
|
||||||
|
const monitor = {
|
||||||
|
hostname: "nonexistent.example.com",
|
||||||
|
location: "us-east-1",
|
||||||
|
dns_resolve_type: "A",
|
||||||
|
port: 53,
|
||||||
|
};
|
||||||
|
|
||||||
|
const heartbeat = {
|
||||||
|
status: PENDING,
|
||||||
|
msg: "",
|
||||||
|
};
|
||||||
|
|
||||||
|
await assert.rejects(
|
||||||
|
async () => {
|
||||||
|
await monitorType.dns(mockClient, monitor, heartbeat, false);
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
assert.strictEqual(
|
||||||
|
error.message,
|
||||||
|
"New York (NY), US, NA, MASSIVEGRID (AS49683) : Failed: NXDOMAIN"
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
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: "us-east-1",
|
||||||
|
dns_resolve_type: "A",
|
||||||
|
port: 53,
|
||||||
|
};
|
||||||
|
|
||||||
|
const heartbeat = {
|
||||||
|
status: PENDING,
|
||||||
|
msg: "",
|
||||||
|
};
|
||||||
|
|
||||||
|
await assert.rejects(monitorType.dns(mockClient, monitor, heartbeat, false), (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: "us-east-1",
|
||||||
|
dns_resolve_type: "A",
|
||||||
|
port: 53,
|
||||||
|
};
|
||||||
|
|
||||||
|
const heartbeat = {
|
||||||
|
status: PENDING,
|
||||||
|
msg: "",
|
||||||
|
};
|
||||||
|
|
||||||
|
await assert.rejects(monitorType.dns(mockClient, monitor, heartbeat, false), (error) => {
|
||||||
|
assert.deepStrictEqual(
|
||||||
|
error,
|
||||||
|
new Error("Failed to fetch measurement (2g8T7V3OwXG3JV6Y10011zF2v): internal_error Server error.")
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should handle regex matched", async () => {
|
||||||
|
const monitorType = new GlobalpingMonitorType("test-agent/1.0");
|
||||||
|
const mockClient = createGlobalpingClientMock();
|
||||||
|
const createResponse = {
|
||||||
|
id: "2g8T7V3OwXG3JV6Y10011zF2v",
|
||||||
|
};
|
||||||
|
const measurement = createDnsMeasurement();
|
||||||
|
const awaitResponse = createMockResponse(measurement);
|
||||||
|
|
||||||
|
mockClient.createMeasurement.mock.mockImplementation(() => createMockResponse(createResponse));
|
||||||
|
mockClient.awaitMeasurement.mock.mockImplementation(() => awaitResponse);
|
||||||
|
|
||||||
|
const redbeanMock = createRedbeanMock();
|
||||||
|
redbeanMock.exec.mock.mockImplementation(() => Promise.resolve());
|
||||||
|
|
||||||
|
const monitor = {
|
||||||
|
id: "1",
|
||||||
|
hostname: "example.com",
|
||||||
|
location: "us-east-1",
|
||||||
|
dns_resolve_type: "A",
|
||||||
|
port: 53,
|
||||||
|
protocol: "udp",
|
||||||
|
keyword: "93\\.184\\.216\\.34",
|
||||||
|
};
|
||||||
|
|
||||||
|
const heartbeat = {
|
||||||
|
status: PENDING,
|
||||||
|
msg: "",
|
||||||
|
ping: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
await monitorType.dns(mockClient, monitor, heartbeat, false, redbeanMock);
|
||||||
|
|
||||||
|
assert.deepStrictEqual(heartbeat, {
|
||||||
|
status: UP,
|
||||||
|
msg: "New York (NY), US, NA, MASSIVEGRID (AS49683) : 93.184.216.34",
|
||||||
|
ping: 25,
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.deepStrictEqual(redbeanMock.exec.mock.calls[0].arguments, [
|
||||||
|
"UPDATE `monitor` SET dns_last_result = ? WHERE id = ? ",
|
||||||
|
["93.184.216.34", "1"],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should handle regex not matched", async () => {
|
||||||
|
const monitorType = new GlobalpingMonitorType("test-agent/1.0");
|
||||||
|
const mockClient = createGlobalpingClientMock();
|
||||||
|
const createResponse = {
|
||||||
|
id: "2g8T7V3OwXG3JV6Y10011zF2v",
|
||||||
|
};
|
||||||
|
const measurement = createDnsMeasurement();
|
||||||
|
const awaitResponse = createMockResponse(measurement);
|
||||||
|
|
||||||
|
mockClient.createMeasurement.mock.mockImplementation(() => createMockResponse(createResponse));
|
||||||
|
mockClient.awaitMeasurement.mock.mockImplementation(() => awaitResponse);
|
||||||
|
|
||||||
|
const redbeanMock = createRedbeanMock();
|
||||||
|
redbeanMock.exec.mock.mockImplementation(() => Promise.resolve());
|
||||||
|
|
||||||
|
const monitor = {
|
||||||
|
id: "1",
|
||||||
|
hostname: "example.com",
|
||||||
|
location: "us-east-1",
|
||||||
|
dns_resolve_type: "A",
|
||||||
|
port: 53,
|
||||||
|
protocol: "udp",
|
||||||
|
keyword: "192\\.168\\.1\\.1",
|
||||||
|
};
|
||||||
|
|
||||||
|
const heartbeat = {
|
||||||
|
status: PENDING,
|
||||||
|
msg: "",
|
||||||
|
};
|
||||||
|
|
||||||
|
await assert.rejects(monitorType.dns(mockClient, monitor, heartbeat, false, redbeanMock), (error) => {
|
||||||
|
assert.deepStrictEqual(
|
||||||
|
error,
|
||||||
|
new Error("New York (NY), US, NA, MASSIVEGRID (AS49683) : No record matched. 93.184.216.34")
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.deepStrictEqual(redbeanMock.exec.mock.calls[0].arguments, [
|
||||||
|
"UPDATE `monitor` SET dns_last_result = ? WHERE id = ? ",
|
||||||
|
["93.184.216.34", "1"],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("helper methods", () => {
|
describe("helper methods", () => {
|
||||||
test("formatProbeLocation should format location correctly", () => {
|
test("formatProbeLocation should format location correctly", () => {
|
||||||
const monitorType = new GlobalpingMonitorType("test-agent/1.0");
|
const monitorType = new GlobalpingMonitorType("test-agent/1.0");
|
||||||
@@ -722,6 +994,16 @@ function createGlobalpingClientMock() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reusable mock factory for RedBean
|
||||||
|
* @returns {object} Mocked RedBean
|
||||||
|
*/
|
||||||
|
function createRedbeanMock() {
|
||||||
|
return {
|
||||||
|
exec: mock.fn(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reusable mock factory for Globalping response
|
* Reusable mock factory for Globalping response
|
||||||
* @param {object} data Response data
|
* @param {object} data Response data
|
||||||
@@ -866,3 +1148,53 @@ function createHttpMeasurement() {
|
|||||||
],
|
],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a successful DNS measurement response
|
||||||
|
* @returns {object} Mock measurement response
|
||||||
|
*/
|
||||||
|
function createDnsMeasurement() {
|
||||||
|
return {
|
||||||
|
id: "2g8T7V3OwXG3JV6Y10011zF2v",
|
||||||
|
type: "dns",
|
||||||
|
status: "finished",
|
||||||
|
createdAt: "2025-11-05T08:30:00.000Z",
|
||||||
|
updatedAt: "2025-11-05T08:30:01.000Z",
|
||||||
|
target: "example.com",
|
||||||
|
probesCount: 1,
|
||||||
|
locations: [{ magic: "us-east-1" }],
|
||||||
|
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",
|
||||||
|
rawOutput: ";; ANSWER SECTION:\nexample.com.\t\t86400\tIN\tA\t93.184.216.34",
|
||||||
|
answers: [
|
||||||
|
{
|
||||||
|
name: "example.com.",
|
||||||
|
type: "A",
|
||||||
|
ttl: 86400,
|
||||||
|
class: "IN",
|
||||||
|
value: "93.184.216.34",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
timings: {
|
||||||
|
total: 25,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user