HTTP/2 support, error handling, rename variables, and various bugfixes

This commit is contained in:
ekrekeler 2025-06-15 16:39:21 -05:00
parent 3ed4a2a2cb
commit 47cf7f9d13
No known key found for this signature in database
GPG key ID: 4C66C864B6B00854
11 changed files with 1436 additions and 1181 deletions

View file

@ -87,8 +87,9 @@ async function createTables() {
table.string("dns_resolve_type", 5);
table.string("dns_resolve_server", 255);
table.string("dns_transport", 3);
table.string("doh_query_path", 255);
table.boolean("skip_remote_dnssec").defaultTo(false);
table.boolean("doh_query_path", 255).defaultTo("dns-query");
table.boolean("force_http2").notNullable().defaultTo(false);
table.boolean("skip_remote_dnssec").notNullable().defaultTo(false);
table.string("dns_last_result", 255);
table.integer("retry_interval").notNullable().defaultTo(0);
table.string("push_token", 20).defaultTo(null);

View file

@ -1,9 +1,10 @@
exports.up = function (knex) {
return knex.schema
.alterTable("monitor", function (table) {
table.string("dns_transport").notNullable().defaultTo("UDP");
table.string("doh_query_path");
table.boolean("skip_remote_dnssec").defaultTo(false);
table.string("dns_transport", 3).notNullable().defaultTo("UDP");
table.string("doh_query_path", 255).defaultTo("dns-query");
table.boolean("force_http2").notNullable().defaultTo(false);
table.boolean("skip_remote_dnssec").notNullable().defaultTo(false);
});
};
@ -11,6 +12,7 @@ exports.down = function (knex) {
return knex.schema.alterTable("monitor", function (table) {
table.dropColumn("dns_transport");
table.dropColumn("doh_query_path");
table.dropColumn("force_http2");
table.dropColumn("skip_remote_dnssec");
});
};

View file

@ -116,12 +116,13 @@ class Monitor extends BeanModel {
packetSize: this.packetSize,
maxredirects: this.maxredirects,
accepted_statuscodes: this.getAcceptedStatuscodes(),
dns_resolve_type: this.dns_resolve_type,
dns_resolve_server: this.dns_resolve_server,
dns_transport: this.dns_transport,
doh_query_path: this.doh_query_path,
skip_remote_dnssec: this.skip_remote_dnssec,
dns_last_result: this.dns_last_result,
dnsResolveType: this.dnsResolveType,
dnsResolveServer: this.dnsResolveServer,
dnsTransport: this.dnsTransport,
dohQueryPath: this.dohQueryPath,
forceHttp2: Boolean(this.forceHttp2),
skipRemoteDnssec: Boolean(this.skipRemoteDnssec),
dnsLastResult: this.dnsLastResult,
docker_container: this.docker_container,
docker_host: this.docker_host,
proxyId: this.proxy_id,

View file

@ -39,26 +39,46 @@ class DnsMonitorType extends MonitorType {
* @inheritdoc
*/
async check(monitor, heartbeat, _server) {
let startTime = dayjs().valueOf();
let dnsMessage = "";
const requestData = {
name: monitor.hostname,
rrtype: monitor.dns_resolve_type,
rrtype: monitor.dnsResolveType,
dnssec: true, // Request DNSSEC information in the response
dnssecCheckingDisabled: monitor.skip_remote_dnssec,
};
let dnsRes = await dnsResolve(requestData, monitor.dns_resolve_server, monitor.port, monitor.dns_transport, monitor.doh_query_path);
const records = dnsRes.answers.map(record => {
return Buffer.isBuffer(record.data) ? record.data.toString() : record.data;
});
const transportData = {
type: monitor.dnsTransport,
ignoreCertErrors: monitor.ignoreTls,
dohQueryPath: monitor.dohQueryPath,
dohUsePost: monitor.method === "POST",
dohUseHttp2: monitor.forceHttp2,
};
let startTime = dayjs().valueOf();
let dnsRes = await dnsResolve(requestData, monitor.dnsResolveServer, monitor.port, transportData);
heartbeat.ping = dayjs().valueOf() - startTime;
let dnsMessage = "";
let rrtype = monitor.dnsResolveType;
const conditions = ConditionExpressionGroup.fromMonitor(monitor);
let conditionsResult = true;
const handleConditions = (data) => conditions ? evaluateExpressionGroup(conditions, data) : true;
switch (monitor.dns_resolve_type) {
const records = dnsRes.answers.reduce((results, record) => {
// Omit records that are not the same as the requested rrtype
if (record.type === monitor.dnsResolveType) {
results.push(Buffer.isBuffer(record.data) ? record.data.toString() : record.data);
}
return results;
}, []);
// Return down status if no records are provided
if (records.length === 0) {
rrtype = null;
dnsMessage = "No records found";
conditionsResult = false;
}
switch (rrtype) {
case "A":
case "AAAA":
case "TXT":
@ -114,7 +134,7 @@ class DnsMonitorType extends MonitorType {
break;
}
if (monitor.dns_last_result !== dnsMessage && dnsMessage !== undefined) {
if (monitor.dnsLastResult !== dnsMessage && dnsMessage !== undefined) {
await R.exec("UPDATE `monitor` SET dns_last_result = ? WHERE id = ? ", [ dnsMessage, monitor.id ]);
}

View file

@ -824,11 +824,12 @@ let needSetup = false;
bean.packetSize = monitor.packetSize;
bean.maxredirects = monitor.maxredirects;
bean.accepted_statuscodes_json = JSON.stringify(monitor.accepted_statuscodes);
bean.dns_resolve_type = monitor.dns_resolve_type;
bean.dns_resolve_server = monitor.dns_resolve_server;
bean.dns_transport = monitor.dns_transport;
bean.doh_query_path = monitor.doh_query_path;
bean.skip_remote_dnssec = monitor.skip_remote_dnssec;
bean.dnsResolveType = monitor.dnsResolveType;
bean.dnsResolveServer = monitor.dnsResolveServer;
bean.dnsTransport = monitor.dnsTransport;
bean.dohQueryPath = monitor.dohQueryPath;
bean.forceHttp2 = monitor.forceHttp2;
bean.skipRemoteDnssec = monitor.skipRemoteDnssec;
bean.pushToken = monitor.pushToken;
bean.docker_container = monitor.docker_container;
bean.docker_host = monitor.docker_host;

View file

@ -24,8 +24,19 @@ const redis = require("redis");
const oidc = require("openid-client");
const tls = require("tls");
const https = require("https");
const http2 = require("http2");
const url = require("url");
const {
HTTP2_HEADER_PATH,
HTTP2_HEADER_METHOD,
HTTP2_HEADER_AUTHORITY,
HTTP2_HEADER_ACCEPT,
HTTP2_HEADER_CONTENT_LENGTH,
HTTP2_HEADER_CONTENT_TYPE,
HTTP2_HEADER_STATUS,
} = http2.constants;
const {
dictionaries: {
rfc2865: { file, attributes },
@ -287,25 +298,24 @@ exports.httpNtlm = function (options, ntlmOptions) {
};
/**
* Adapted from https://github.com/hildjj/dohdec/blob/v7.0.2/pkg/dohdec/lib/dnsUtils.js
* Encode a DNS query packet to a buffer.
* Encode DNS query packet data to a buffer. Adapted from
* https://github.com/hildjj/dohdec/blob/v7.0.2/pkg/dohdec/lib/dnsUtils.js
* @param {object} opts Options for the query.
* @param {string} opts.name The name to look up.
* @param {number} [opts.id=0] ID for the query. SHOULD be 0 for DOH.
* @param {packet.RecordType} [opts.rrtype="A"] The record type to look up.
* @param {boolean} [opts.dnssec=false] Request DNSSec information?
* @param {boolean} [opts.dnssecCheckingDisabled=false] Disable DNSSec
* validation?
* @param {string} [opts.ecsSubnet] Subnet to use for ECS.
* @param {number} [opts.ecs] Number of ECS bits. Defaults to 24 or 56
* (IPv4/IPv6).
* @param {boolean} [opts.stream=false] Encode for streaming, with the packet
* prefixed by a 2-byte big-endian integer of the number of bytes in the
* packet.
* @param {number} opts.id ID for the query. SHOULD be 0 for DOH.
* @param {packet.RecordType} opts.rrtype The record type to look up.
* @param {boolean} opts.dnssec Request DNSSec information?
* @param {boolean} opts.dnssecCheckingDisabled Disable DNSSec validation?
* @param {string} opts.ecsSubnet Subnet to use for ECS.
* @param {number} opts.ecs Number of ECS bits. Defaults to 24 (IPv4) or 56
* (IPv6).
* @param {boolean} opts.stream Encode for streaming, with the packet prefixed
* by a 2-byte big-endian integer of the number of bytes in the packet.
* @param {number} opts.udpPayloadSize Set a custom UDP payload size (EDNS).
* @returns {Buffer} The encoded packet.
* @throws {TypeError} opts does not contain a name attribute.
*/
exports.makePacket = function (opts) {
exports.makeDnsPacket = function (opts) {
const PAD_SIZE = 128;
if (!opts?.name) {
@ -316,7 +326,7 @@ exports.makePacket = function (opts) {
const opt = {
name: ".",
type: "OPT",
udpPayloadSize: 4096,
udpPayloadSize: opts.udpPayloadSize || 4096,
extendedRcode: 0,
flags: 0,
flag_do: false, // Setting here has no effect
@ -369,16 +379,65 @@ exports.makePacket = function (opts) {
return dnsPacket.encode(dns);
};
/**
* Decodes DNS packet response data with error handling
* @param {Buffer} data DNS packet data to decode
* @param {boolean} isStream If the data is encoded as a stream
* @param {Function} callback function to call if error is encountered
* Passes error object as a parameter to the function
* @returns {dnsPacket.Packet} DNS packet data in an object
*/
exports.decodeDnsPacket = function (data, isStream = false, callback) {
let decodedData;
try {
decodedData = isStream ? dnsPacket.streamDecode(data) : dnsPacket.decode(data);
log.debug("dns", "Response decoded");
// If the truncated bit is set, the answers section was too large
if (decodedData.flag_tc) {
callback({ message: "Response is truncated." });
}
} catch (err) {
err.message = `Error decoding DNS response data: ${err.message}`;
log.warn("dns", err.message);
if (callback) {
callback(err);
}
}
return decodedData;
};
/**
* Resolves a given record using the specified DNS server
* @param {string} opts Options for the query
* @param {object} opts Options for the query, used to generate DNS packet
* @param {string} opts.name The name of the record to query
* @param {string} opts.rrtype The resource record type
* @param {number} opts.id Set a specific ID number to use on the query
* @param {number} opts.udpPayloadSize Set a custom UDP payload size (EDNS).
* Defaults to safe values, 1432 bytes (IPv4) or 1232 bytes (IPv6).
* @param {string} resolverServer The DNS server to use
* @param {string} resolverPort Port the DNS server is listening on
* @param {string} transport The transport method to use
* @param {string} dohQuery The query path used only for DoH
* @param {number} resolverPort Port the DNS server is listening on
* @param {object} transport The transport method and options
* @param {string} transport.type Transport method, default is UDP
* @param {number} transport.timeout Timeout to use for queries
* @param {boolean} transport.ignoreCertErrors Proceed with secure connections
* even if the server presents an untrusted or expired certificate
* @param {string} transport.dohQueryPath Query path to use for DoH requests
* @param {boolean} transport.dohUsePost If true, DNS query will be sent using
* HTTP POST method for DoH requests, otherwise use HTTP GET method
* @param {boolean} transport.dohUseHttp2 If true, DNS query will be made with
* HTTP/2 session for DOH requests, otherwise use HTTP/1.1
* @returns {Promise<(string[] | object[] | object)>} DNS response
*/
exports.dnsResolve = function (opts, resolverServer, resolverPort, transport, dohQuery) {
exports.dnsResolve = function (opts, resolverServer, resolverPort, transport) {
// Set transport variables to defaults if not defined
const method = ("type" in transport) ? transport.type.toUpperCase() : "UDP";
const isSecure = [ "DOH", "DOT", "DOQ" ].includes(method);
const timeout = transport.timeout ?? 30000; // 30 seconds
const skipCertCheck = transport.ignoreCertErrors ?? false;
const dohQuery = transport.dohQueryPath ?? "dns-query";
const dohUsePost = transport.dohUsePost ?? false;
const dohUseHttp2 = transport.dohUseHttp2 ?? false;
// Parse IPv4 and IPv6 addresses to determine address family and
// add square brackets to IPv6 addresses, following RFC 3986 syntax
resolverServer = resolverServer.replace("[", "").replace("]", "");
@ -404,29 +463,42 @@ exports.dnsResolve = function (opts, resolverServer, resolverPort, transport, do
if (opts.id == null) {
// Set query ID to "0" for HTTP cache friendlyness on DoH requests.
// See https://github.com/mafintosh/dns-packet/issues/77
opts.id = (transport.toUpperCase() === "DOH") ? 0 : Math.floor(Math.random() * 65534) + 1;
opts.id = (method === "DOH") ? 0 : Math.floor(Math.random() * 65534) + 1;
}
// Set UDP payload size to safe levels for transmission over 1500 MTU
if (!opts.udpPayloadSize) {
opts.udpPayloadSize = (addressFamily === 4) ? 1432 : 1232;
}
// Enable stream encoding for TCP and DOT transport methods
if ([ "TCP", "DOT" ].includes(method)) {
opts.stream = true;
}
// Generate buffer with encoded DNS query
const buf = exports.makeDnsPacket(opts);
let client;
let resolver = null;
let resolver;
// Transport method determines which client type to use
const isSecure = [ "DOH", "DOT" ].includes(transport.toUpperCase());
switch (transport.toUpperCase()) {
switch (method) {
case "TCP":
case "DOT": {
opts.stream = true;
const buf = exports.makePacket(opts);
if (isSecure) {
const options = {
port: resolverPort,
host: resolverServer,
// TODO: Option for relaxing certificate validation
rejectUnauthorized: !skipCertCheck,
secureContext: tls.createSecureContext({
secureProtocol: "TLSv1_2_method",
minVersion: "TLSv1.2",
}),
};
// TODO: Error handling for untrusted or expired cert
// Set TLS ServerName only if server is not an IP address per
// Section 3 of RFC 6066
if (addressFamily === 0) {
options.servername = resolverServer;
}
client = tls.connect(options, () => {
log.debug("dns", `Connected to ${resolverServer}:${resolverPort}`);
client.write(buf);
@ -438,99 +510,201 @@ exports.dnsResolve = function (opts, resolverServer, resolverPort, transport, do
client.write(buf);
});
}
client.on("close", () => {
log.debug("dns", "Connection closed");
});
resolver = new Promise((resolve, reject) => {
// The below message is used when the response received does
// not follow Section 4.2.2 of RFC 1035
const lenErrMsg = "Resolver returned invalid DNS response";
let data = Buffer.alloc(0);
let expectedLength = 0;
let isValidLength = false;
client.on("error", (err) => {
reject(err);
});
client.setTimeout(timeout, () => {
client.destroy();
reject({ message: "Connection timed out" });
});
client.on("data", (chunk) => {
if (data.length === 0) {
if (chunk.byteLength > 1) {
const plen = chunk.readUInt16BE(0);
expectedLength = plen;
if (plen < 12) {
reject("Response received is below DNS minimum packet length");
expectedLength = chunk.readUInt16BE(0);
if (expectedLength < 12) {
reject({ message: lenErrMsg });
}
}
}
data = Buffer.concat([ data, chunk ]);
if (data.byteLength >= expectedLength) {
if (data.byteLength - 2 === expectedLength) {
isValidLength = true;
client.destroy();
const response = dnsPacket.streamDecode(data);
log.debug("dns", "Response decoded");
const response = exports.decodeDnsPacket(data, true, reject);
resolve(response);
}
});
client.on("close", () => {
log.debug("dns", "Connection closed");
if (!isValidLength) {
reject({ message: lenErrMsg });
}
});
});
break;
}
case "DOH": {
const buf = exports.makePacket(opts);
// TODO: implement POST requests for wireformat and JSON
dohQuery = dohQuery || "dns-query?dns={query}";
dohQuery = dohQuery.replace("{query}", buf.toString("base64url"));
const requestURL = url.parse(`https://${resolverServer}:${resolverPort}/${dohQuery}`, true);
const queryPath = dohUsePost ? dohQuery : `${dohQuery}?dns=${buf.toString("base64url")}`;
const requestURL = url.parse(`https://${resolverServer}:${resolverPort}/${queryPath}`, true);
const mimeType = "application/dns-message";
const options = {
hostname: requestURL.hostname,
port: requestURL.port,
path: requestURL.path,
method: "GET",
headers: {
"Content-Type": "application/dns-message",
"accept": mimeType,
},
// TODO: Option for relaxing certificate validation
rejectUnauthorized: !skipCertCheck,
};
if (dohUsePost) {
options.method = "POST";
// Setting Content-Length header is required for some resolvers
options.headers["content-length"] = buf.byteLength;
options.headers["content-type"] = mimeType;
}
resolver = new Promise((resolve, reject) => {
client = https.request(options, (response) => {
/**
* Helper function to validate HTTP response
* @param {IncomingMessage|ClientHttp2Stream} httpResponse
* The response from https or http2 client
* @param {object} http2Headers Response headers from http2
* @returns {void}
* @throws missing one or more headers for HTTP/2 response
*/
const handleResponse = (httpResponse, http2Headers) => {
// Determine status code and content type
let statusCode;
let contentType;
if (dohUseHttp2) {
if (!http2Headers) {
throw new Error("No headers passed for HTTP/2 response");
}
statusCode = http2Headers[HTTP2_HEADER_STATUS];
contentType = http2Headers[HTTP2_HEADER_CONTENT_TYPE];
} else {
statusCode = httpResponse.statusCode;
contentType = httpResponse.headers["content-type"];
}
// Validate response from resolver
if (statusCode !== 200) {
reject({ message: `Request failed with status code ${statusCode}` });
return;
} else if (contentType !== mimeType) {
reject({ message: `Content-Type was "${contentType}", expected ${mimeType}` });
return;
}
// Read the response body into a buffer
let data = Buffer.alloc(0);
response.on("data", (chunk) => {
httpResponse.on("data", (chunk) => {
data = Buffer.concat([ data, chunk ]);
});
response.on("end", () => {
const response = dnsPacket.decode(data);
log.debug("dns", "Response decoded");
httpResponse.on("end", () => {
const response = exports.decodeDnsPacket(data, false, reject);
resolve(response);
});
});
client.on("socket", (socket) => {
socket.on("secureConnect", () => {
};
if (dohUseHttp2) {
const headers = {};
headers[HTTP2_HEADER_AUTHORITY] = options.hostname;
headers[HTTP2_HEADER_PATH] = options.path;
headers[HTTP2_HEADER_METHOD] = options.method;
headers[HTTP2_HEADER_ACCEPT] = options.headers["accept"];
if (dohUsePost) {
headers[HTTP2_HEADER_CONTENT_LENGTH] = options.headers["content-length"];
headers[HTTP2_HEADER_CONTENT_TYPE] = options.headers["content-type"];
}
client = http2.connect(`https://${options.hostname}:${options.port}`, {
rejectUnauthorized: options.rejectUnauthorized,
});
client.setTimeout(timeout, () => {
client.destroy();
reject({ message: "Request timed out" });
});
client.on("connect", () => {
log.debug("dns", `Connected to ${resolverServer}:${resolverPort}`);
});
});
const req = client.request(headers);
req.on("error", (err) => {
err.message = "HTTP/2: " + err.message;
reject(err);
});
req.on("response", (resHeaders) => {
handleResponse(req, resHeaders);
});
req.on("end", () => {
client.close();
});
if (dohUsePost) {
req.write(buf);
}
req.end();
} else {
client = https.request(options, (httpResponse) => {
handleResponse(httpResponse);
});
client.setTimeout(timeout, () => {
client.destroy();
reject({ message: "Request timed out" });
});
client.on("socket", (socket) => {
socket.on("secureConnect", () => {
log.debug("dns", `Connected to ${resolverServer}:${resolverPort}`);
});
});
if (dohUsePost) {
client.write(buf);
}
client.end();
}
client.on("error", (err) => {
reject(err);
});
client.on("close", () => {
log.debug("dns", "Connection closed");
});
client.write(buf);
client.end();
});
break;
}
//case "UDP":
case "UDP":
default: {
const buf = exports.makePacket(opts);
client = dgram.createSocket("udp" + String(addressFamily));
client.on("connect", () => {
log.debug("dns", `Connected to ${resolverServer}:${resolverPort}`);
});
client.on("close", () => {
log.debug("dns", "Connection closed");
});
if (addressFamily === 0) {
return new Promise((resolve, reject) => {
reject({ message: "Resolver server must be IP address for UDP transport method" });
});
}
client = dgram.createSocket(`udp${addressFamily}`);
resolver = new Promise((resolve, reject) => {
let timer;
client.on("message", (rdata, rinfo) => {
client.close();
const response = dnsPacket.decode(rdata);
log.debug("dns", "Response decoded");
const response = exports.decodeDnsPacket(rdata, false, reject);
resolve(response);
});
client.on("error", (err) => {
clearTimeout(timer);
reject(err);
});
client.on("listening", () => {
log.debug("dns", `Connected to ${resolverServer}:${resolverPort}`);
timer = setTimeout(() => {
reject({ message: "Query timed out" });
client.close();
}, timeout);
});
client.on("close", () => {
clearTimeout(timer);
log.debug("dns", "Connection closed");
});
});
client.send(buf, 0, buf.length, resolverPort, resolverServer);
}

View file

@ -48,6 +48,7 @@
.multiselect__single {
line-height: 14px;
margin-bottom: 0;
vertical-align: middle;
}
.dark {

File diff suppressed because it is too large Load diff

View file

@ -31,9 +31,9 @@
<br>
<span>{{ $t("Expected Value") }}:</span> <span class="keyword">{{ monitor.expectedValue }}</span>
</span>
<span v-if="monitor.type === 'dns'">[{{ monitor.dns_resolve_type }}] {{ monitor.hostname }}
<span v-if="monitor.type === 'dns'">[{{ monitor.dnsResolveType }}] {{ monitor.hostname }}
<br>
<span>{{ $t("Last Result") }}:</span> <span class="keyword">{{ monitor.dns_last_result }}</span>
<span>{{ $t("Last Result") }}:</span> <span class="keyword">{{ monitor.dnsLastResult }}</span>
</span>
<span v-if="monitor.type === 'docker'">Docker container: {{ monitor.docker_container }}</span>
<span v-if="monitor.type === 'gamedig'">Gamedig - {{ monitor.game }}: {{ monitor.hostname }}:{{ monitor.port }}</span>

View file

@ -370,18 +370,19 @@
<!-- For DNS Type -->
<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" v-model="monitor.dns_resolve_server" type="text" class="form-control" :pattern="dnsResolverRegex" required>
<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>
<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>
<label for="dns-transport" class="form-label">{{ $t("Transport Method") }}</label>
<VueMultiselect
id="dns_transport"
v-model="monitor.dns_transport"
id="dns-transport"
v-model="monitor.dnsTransport"
:options="dnsTransportOptions"
:multiple="false"
:close-on-select="true"
@ -408,12 +409,13 @@
</div>
<div class="my-3">
<label for="dns_resolve_type" class="form-label">{{ $t("Resource Record Type") }}</label>
<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.dns_resolve_type"
id="dns-resolve-type"
v-model="monitor.dnsResolveType"
:options="dnsresolvetypeOptions"
:multiple="false"
:close-on-select="true"
@ -646,10 +648,10 @@
</div>
</div>
<div v-if="monitor.type === 'http' || monitor.type === 'keyword' || monitor.type === 'json-query' || monitor.type === 'redis' " class="my-3 form-check">
<div v-if="monitor.type === 'http' || monitor.type === 'keyword' || monitor.type === 'json-query' || monitor.type === 'redis' || (monitor.type === 'dns' && isSecureDnsTransport)" class="my-3 form-check">
<input id="ignore-tls" v-model="monitor.ignoreTls" class="form-check-input" type="checkbox" value="">
<label class="form-check-label" for="ignore-tls">
{{ monitor.type === "redis" ? $t("ignoreTLSErrorGeneral") : $t("ignoreTLSError") }}
{{ monitor.type === "redis" || monitor.type === "dns" ? $t("ignoreTLSErrorGeneral") : $t("ignoreTLSError") }}
</label>
</div>
@ -668,18 +670,51 @@
<!-- Advanced DNS monitor settings -->
<div v-if="monitor.type === 'dns'" class="my-3">
<div v-if="dohSelected">
<label for="doh_query_path" class="form-label">{{ $t("Query Path") }}</label>
<div class="d-flex">
<label for="doh_query_path" class="px-2 fs-5">/</label>
<input id="doh_query_path" v-model="monitor.doh_query_path" type="text" class="form-control" :pattern="urlQueryRegex" placeholder="dns-query?dns={query}">
<div class="my-3 flex-column flex-fill">
<div>
<label for="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">
<option value="GET">
GET
</option>
<option value="POST">
POST
</option>
</select>
</div>
<div class="mx-3 flex-fill">
<input :value="dohDisplayUrl" type="button" class="form-control text-center" @click="focusElement('dns-resolve-server')">
</div>
</div>
<div class="form-text">
{{ $t("dohHttpMethodDescription") }}
</div>
</div>
<div class="my-3 flex-column flex-fill">
<label for="doh-query-path" class="form-label">{{ $t("Query Path") }}</label>
<input id="doh-query-path" v-model="monitor.dohQueryPath" type="text" class="form-control" :pattern="urlQueryRegex" placeholder="dns-query">
<div class="form-text">
{{ $t("dohQueryPathDescription") }}
</div>
</div>
</div>
<div class="form-text">
{{ $t("dohQueryPathDescription") + ' "{query}".' }}
<div class="my-3 form-check">
<input id="force-http2" v-model="monitor.forceHttp2" class="form-check-input" type="checkbox">
<label class="form-check-label" for="force-http2">
{{ $t("Force HTTP2") }}
</label>
<div class="form-text">
{{ $t("forceHttp2") }}
</div>
</div>
</div>
<div class="form-check">
<input id="skip_remote_dnssec" v-model="monitor.skip_remote_dnssec" class="form-check-input" type="checkbox" value="">
<div class="my-3 form-check">
<input id="skip_remote_dnssec" v-model="monitor.skip_remote_dnssec" class="form-check-input" type="checkbox">
<label class="form-check-label" for="skip_remote_dnssec">
{{ $t("Skip Remote DNSSEC Verification") }}
</label>
@ -1135,9 +1170,11 @@ const monitorDefaults = {
expiryNotification: false,
maxredirects: 10,
accepted_statuscodes: [ "200-299" ],
dns_resolve_type: "A",
dns_resolve_server: "1.1.1.1",
dns_transport: "UDP",
dnsResolveType: "A",
dnsResolveServer: "1.1.1.1",
dnsTransport: "UDP",
dohQueryPath: "dns-query",
forceHttp2: false,
skip_remote_dnssec: false,
docker_container: "",
docker_host: null,
@ -1235,7 +1272,7 @@ export default {
// Permit IP address for TCP/UDP resolvers, hostname for DoH/DoT
if (! isDev) {
switch (this.monitor.dns_transport) {
switch (this.monitor.dnsTransport) {
case "UDP":
case "TCP":
return this.ipRegexPattern.source;
@ -1484,7 +1521,7 @@ message HealthCheckResponse {
// 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.dns_resolve_type) {
switch (this.monitor.dnsResolveType) {
case "A":
case "AAAA":
case "TXT":
@ -1508,8 +1545,18 @@ message HealthCheckResponse {
},
dohSelected() {
return this.monitor.dns_transport === "DoH";
}
return this.monitor.dnsTransport === "DoH";
},
dohDisplayUrl() {
const port = (this.monitor.port !== 443) ? `:${this.monitor.port}` : "";
return `https://${this.monitor.dnsResolveServer}${port}/`;
},
isSecureDnsTransport() {
return [ "DoH", "DoT", "DoQ" ].includes(this.monitor.dnsTransport);
},
},
watch: {
"$root.proxyList"() {
@ -1977,6 +2024,11 @@ message HealthCheckResponse {
}
},
focusElement(refId) {
// Focus the element that has a defined reference
this.$refs[refId].focus();
},
},
};
</script>

View file

@ -165,7 +165,7 @@ export function dnsNameRegexPattern() {
* @param {boolean} qstr whether or not the url follows query string format
* @returns {RegExp} The requested regex
*/
export function urlPathRegexPattern(qstr = false) {
export function urlPathRegexPattern(qstr = true) {
// Ensures a URL path follows query string format
const queryStringRegexPattern = /^\/?(([a-zA-Z0-9\-_%])+\/)*[a-zA-Z0-9\-_%]*\?([a-zA-Z0-9\-_%]+=[a-zA-Z0-9\-_%]*&?)+$/;
// Only checks for valid URL path containing "{query}"