mirror of
https://github.com/louislam/uptime-kuma.git
synced 2025-07-18 23:34:04 +02:00
Write tests, more optimizations
This commit is contained in:
parent
a670386281
commit
b0bd8eeac5
6 changed files with 292 additions and 51 deletions
|
@ -4,7 +4,11 @@ const dayjs = require("dayjs");
|
|||
const { dnsResolve } = require("../util-server");
|
||||
const { R } = require("redbean-node");
|
||||
const { ConditionVariable } = require("../monitor-conditions/variables");
|
||||
const { defaultStringOperators, defaultNumberOperators, defaultArrayOperators } = require("../monitor-conditions/operators");
|
||||
const {
|
||||
defaultStringOperators,
|
||||
defaultNumberOperators,
|
||||
defaultArrayOperators
|
||||
} = require("../monitor-conditions/operators");
|
||||
const { ConditionExpressionGroup } = require("../monitor-conditions/expression");
|
||||
const { evaluateExpressionGroup } = require("../monitor-conditions/evaluator");
|
||||
|
||||
|
@ -14,9 +18,9 @@ class DnsMonitorType extends MonitorType {
|
|||
supportsConditions = true;
|
||||
|
||||
conditionVariables = [
|
||||
// A, AAAA, TXT, PTR, NS
|
||||
// A, AAAA, TXT, NS
|
||||
new ConditionVariable("records", defaultArrayOperators),
|
||||
// CNAME
|
||||
// PTR, CNAME
|
||||
new ConditionVariable("hostname", defaultStringOperators),
|
||||
// CAA
|
||||
new ConditionVariable("flags", defaultStringOperators),
|
||||
|
@ -43,10 +47,11 @@ class DnsMonitorType extends MonitorType {
|
|||
name: monitor.hostname,
|
||||
rrtype: monitor.dnsResolveType,
|
||||
dnssec: true, // Request DNSSEC information in the response
|
||||
dnssecCheckingDisabled: monitor.skip_remote_dnssec,
|
||||
dnssecCheckingDisabled: monitor.skipRemoteDnssec,
|
||||
};
|
||||
const transportData = {
|
||||
type: monitor.dnsTransport,
|
||||
timeout: monitor.timeout,
|
||||
ignoreCertErrors: monitor.ignoreTls,
|
||||
dohQueryPath: monitor.dohQueryPath,
|
||||
dohUsePost: monitor.method === "POST",
|
||||
|
@ -62,32 +67,37 @@ class DnsMonitorType extends MonitorType {
|
|||
const conditions = ConditionExpressionGroup.fromMonitor(monitor);
|
||||
let conditionsResult = true;
|
||||
const handleConditions = (data) => conditions ? evaluateExpressionGroup(conditions, data) : true;
|
||||
|
||||
const records = dnsRes.answers.reduce((results, record) => {
|
||||
const checkRecord = (results, record) => {
|
||||
// Omit records that are not the same as the requested rrtype
|
||||
if (record.type === monitor.dnsResolveType) {
|
||||
// Add the record to the array
|
||||
results.push(Buffer.isBuffer(record.data) ? record.data.toString() : record.data);
|
||||
}
|
||||
return results;
|
||||
}, []);
|
||||
};
|
||||
|
||||
const records = dnsRes.answers.reduce(checkRecord, []);
|
||||
// Return down status if no records are provided
|
||||
if (records.length === 0) {
|
||||
rrtype = null;
|
||||
dnsMessage = "No records found";
|
||||
conditionsResult = false;
|
||||
|
||||
if (dnsRes.authorities.map(auth => auth.type).includes(monitor.dnsResolveType)) {
|
||||
records.push(...dnsRes.authorities.reduce(checkRecord, []));
|
||||
} else {
|
||||
rrtype = null;
|
||||
dnsMessage = "No records found";
|
||||
conditionsResult = false;
|
||||
}
|
||||
}
|
||||
|
||||
switch (rrtype) {
|
||||
case "A":
|
||||
case "AAAA":
|
||||
case "TXT":
|
||||
case "PTR":
|
||||
case "NS":
|
||||
dnsMessage = records.join(" | ");
|
||||
conditionsResult = handleConditions({ records: records });
|
||||
break;
|
||||
|
||||
case "PTR":
|
||||
case "CNAME":
|
||||
dnsMessage = records[0];
|
||||
conditionsResult = handleConditions({ hostname: records[0].value });
|
||||
|
@ -134,7 +144,7 @@ class DnsMonitorType extends MonitorType {
|
|||
break;
|
||||
}
|
||||
|
||||
if (monitor.dnsLastResult !== dnsMessage && dnsMessage !== undefined) {
|
||||
if (monitor.dnsLastResult !== dnsMessage && dnsMessage !== undefined && monitor.id !== undefined) {
|
||||
await R.exec("UPDATE `monitor` SET dns_last_result = ? WHERE id = ? ", [ dnsMessage, monitor.id ]);
|
||||
}
|
||||
|
||||
|
|
|
@ -521,6 +521,8 @@ exports.dnsResolve = function (opts, resolverServer, resolverPort, transport) {
|
|||
client.on("error", (err) => {
|
||||
if (err.code === "ETIMEDOUT") {
|
||||
err.message = `Connection to ${socketName} timed out`;
|
||||
} else if (err.code === "ECONNREFUSED") {
|
||||
err.message = `Connection to ${socketName} refused`;
|
||||
}
|
||||
reject(err);
|
||||
});
|
||||
|
@ -638,7 +640,15 @@ exports.dnsResolve = function (opts, resolverServer, resolverPort, transport) {
|
|||
});
|
||||
const req = client.request(headers);
|
||||
req.on("error", (err) => {
|
||||
err.message = "HTTP/2: " + err.message;
|
||||
if (err.cause.code === "ETIMEDOUT") {
|
||||
err = err.cause;
|
||||
err.message = `Connection to ${socketName} timed out`;
|
||||
} else if (err.cause.code === "ECONNREFUSED") {
|
||||
err = err.cause;
|
||||
err.message = `Connection to ${socketName} refused`;
|
||||
} else {
|
||||
err.message = "HTTP/2: " + err.message;
|
||||
}
|
||||
reject(err);
|
||||
});
|
||||
req.on("response", (resHeaders) => {
|
||||
|
@ -670,6 +680,11 @@ exports.dnsResolve = function (opts, resolverServer, resolverPort, transport) {
|
|||
client.end();
|
||||
}
|
||||
client.on("error", (err) => {
|
||||
if (err.code === "ETIMEDOUT") {
|
||||
err.message = `Connection to ${socketName} timed out`;
|
||||
} else if (err.code === "ECONNREFUSED") {
|
||||
err.message = `Connection to ${socketName} refused`;
|
||||
}
|
||||
reject(err);
|
||||
});
|
||||
client.on("close", () => {
|
||||
|
|
|
@ -629,6 +629,10 @@ $shadow-box-padding: 20px;
|
|||
}
|
||||
}
|
||||
|
||||
#doh-method {
|
||||
width: 90px;
|
||||
}
|
||||
|
||||
@media (max-width: 770px) {
|
||||
.toast-container {
|
||||
margin-bottom: 100px !important;
|
||||
|
|
|
@ -371,38 +371,16 @@
|
|||
<template v-if="monitor.type === 'dns'">
|
||||
<div class="my-3">
|
||||
<label for="dns-resolve-server" class="form-label">{{ $t("Resolver Server") }}</label>
|
||||
<input id="dns-resolve-server" ref="dns-resolve-server" v-model="monitor.dnsResolveServer" type="text" class="form-control" :pattern="dnsResolverRegex" required>
|
||||
<input id="dns-resolve-server" ref="dns-resolve-server" v-model="monitor.dnsResolveServer" type="text" class="form-control" :pattern="dnsResolverRegex" required data-testid="resolve-server-input">
|
||||
<div class="form-text">
|
||||
{{ $t("resolverserverDescription") }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- TODO center selected option text -->
|
||||
<div class="my-3">
|
||||
<label for="dns-transport" class="form-label">{{ $t("Transport Method") }}</label>
|
||||
<VueMultiselect
|
||||
id="dns-transport"
|
||||
v-model="monitor.dnsTransport"
|
||||
:options="dnsTransportOptions"
|
||||
:multiple="false"
|
||||
:close-on-select="true"
|
||||
:clear-on-select="false"
|
||||
:preserve-search="false"
|
||||
:placeholder="$t('Select the transport method...')"
|
||||
:preselect-first="false"
|
||||
:max-height="500"
|
||||
:taggable="false"
|
||||
data-testid="resolve-type-select"
|
||||
></VueMultiselect>
|
||||
<div class="form-text">
|
||||
{{ $t("dnsTransportDescription") }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 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">
|
||||
<input id="port" v-model="monitor.port" type="number" class="form-control" required min="0" max="65535" step="1" data-testid="port-input">
|
||||
<div class="form-text">
|
||||
{{ $t("dnsPortDescription") }}
|
||||
</div>
|
||||
|
@ -412,7 +390,6 @@
|
|||
<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 -->
|
||||
<!-- TODO center selected option text -->
|
||||
<VueMultiselect
|
||||
id="dns-resolve-type"
|
||||
v-model="monitor.dnsResolveType"
|
||||
|
@ -432,6 +409,27 @@
|
|||
{{ $t("rrtypeDescription") }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="my-3">
|
||||
<label for="dns-transport" class="form-label">{{ $t("Transport Method") }}</label>
|
||||
<VueMultiselect
|
||||
id="dns-transport"
|
||||
v-model="monitor.dnsTransport"
|
||||
:options="dnsTransportOptions"
|
||||
:multiple="false"
|
||||
:close-on-select="true"
|
||||
:clear-on-select="false"
|
||||
:preserve-search="false"
|
||||
:placeholder="$t('Select the transport method...')"
|
||||
:preselect-first="false"
|
||||
:max-height="500"
|
||||
:taggable="false"
|
||||
data-testid="transport-method-select"
|
||||
></VueMultiselect>
|
||||
<div class="form-text">
|
||||
{{ $t("dnsTransportDescription") }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Docker Container Name / ID -->
|
||||
|
@ -673,11 +671,11 @@
|
|||
<div class="d-flex">
|
||||
<div class="my-3 flex-column flex-fill">
|
||||
<div>
|
||||
<label for="method" class="form-label">{{ $t("Method") }}</label>
|
||||
<label for="doh-method" class="form-label">{{ $t("Method") }}</label>
|
||||
</div>
|
||||
<div class="d-flex flex-row">
|
||||
<div class="d-inline-flex">
|
||||
<select id="method" v-model="monitor.method" class="form-select">
|
||||
<select id="doh-method" v-model="monitor.method" class="form-select" data-testid="method-select">
|
||||
<option value="GET">
|
||||
GET
|
||||
</option>
|
||||
|
@ -703,7 +701,7 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="my-3 form-check">
|
||||
<input id="force-http2" v-model="monitor.forceHttp2" class="form-check-input" type="checkbox">
|
||||
<input id="force-http2" v-model="monitor.forceHttp2" class="form-check-input" type="checkbox" data-testid="http2-check">
|
||||
<label class="form-check-label" for="force-http2">
|
||||
{{ $t("Force HTTP2") }}
|
||||
</label>
|
||||
|
@ -1516,19 +1514,18 @@ message HealthCheckResponse {
|
|||
},
|
||||
|
||||
conditionVariables() {
|
||||
// For DNS monitor, the variables depend on rrtype. Conditions for
|
||||
// all rrtypes are added to an array in the DnsMonitorType class in
|
||||
// order to pass to Vue, then identified based on index.
|
||||
if (this.monitor.type === "dns") {
|
||||
// When the monitor type is DNS, the conditions depend on
|
||||
// record type. Condition variables are all added to a single
|
||||
// array defined in server\monitor-types\dns.js in order to
|
||||
// pass to Vue, then sliced below based on index.
|
||||
const dnsConditionVariables = this.$root.monitorTypeList["dns"]?.conditionVariables;
|
||||
switch (this.monitor.dnsResolveType) {
|
||||
case "A":
|
||||
case "AAAA":
|
||||
case "TXT":
|
||||
case "PTR":
|
||||
case "NS":
|
||||
return dnsConditionVariables.slice(0, 1);
|
||||
case "PTR":
|
||||
case "CNAME":
|
||||
return dnsConditionVariables.slice(1, 2);
|
||||
case "CAA":
|
||||
|
|
181
test/backend-test/test-dns.js
Normal file
181
test/backend-test/test-dns.js
Normal file
|
@ -0,0 +1,181 @@
|
|||
const { describe, test } = require("node:test");
|
||||
const assert = require("node:assert");
|
||||
const { DnsMonitorType } = require("../../server/monitor-types/dns");
|
||||
const { UP, PENDING } = require("../../src/util");
|
||||
|
||||
const queryName = ".";
|
||||
const rrtype = "NS";
|
||||
const sampleRecordRegex = /a\.root-servers\.net/;
|
||||
const bogusServer = "0.0.0.0";
|
||||
|
||||
/**
|
||||
* Performs DNS query and checks the result
|
||||
* @param {object} monitorOpts the parameters for the monitor
|
||||
* @returns {Promise<Heartbeat>} the heartbeat produced by the check
|
||||
*/
|
||||
async function testDns(monitorOpts) {
|
||||
if (!monitorOpts.hostname) {
|
||||
monitorOpts.hostname = queryName;
|
||||
}
|
||||
if (!monitorOpts.dnsResolveType) {
|
||||
monitorOpts.dnsResolveType = rrtype;
|
||||
}
|
||||
if (!monitorOpts.conditions) {
|
||||
monitorOpts.conditions = "[]";
|
||||
}
|
||||
const heartbeat = {
|
||||
msg: "",
|
||||
status: PENDING,
|
||||
};
|
||||
const dnsMonitor = new DnsMonitorType();
|
||||
|
||||
await dnsMonitor.check(monitorOpts, heartbeat, {});
|
||||
|
||||
return heartbeat;
|
||||
}
|
||||
|
||||
describe("DNS monitor transport methods", {
|
||||
concurrency: true
|
||||
}, () => {
|
||||
test("DNS (UDP)", async () => {
|
||||
|
||||
const monitor = {
|
||||
dnsResolveServer: "1.1.1.1",
|
||||
port: 53,
|
||||
dnsTransport: "UDP",
|
||||
};
|
||||
|
||||
const heartbeat = await testDns(monitor);
|
||||
assert.strictEqual(heartbeat.status, UP);
|
||||
assert.match(heartbeat.msg, sampleRecordRegex);
|
||||
});
|
||||
|
||||
test("DNS (UDP) timeout", async () => {
|
||||
|
||||
const monitor = {
|
||||
dnsResolveServer: bogusServer,
|
||||
port: 53,
|
||||
dnsTransport: "UDP",
|
||||
timeout: 10000,
|
||||
};
|
||||
|
||||
await assert.rejects(testDns(monitor), {
|
||||
message: /Query to .* timed out/
|
||||
}, "Expected query timeout error");
|
||||
});
|
||||
|
||||
test("DNS (TCP)", async () => {
|
||||
|
||||
const monitor = {
|
||||
dnsResolveServer: "1.1.1.1",
|
||||
port: 53,
|
||||
dnsTransport: "TCP",
|
||||
};
|
||||
|
||||
const heartbeat = await testDns(monitor);
|
||||
assert.strictEqual(heartbeat.status, UP);
|
||||
assert.match(heartbeat.msg, sampleRecordRegex);
|
||||
});
|
||||
|
||||
test("DNS (TCP) timeout", async () => {
|
||||
|
||||
const monitor = {
|
||||
dnsResolveServer: bogusServer,
|
||||
port: 53,
|
||||
dnsTransport: "TCP",
|
||||
timeout: 10000,
|
||||
};
|
||||
|
||||
await assert.rejects(testDns(monitor), {
|
||||
message: /Connection to .* (timed out|refused)/
|
||||
}, "Expected connection timeout error");
|
||||
});
|
||||
|
||||
test("DNS over TLS", async () => {
|
||||
|
||||
const monitor = {
|
||||
dnsResolveServer: "one.one.one.one",
|
||||
port: 853,
|
||||
dnsTransport: "DoT",
|
||||
};
|
||||
|
||||
const heartbeat = await testDns(monitor);
|
||||
assert.strictEqual(heartbeat.status, UP);
|
||||
assert.match(heartbeat.msg, sampleRecordRegex);
|
||||
});
|
||||
|
||||
test("DNS over TLS timeout", async () => {
|
||||
|
||||
const monitor = {
|
||||
dnsResolveServer: bogusServer,
|
||||
port: 853,
|
||||
dnsTransport: "DoT",
|
||||
timeout: 10000,
|
||||
};
|
||||
|
||||
await assert.rejects(testDns(monitor), {
|
||||
message: /Connection to .* (timed out|refused)/
|
||||
}, "Expected connection timeout error");
|
||||
});
|
||||
|
||||
test("DNS over HTTPS (GET)", async () => {
|
||||
|
||||
const monitor = {
|
||||
dnsResolveServer: "cloudflare-dns.com",
|
||||
port: 443,
|
||||
dnsTransport: "DoH",
|
||||
dohQueryPath: "dns-query",
|
||||
method: "GET",
|
||||
};
|
||||
|
||||
const heartbeat = await testDns(monitor);
|
||||
assert.strictEqual(heartbeat.status, UP);
|
||||
assert.match(heartbeat.msg, sampleRecordRegex);
|
||||
});
|
||||
|
||||
test("DNS over HTTPS timeout", async () => {
|
||||
|
||||
const monitor = {
|
||||
dnsResolveServer: bogusServer,
|
||||
port: 443,
|
||||
dnsTransport: "DoH",
|
||||
timeout: 10000,
|
||||
};
|
||||
|
||||
await assert.rejects(testDns(monitor), {
|
||||
message: /Connection to .* (timed out|refused)/
|
||||
}, "Expected connection timeout error");
|
||||
});
|
||||
|
||||
test("DNS over HTTP/2 (POST)", async () => {
|
||||
|
||||
const monitor = {
|
||||
dnsResolveServer: "cloudflare-dns.com",
|
||||
port: 443,
|
||||
dnsTransport: "DoH",
|
||||
dohQueryPath: "dns-query",
|
||||
method: "POST",
|
||||
forceHttp2: true,
|
||||
};
|
||||
|
||||
const heartbeat = await testDns(monitor);
|
||||
assert.strictEqual(heartbeat.status, UP);
|
||||
assert.match(heartbeat.msg, sampleRecordRegex);
|
||||
});
|
||||
|
||||
test("DNS over HTTP/2 timeout", async () => {
|
||||
|
||||
const monitor = {
|
||||
dnsResolveServer: bogusServer,
|
||||
port: 443,
|
||||
dnsTransport: "DoH",
|
||||
timeout: 10000,
|
||||
forceHttp2: true,
|
||||
};
|
||||
|
||||
await assert.rejects(testDns(monitor), {
|
||||
message: /Connection to .* (timed out|refused)/
|
||||
}, "Expected connection timeout error");
|
||||
});
|
||||
|
||||
});
|
|
@ -53,7 +53,7 @@ test.describe("Monitor Form", () => {
|
|||
|
||||
const friendlyName = "Example DNS NS";
|
||||
await page.getByTestId("friendly-name-input").fill(friendlyName);
|
||||
await page.getByTestId("hostname-input").fill("example.com");
|
||||
await page.getByTestId("hostname-input").fill(".");
|
||||
|
||||
const resolveTypeSelect = page.getByTestId("resolve-type-select");
|
||||
await resolveTypeSelect.click();
|
||||
|
@ -65,9 +65,9 @@ test.describe("Monitor Form", () => {
|
|||
await page.getByTestId("add-condition-button").click();
|
||||
expect(await page.getByTestId("condition").count()).toEqual(2); // 2 explicitly added
|
||||
|
||||
await page.getByTestId("condition-value").nth(0).fill("a.iana-servers.net");
|
||||
await page.getByTestId("condition-value").nth(0).fill("a.root-servers.net");
|
||||
await page.getByTestId("condition-and-or").nth(0).selectOption("or");
|
||||
await page.getByTestId("condition-value").nth(1).fill("b.iana-servers.net");
|
||||
await page.getByTestId("condition-value").nth(1).fill("b.root-servers.net");
|
||||
|
||||
await screenshot(testInfo, page);
|
||||
await page.getByTestId("save-button").click();
|
||||
|
@ -86,7 +86,7 @@ test.describe("Monitor Form", () => {
|
|||
|
||||
const friendlyName = "Example DNS NS";
|
||||
await page.getByTestId("friendly-name-input").fill(friendlyName);
|
||||
await page.getByTestId("hostname-input").fill("example.com");
|
||||
await page.getByTestId("hostname-input").fill(".");
|
||||
|
||||
const resolveTypeSelect = page.getByTestId("resolve-type-select");
|
||||
await resolveTypeSelect.click();
|
||||
|
@ -105,4 +105,38 @@ test.describe("Monitor Form", () => {
|
|||
|
||||
await screenshot(testInfo, page);
|
||||
});
|
||||
|
||||
test("dns transport", async ({ page }, testInfo) => {
|
||||
await page.goto("./add");
|
||||
await login(page);
|
||||
await screenshot(testInfo, page);
|
||||
await selectMonitorType(page);
|
||||
|
||||
const friendlyName = "Cloudflare";
|
||||
await page.getByTestId("friendly-name-input").fill(friendlyName);
|
||||
await page.getByTestId("hostname-input").fill("one.one.one.one");
|
||||
await page.getByTestId("resolve-server-input").fill("cloudflare-dns.com");
|
||||
await page.getByTestId("port-input").fill("443");
|
||||
|
||||
const resolveTypeSelect = page.getByTestId("resolve-type-select");
|
||||
await resolveTypeSelect.click();
|
||||
await resolveTypeSelect.getByRole("option", { name: "SOA" }).click();
|
||||
|
||||
const transportMethodSelect = page.getByTestId("transport-method-select");
|
||||
await transportMethodSelect.click();
|
||||
await transportMethodSelect.getByRole("option", { name: "DoH" }).click();
|
||||
|
||||
const httpMethodSelect = page.getByTestId("method-select");
|
||||
await httpMethodSelect.selectOption("POST");
|
||||
|
||||
await page.getByTestId("http2-check").check();
|
||||
|
||||
await screenshot(testInfo, page);
|
||||
await page.getByTestId("save-button").click();
|
||||
await page.waitForURL("/dashboard/*");
|
||||
|
||||
expect(page.getByTestId("monitor-status")).toHaveText("up", { ignoreCase: true });
|
||||
|
||||
await screenshot(testInfo, page);
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Add table
Reference in a new issue