mirror of
https://github.com/louislam/uptime-kuma.git
synced 2025-07-18 23:34:04 +02:00
HTTP/2 support, error handling, rename variables, and various bugfixes
This commit is contained in:
parent
3ed4a2a2cb
commit
47cf7f9d13
11 changed files with 1436 additions and 1181 deletions
|
@ -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);
|
||||
|
|
|
@ -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");
|
||||
});
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 ]);
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
};
|
||||
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);
|
||||
}
|
||||
|
|
|
@ -48,6 +48,7 @@
|
|||
.multiselect__single {
|
||||
line-height: 14px;
|
||||
margin-bottom: 0;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.dark {
|
||||
|
|
|
@ -84,7 +84,7 @@
|
|||
"retriesDescription": "Maximum retries before the service is marked as down and a notification is sent",
|
||||
"ignoredTLSError": "TLS/SSL errors have been ignored",
|
||||
"ignoreTLSError": "Ignore TLS/SSL errors for HTTPS websites",
|
||||
"ignoreTLSErrorGeneral": "Ignore TLS/SSL error for connection",
|
||||
"ignoreTLSErrorGeneral": "Ignore TLS/SSL errors for secure connections",
|
||||
"upsideDownModeDescription": "Flip the status upside down. If the service is reachable, it is DOWN.",
|
||||
"maxRedirectDescription": "Maximum number of redirects to follow. Set to 0 to disable redirects.",
|
||||
"Upside Down Mode": "Upside Down Mode",
|
||||
|
@ -145,6 +145,7 @@
|
|||
"Resolver Server": "Resolver Server",
|
||||
"Transport Method": "Transport Method",
|
||||
"Query Path": "Query Path",
|
||||
"Force HTTP2": "HTTP/2",
|
||||
"Resource Record Type": "Resource Record Type",
|
||||
"Skip Remote DNSSEC Verification": "Skip Remote DNSSEC Verification",
|
||||
"Last Result": "Last Result",
|
||||
|
@ -579,7 +580,9 @@
|
|||
"resolverserverDescription": "Cloudflare is the default server. Use IP address for UDP/TCP/DoT, and domain name for DoH/DoT.",
|
||||
"rrtypeDescription": "Select the RR type you want to monitor",
|
||||
"dnsTransportDescription": "Select the transport method for querying the DNS server.",
|
||||
"dohQueryPathDescription": "Set the query path to use for DNS wireformat. Must contain",
|
||||
"dohQueryPathDescription": "Set the query path to use for DNS wireformat.",
|
||||
"dohHttpMethodDescription": "Set the HTTP method to use for DoH query.",
|
||||
"forceHttp2": "Send the request using HTTP/2. Fails if the server does not support HTTP/2.",
|
||||
"skipRemoteDnssecDescription": "Requests the resolver server not to perform DNSSEC verification against queried records.",
|
||||
"pauseMonitorMsg": "Are you sure want to pause?",
|
||||
"enableDefaultNotificationDescription": "This notification will be enabled by default for new monitors. You can still disable the notification separately for each monitor.",
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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("dohQueryPathDescription") + ' "{query}".' }}
|
||||
{{ $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="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>
|
||||
|
|
|
@ -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}"
|
||||
|
|
Loading…
Add table
Reference in a new issue