diff --git a/db/patch7.sql b/db/patch7.sql new file mode 100644 index 00000000..2e8eba15 --- /dev/null +++ b/db/patch7.sql @@ -0,0 +1,10 @@ +-- You should not modify if this have pushed to Github, unless it does serious wrong with the db. +BEGIN TRANSACTION; + +ALTER TABLE monitor + ADD dns_resolve_type VARCHAR(5); + +ALTER TABLE monitor + ADD dns_resolve_server VARCHAR(255); + +COMMIT; diff --git a/extra/simple-dns-server.js b/extra/simple-dns-server.js new file mode 100644 index 00000000..5e5745f0 --- /dev/null +++ b/extra/simple-dns-server.js @@ -0,0 +1,144 @@ +/* + * Simple DNS Server + * For testing DNS monitoring type, dev only + */ +const dns2 = require("dns2"); + +const { Packet } = dns2; + +const server = dns2.createServer({ + udp: true +}); + +server.on("request", (request, send, rinfo) => { + for (let question of request.questions) { + console.log(question.name, type(question.type), question.class); + + const response = Packet.createResponseFromRequest(request); + + if (question.name === "existing.com") { + + if (question.type === Packet.TYPE.A) { + response.answers.push({ + name: question.name, + type: question.type, + class: question.class, + ttl: 300, + address: "1.2.3.4" + }); + } if (question.type === Packet.TYPE.AAAA) { + response.answers.push({ + name: question.name, + type: question.type, + class: question.class, + ttl: 300, + address: "fe80::::1234:5678:abcd:ef00", + }); + } else if (question.type === Packet.TYPE.CNAME) { + response.answers.push({ + name: question.name, + type: question.type, + class: question.class, + ttl: 300, + domain: "cname1.existing.com", + }); + } else if (question.type === Packet.TYPE.MX) { + response.answers.push({ + name: question.name, + type: question.type, + class: question.class, + ttl: 300, + exchange: "mx1.existing.com", + priority: 5 + }); + } else if (question.type === Packet.TYPE.NS) { + response.answers.push({ + name: question.name, + type: question.type, + class: question.class, + ttl: 300, + ns: "ns1.existing.com", + }); + } else if (question.type === Packet.TYPE.SOA) { + response.answers.push({ + name: question.name, + type: question.type, + class: question.class, + ttl: 300, + primary: "existing.com", + admin: "admin@existing.com", + serial: 2021082701, + refresh: 300, + retry: 3, + expiration: 10, + minimum: 10, + }); + } else if (question.type === Packet.TYPE.SRV) { + response.answers.push({ + name: question.name, + type: question.type, + class: question.class, + ttl: 300, + priority: 5, + weight: 5, + port: 8080, + target: "srv1.existing.com", + }); + } else if (question.type === Packet.TYPE.TXT) { + response.answers.push({ + name: question.name, + type: question.type, + class: question.class, + ttl: 300, + data: "#v=spf1 include:_spf.existing.com ~all", + }); + } else if (question.type === Packet.TYPE.CAA) { + response.answers.push({ + name: question.name, + type: question.type, + class: question.class, + ttl: 300, + flags: 0, + tag: "issue", + value: "ca.existing.com", + }); + } + + } + + if (question.name === "4.3.2.1.in-addr.arpa") { + if (question.type === Packet.TYPE.PTR) { + response.answers.push({ + name: question.name, + type: question.type, + class: question.class, + ttl: 300, + domain: "ptr1.existing.com", + }); + } + } + + send(response); + } +}); + +server.on("listening", () => { + console.log("Listening"); + console.log(server.addresses()); +}); + +server.on("close", () => { + console.log("server closed"); +}); + +server.listen({ + udp: 5300 +}); + +function type(code) { + for (let name in Packet.TYPE) { + if (Packet.TYPE[name] === code) { + return name; + } + } +} diff --git a/package-lock.json b/package-lock.json index cc1feebc..61bab41e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -53,6 +53,7 @@ "@vitejs/plugin-vue": "^1.4.0", "@vue/compiler-sfc": "^3.2.2", "core-js": "^3.16.1", + "dns2": "^2.0.1", "eslint": "^7.32.0", "eslint-plugin-vue": "^7.16.0", "sass": "^1.37.5", @@ -2457,6 +2458,12 @@ "node": ">=8" } }, + "node_modules/dns2": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/dns2/-/dns2-2.0.1.tgz", + "integrity": "sha512-jHRTCcS2h/MEQjhcCnOWGENtz5A4RrLoK1YFqlHCejGfK5zYu99C8cxVwTsIY7JUqolhDN8zuGlyqnbEe6azqg==", + "dev": true + }, "node_modules/doctrine": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", @@ -9441,6 +9448,12 @@ "path-type": "^4.0.0" } }, + "dns2": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/dns2/-/dns2-2.0.1.tgz", + "integrity": "sha512-jHRTCcS2h/MEQjhcCnOWGENtz5A4RrLoK1YFqlHCejGfK5zYu99C8cxVwTsIY7JUqolhDN8zuGlyqnbEe6azqg==", + "dev": true + }, "doctrine": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", diff --git a/package.json b/package.json index 2d20c409..24e67373 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,8 @@ "test-install-script-alpine3": "npm run compile-install-script && docker build --progress plain -f test/test_install_script/alpine3.dockerfile .", "test-install-script-ubuntu": "npm run compile-install-script && docker build --progress plain -f test/test_install_script/ubuntu.dockerfile .", "test-install-script-ubuntu1604": "npm run compile-install-script && docker build --progress plain -f test/test_install_script/ubuntu1604.dockerfile .", - "test-install-script-debian": "npm run compile-install-script && docker build --progress plain -f test/test_install_script/debian.dockerfile ." + "test-install-script-debian": "npm run compile-install-script && docker build --progress plain -f test/test_install_script/debian.dockerfile .", + "simple-dns-server": "node extra/simple-dns-server.js" }, "dependencies": { "@fortawesome/fontawesome-svg-core": "^1.2.36", @@ -79,6 +80,7 @@ "@vitejs/plugin-vue": "^1.4.0", "@vue/compiler-sfc": "^3.2.2", "core-js": "^3.16.1", + "dns2": "^2.0.1", "eslint": "^7.32.0", "eslint-plugin-vue": "^7.16.0", "sass": "^1.37.5", diff --git a/server/database.js b/server/database.js index 5b923e8a..2b9b715c 100644 --- a/server/database.js +++ b/server/database.js @@ -6,7 +6,7 @@ class Database { static templatePath = "./db/kuma.db" static path = "./data/kuma.db"; - static latestVersion = 6; + static latestVersion = 7; static noReject = true; static sqliteInstance = null; diff --git a/server/model/monitor.js b/server/model/monitor.js index 17ab2779..f5efadc1 100644 --- a/server/model/monitor.js +++ b/server/model/monitor.js @@ -7,7 +7,7 @@ dayjs.extend(timezone) const axios = require("axios"); const { Prometheus } = require("../prometheus"); const { debug, UP, DOWN, PENDING, flipStatus, TimeLogger } = require("../../src/util"); -const { tcping, ping, checkCertificate, checkStatusCode } = require("../util-server"); +const { tcping, ping, dnsResolve, checkCertificate, checkStatusCode } = require("../util-server"); const { R } = require("redbean-node"); const { BeanModel } = require("redbean-node/dist/bean-model"); const { Notification } = require("../notification") @@ -48,6 +48,8 @@ class Monitor extends BeanModel { upsideDown: this.isUpsideDown(), maxredirects: this.maxredirects, accepted_statuscodes: this.getAcceptedStatuscodes(), + dns_resolve_type: this.dns_resolve_type, + dns_resolve_server: this.dns_resolve_server, notificationIDList, }; } @@ -174,6 +176,39 @@ class Monitor extends BeanModel { bean.ping = await ping(this.hostname); bean.msg = "" bean.status = UP; + } else if (this.type === "dns") { + let startTime = dayjs().valueOf(); + let dnsMessage = ""; + + let dnsRes = await dnsResolve(this.hostname, this.dns_resolve_server, this.dns_resolve_type); + bean.ping = dayjs().valueOf() - startTime; + + if (this.dns_resolve_type == "A" || this.dns_resolve_type == "AAAA" || this.dns_resolve_type == "TXT") { + dnsMessage += "Records: "; + dnsMessage += dnsRes.join(" | "); + } else if (this.dns_resolve_type == "CNAME" || this.dns_resolve_type == "PTR") { + dnsMessage = dnsRes[0]; + } else if (this.dns_resolve_type == "CAA") { + dnsMessage = dnsRes[0].issue; + } else if (this.dns_resolve_type == "MX") { + dnsRes.forEach(record => { + dnsMessage += `Hostname: ${record.exchange} - Priority: ${record.priority} | `; + }); + dnsMessage = dnsMessage.slice(0, -2) + } else if (this.dns_resolve_type == "NS") { + dnsMessage += "Servers: "; + dnsMessage += dnsRes.join(" | "); + } else if (this.dns_resolve_type == "SOA") { + dnsMessage += `NS-Name: ${dnsRes.nsname} | Hostmaster: ${dnsRes.hostmaster} | Serial: ${dnsRes.serial} | Refresh: ${dnsRes.refresh} | Retry: ${dnsRes.retry} | Expire: ${dnsRes.expire} | MinTTL: ${dnsRes.minttl}`; + } else if (this.dns_resolve_type == "SRV") { + dnsRes.forEach(record => { + dnsMessage += `Name: ${record.name} | Port: ${record.port} | Priority: ${record.priority} | Weight: ${record.weight} | `; + }); + dnsMessage = dnsMessage.slice(0, -2) + } + + bean.msg = dnsMessage; + bean.status = UP; } if (this.isUpsideDown()) { diff --git a/server/server.js b/server/server.js index 34406ccc..d4fe668b 100644 --- a/server/server.js +++ b/server/server.js @@ -291,6 +291,8 @@ let indexHTML = fs.readFileSync("./dist/index.html").toString(); bean.upsideDown = monitor.upsideDown; 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; await R.store(bean) diff --git a/server/util-server.js b/server/util-server.js index 8a2f0387..a30bcfec 100644 --- a/server/util-server.js +++ b/server/util-server.js @@ -4,6 +4,7 @@ const { R } = require("redbean-node"); const { debug } = require("../src/util"); const passwordHash = require("./password-hash"); const dayjs = require("dayjs"); +const { Resolver } = require("dns"); /** * Init or reset JWT secret @@ -76,6 +77,30 @@ exports.pingAsync = function (hostname, ipv6 = false) { }); } +exports.dnsResolve = function (hostname, resolver_server, rrtype) { + const resolver = new Resolver(); + resolver.setServers([resolver_server]); + return new Promise((resolve, reject) => { + if (rrtype == "PTR") { + resolver.reverse(hostname, (err, records) => { + if (err) { + reject(err); + } else { + resolve(records); + } + }); + } else { + resolver.resolve(hostname, rrtype, (err, records) => { + if (err) { + reject(err); + } else { + resolve(records); + } + }); + } + }) +} + exports.setting = async function (key) { let value = await R.getCell("SELECT `value` FROM setting WHERE `key` = ? ", [ key, diff --git a/src/pages/EditMonitor.vue b/src/pages/EditMonitor.vue index 9590e11b..33dc1621 100644 --- a/src/pages/EditMonitor.vue +++ b/src/pages/EditMonitor.vue @@ -23,6 +23,9 @@ + @@ -54,6 +57,41 @@ +
+ + +
+ +
+ + +
+ Cloudflare is the default server, you can change the resolver server anytime. +
+
+ +
+ + + + +
+ Select the RR-Type you want to monitor +
+
+
@@ -155,6 +193,7 @@ import NotificationDialog from "../components/NotificationDialog.vue"; import { useToast } from "vue-toastification" import VueMultiselect from "vue-multiselect" +import { isDev } from "../util.ts"; const toast = useToast() export default { @@ -170,10 +209,24 @@ export default { notificationIDList: {}, }, acceptedStatusCodeOptions: [], + dnsresolvetypeOptions: [], + + // Source: https://digitalfortress.tech/tips/top-15-commonly-used-regex/ + ipRegexPattern: "((^\s*((([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))\s*$)|(^\s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))(%.+)?\s*$))", } }, computed: { + + ipRegex() { + + // Allow to test with simple dns server with port (127.0.0.1:5300) + if (! isDev) { + return this.ipRegexPattern; + } + return null; + }, + pageName() { return this.$t((this.isAdd) ? "Add New Monitor" : "Edit"); }, @@ -200,11 +253,25 @@ export default { "500-599", ]; + let dnsresolvetypeOptions = [ + "A", + "AAAA", + "CAA", + "CNAME", + "MX", + "NS", + "PTR", + "SOA", + "SRV", + "TXT", + ]; + for (let i = 100; i <= 999; i++) { acceptedStatusCodeOptions.push(i.toString()); } this.acceptedStatusCodeOptions = acceptedStatusCodeOptions; + this.dnsresolvetypeOptions = dnsresolvetypeOptions; }, methods: { init() { @@ -221,6 +288,7 @@ export default { upsideDown: false, maxredirects: 10, accepted_statuscodes: ["200-299"], + dns_resolve_server: "1.1.1.1", } } else if (this.isEdit) { this.$root.getSocket().emit("getMonitor", this.$route.params.id, (res) => {