diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 000000000..6e53fa086 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,12 @@ +# These are supported funding model platforms + +#github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] +#patreon: # Replace with a single Patreon username +open_collective: uptime-kuma # Replace with a single Open Collective username +#ko_fi: # Replace with a single Ko-fi username +#tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel +#community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry +#liberapay: # Replace with a single Liberapay username +#issuehunt: # Replace with a single IssueHunt username +#otechie: # Replace with a single Otechie username +#custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5bbf343a1..0734285c7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -22,13 +22,13 @@ If you are not sure, feel free to create an empty pull request draft first. ### *️⃣ Requires one more reviewer -I do not have such knowledge to test it +I do not have such knowledge to test it. - Add k8s supports ### *️⃣ Low Priority -It chnaged my current workflow and require further studies. +It changed my current workflow and require further studies. - Change my release approach diff --git a/README.md b/README.md index 025510f52..d5f0d317e 100644 --- a/README.md +++ b/README.md @@ -12,16 +12,16 @@ It is a self-hosted monitoring tool like "Uptime Robot". ## ⭐ Features -* Monitoring uptime for HTTP(s) / TCP / Ping. +* Monitoring uptime for HTTP(s) / TCP / Ping / DNS Record. * Fancy, Reactive, Fast UI/UX. -* Notifications via Webhook, Telegram, Discord, Gotify, Slack, Pushover, Email (SMTP) and more by Apprise. +* Notifications via Telegram, Discord, Gotify, Slack, Pushover, Email (SMTP), and [70+ notification services, click here for the full list](https://github.com/louislam/uptime-kuma/issues/284). * 20 seconds interval. ## 🔧 How to Install -### 🚀 Installer via cli +### 🚀 Installer via CLI -Interactive cli installer, supports Docker or without Docker. +Interactive CLI installer, supports Docker or without Docker. ```bash curl -o kuma_install.sh https://raw.githubusercontent.com/louislam/uptime-kuma/master/install.sh && sudo bash kuma_install.sh @@ -36,11 +36,6 @@ docker run -d --restart=always -p 3001:3001 -v uptime-kuma:/app/data --name upti Browse to http://localhost:3001 after started. -### ☸️ Kubernetes - -See more [here](kubernetes/README.md) - - ### Advanced Installation If you need more options or need to browse via a reserve proxy, please read: @@ -76,7 +71,7 @@ Telegram Notification Sample: ## Motivation -* I was looking for a self-hosted monitoring tool like "Uptime Robot", but it is hard to find a suitable one. One of the close one is statping. Unfortunately, it is not stable and unmaintained. +* I was looking for a self-hosted monitoring tool like "Uptime Robot", but it is hard to find a suitable one. One of the close ones is statping. Unfortunately, it is not stable and unmaintained. * Want to build a fancy UI. * Learn Vue 3 and vite.js. * Show the power of Bootstrap 5. @@ -89,6 +84,6 @@ If you love this project, please consider giving me a ⭐. If you want to report a bug or request a new feature. Free feel to open a new issue. -If you want to modify Uptime Kuma, this guideline maybe useful for you: https://github.com/louislam/uptime-kuma/blob/master/CONTRIBUTING.md +If you want to modify Uptime Kuma, this guideline may be useful for you: https://github.com/louislam/uptime-kuma/blob/master/CONTRIBUTING.md -English proofreading is needed too, because my grammar is not that great sadly. Feel free to correct my grammar in this Readme, source code or wiki. +English proofreading is needed too because my grammar is not that great sadly. Feel free to correct my grammar in this readme, source code, or wiki. diff --git a/db/patch7.sql b/db/patch7.sql new file mode 100644 index 000000000..4085daf3f --- /dev/null +++ b/db/patch7.sql @@ -0,0 +1,13 @@ +-- 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); + +ALTER TABLE monitor + ADD dns_last_result VARCHAR(255); + +COMMIT; diff --git a/db/patch8.sql b/db/patch8.sql new file mode 100644 index 000000000..d63a59476 --- /dev/null +++ b/db/patch8.sql @@ -0,0 +1,7 @@ +-- 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_last_result VARCHAR(255); + +COMMIT; diff --git a/extra/simple-dns-server.js b/extra/simple-dns-server.js new file mode 100644 index 000000000..5e5745f04 --- /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 cc1feebc5..61bab41eb 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 2d20c4094..24e67373f 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 5b923e8a9..7ce81be11 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 = 8; static noReject = true; static sqliteInstance = null; diff --git a/server/model/monitor.js b/server/model/monitor.js index 17ab27795..6d0d812bd 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,9 @@ 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, + dns_last_result: this.dns_last_result, notificationIDList, }; } @@ -174,6 +177,46 @@ 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) + } + + if (this.dnsLastResult !== dnsMessage) { + R.exec("UPDATE `monitor` SET dns_last_result = ? WHERE id = ? ", [ + dnsMessage, + this.id + ]); + } + + bean.msg = dnsMessage; + bean.status = UP; } if (this.isUpsideDown()) { diff --git a/server/server.js b/server/server.js index 34406ccc7..d4fe668b3 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 8a2f03879..a30bcfec0 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/languages/de-DE.js b/src/languages/de-DE.js index b2bb6081d..ba852cf32 100644 --- a/src/languages/de-DE.js +++ b/src/languages/de-DE.js @@ -99,4 +99,9 @@ export default { keywordDescription: "Suche nach einen Schlüsselwort in einer schlichten HTML oder JSON Ausgabe. Bitte beachte, es wird in der Groß-/Kleinschreibung unterschieden.", deleteMonitorMsg: "Bist du sicher das du den Monitor löschen möchtest?", deleteNotificationMsg: "Möchtest du diese Benachrichtigung wirklich für alle Monitore löschen?", + resoverserverDescription: "Cloudflare ist der Standardserver, dieser kann jederzeit geändern werden.", + "Resolver Server": "Auflösungsserver", + rrtypeDescription: "Wähle den RR-Typ aus, welchen du überwachen möchtest.", + "Last Result": "Letztes Ergebnis", + pauseMonitorMsg: "Bist du sicher das du den Monitor pausieren möchtest?", } diff --git a/src/languages/en.js b/src/languages/en.js index 05e3fc929..75c25dd57 100644 --- a/src/languages/en.js +++ b/src/languages/en.js @@ -13,4 +13,7 @@ export default { pauseDashboardHome: "Pause", deleteMonitorMsg: "Are you sure want to delete this monitor?", deleteNotificationMsg: "Are you sure want to delete this notification for all monitors?", + resoverserverDescription: "Cloudflare is the default server, you can change the resolver server anytime.", + rrtypeDescription: "Select the RR-Type you want to monitor", + pauseMonitorMsg: "Are you sure want to pause?", } diff --git a/src/languages/zh-HK.js b/src/languages/zh-HK.js index c61434d45..d0cd334a6 100644 --- a/src/languages/zh-HK.js +++ b/src/languages/zh-HK.js @@ -99,4 +99,8 @@ export default { "Certificate Info": "憑證詳細資料", deleteMonitorMsg: "是否確定刪除這個監測器", deleteNotificationMsg: "是否確定刪除這個通知設定?如監測器啟用了這個通知,將會收不到通知。", + "Resolver Server": "DNS 伺服器", + "Resource Record Type": "DNS 記錄類型", + resoverserverDescription: "預設值為 Cloudflare DNS 伺服器,你可以轉用其他 DNS 伺服器。", + rrtypeDescription: "請選擇 DNS 記錄類型", } diff --git a/src/pages/Details.vue b/src/pages/Details.vue index 3d49d6064..ce4f5229c 100644 --- a/src/pages/Details.vue +++ b/src/pages/Details.vue @@ -10,6 +10,10 @@
{{ $t("Keyword") }}: {{ monitor.keyword }} + [{{ monitor.dns_resolve_type }}] {{ monitor.hostname }} +
+ {{ $t("Last Result") }}: {{ monitor.dns_last_result }} +

@@ -161,8 +165,8 @@
- - Are you sure want to pause? + + {{ $t("pauseMonitorMsg") }} diff --git a/src/pages/EditMonitor.vue b/src/pages/EditMonitor.vue index 9590e11be..552261735 100644 --- a/src/pages/EditMonitor.vue +++ b/src/pages/EditMonitor.vue @@ -23,6 +23,9 @@ + @@ -40,20 +43,56 @@
- {{ $t("keywordDescription")}} + {{ $t("keywordDescription") }}
-
+ +
+
+ + +
@@ -86,35 +125,38 @@
-
- - -
- {{ $t("maxRedirectDescription") }} + +
@@ -155,6 +197,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 { @@ -168,12 +211,27 @@ export default { processing: false, monitor: { notificationIDList: {}, + // Do not add default value here, please check init() method }, 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 +258,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 +293,8 @@ export default { upsideDown: false, maxredirects: 10, accepted_statuscodes: ["200-299"], + dns_resolve_type: "A", + dns_resolve_server: "1.1.1.1", } } else if (this.isEdit) { this.$root.getSocket().emit("getMonitor", this.$route.params.id, (res) => {