diff --git a/.github/ISSUE_TEMPLATE/--please-go-to--discussion--tab-if-you-want-to-ask-or-share-something.md b/.github/ISSUE_TEMPLATE/--please-go-to--discussion--tab-if-you-want-to-ask-or-share-something.md deleted file mode 100644 index eb8623709..000000000 --- a/.github/ISSUE_TEMPLATE/--please-go-to--discussion--tab-if-you-want-to-ask-or-share-something.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -name: ⚠ Please go to "Discussions" Tab if you want to ask or share something -about: BUG REPORT ONLY HERE -title: '' -labels: '' -assignees: '' - ---- - -BUG REPORT ONLY HERE diff --git a/.github/ISSUE_TEMPLATE/ask-for-help.md b/.github/ISSUE_TEMPLATE/ask-for-help.md new file mode 100644 index 000000000..c3657267a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/ask-for-help.md @@ -0,0 +1,10 @@ +--- +name: Ask for help +about: You can ask any question related to Uptime Kuma. +title: '' +labels: help +assignees: '' + +--- + + diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 000000000..11fc491ef --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: enhancement +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml new file mode 100644 index 000000000..3949d2495 --- /dev/null +++ b/.github/workflows/codeql-analysis.yml @@ -0,0 +1,71 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL" + +on: + push: + branches: [ master ] + pull_request: + # The branches below must be a subset of the branches above + branches: [ master ] + schedule: + - cron: '35 5 * * 2' + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: [ 'javascript' ] + # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] + # Learn more: + # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v1 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + # queries: ./path/to/local/query, your-org/your-repo/queries@main + + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v1 + + # ℹī¸ Command-line programs to run using the OS shell. + # 📚 https://git.io/JvXDl + + # ✏ī¸ If the Autobuild fails above, remove it and uncomment the following three lines + # and modify them (or add more) to build your code if your project + # uses a compiled language + + #- run: | + # make bootstrap + # make release + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v1 diff --git a/README.md b/README.md index 7777cb2c5..ef8211368 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ It is a self-hosted monitoring tool like "Uptime Robot". * Monitoring uptime for HTTP(s) / TCP / Ping. * Fancy, Reactive, Fast UI/UX. -* Notifications via Webhook, Telegram, Discord and email (SMTP). +* Notifications via Webhook, Telegram, Discord, Gotify, Slack, Pushover, Email (SMTP) and more by Apprise. * 20 seconds interval. # How to Use @@ -80,7 +80,7 @@ PS: For every new release, it takes some time to build the docker image, please ```bash git fetch --all -git checkout 1.0.6 --force +git checkout 1.0.7 --force npm install npm run build pm2 restart uptime-kuma diff --git a/db/patch2.sql b/db/patch2.sql new file mode 100644 index 000000000..012d01502 --- /dev/null +++ b/db/patch2.sql @@ -0,0 +1,9 @@ +BEGIN TRANSACTION; + +CREATE TABLE monitor_tls_info ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + monitor_id INTEGER NOT NULL, + info_json TEXT +); + +COMMIT; diff --git a/package-lock.json b/package-lock.json index 99dd01b6b..2fcc0feec 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "uptime-kuma", - "version": "1.0.6", + "version": "1.0.7", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -29,21 +29,96 @@ "to-fast-properties": "^2.0.0" } }, + "@fortawesome/fontawesome-common-types": { + "version": "0.2.35", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-0.2.35.tgz", + "integrity": "sha512-IHUfxSEDS9dDGqYwIW7wTN6tn/O8E0n5PcAHz9cAaBoZw6UpG20IG/YM3NNLaGPwPqgjBAFjIURzqoQs3rrtuw==" + }, + "@fortawesome/fontawesome-svg-core": { + "version": "1.2.35", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-1.2.35.tgz", + "integrity": "sha512-uLEXifXIL7hnh2sNZQrIJWNol7cTVIzwI+4qcBIq9QWaZqUblm0IDrtSqbNg+3SQf8SMGHkiSigD++rHmCHjBg==", + "requires": { + "@fortawesome/fontawesome-common-types": "^0.2.35" + } + }, + "@fortawesome/free-regular-svg-icons": { + "version": "5.15.3", + "resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-5.15.3.tgz", + "integrity": "sha512-q4/p8Xehy9qiVTdDWHL4Z+o5PCLRChePGZRTXkl+/Z7erDVL8VcZUuqzJjs6gUz6czss4VIPBRdCz6wP37/zMQ==", + "requires": { + "@fortawesome/fontawesome-common-types": "^0.2.35" + } + }, + "@fortawesome/free-solid-svg-icons": { + "version": "5.15.3", + "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-5.15.3.tgz", + "integrity": "sha512-XPeeu1IlGYqz4VWGRAT5ukNMd4VHUEEJ7ysZ7pSSgaEtNvSo+FLurybGJVmiqkQdK50OkSja2bfZXOeyMGRD8Q==", + "requires": { + "@fortawesome/fontawesome-common-types": "^0.2.35" + } + }, + "@fortawesome/vue-fontawesome": { + "version": "3.0.0-4", + "resolved": "https://registry.npmjs.org/@fortawesome/vue-fontawesome/-/vue-fontawesome-3.0.0-4.tgz", + "integrity": "sha512-dQVhhMRcUPCb0aqk5ohm0KGk5OJ7wFZ9aYapLzJB3Z+xs7LhkRWLTb87reelUAG5PFDjutDAXuloT9hi6cz72A==" + }, "@popperjs/core": { "version": "2.9.2", "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.9.2.tgz", "integrity": "sha512-VZMYa7+fXHdwIq1TDhSXoVmSPEGM/aa+6Aiq3nVVJ9bXr24zScr+NlKFKC3iPljA7ho/GAZr+d2jOf5GIRC30Q==" }, + "@types/accepts": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/accepts/-/accepts-1.3.5.tgz", + "integrity": "sha512-jOdnI/3qTpHABjM5cx1Hc0sKsPoYCp+DP/GJRGtDlPd7fiV9oXGGIcjW/ZOxLIvjGz8MA+uMZI9metHlgqbgwQ==", + "requires": { + "@types/node": "*" + } + }, + "@types/body-parser": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.1.tgz", + "integrity": "sha512-a6bTJ21vFOGIkwM0kzh9Yr89ziVxq4vYH2fQ6N8AeipEzai/cFK6aGMArIkUeIdRIgpwQa+2bXiLuUJCpSf2Cg==", + "requires": { + "@types/connect": "*", + "@types/node": "*" + } + }, "@types/component-emitter": { "version": "1.2.10", "resolved": "https://registry.npmjs.org/@types/component-emitter/-/component-emitter-1.2.10.tgz", "integrity": "sha512-bsjleuRKWmGqajMerkzox19aGbscQX5rmmvvXl3wlIp5gMG1HgkiwPxsN5p070fBDKTNSPgojVbuY1+HWMbFhg==" }, + "@types/connect": { + "version": "3.4.35", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz", + "integrity": "sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==", + "requires": { + "@types/node": "*" + } + }, + "@types/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/@types/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-0mPF08jn9zYI0n0Q/Pnz7C4kThdSt+6LD4amsrYDDpgBfrVWa3TcCOxKX1zkGgYniGagRv8heN2cbh+CAn+uuQ==" + }, "@types/cookie": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz", "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==" }, + "@types/cookies": { + "version": "0.7.7", + "resolved": "https://registry.npmjs.org/@types/cookies/-/cookies-0.7.7.tgz", + "integrity": "sha512-h7BcvPUogWbKCzBR2lY4oqaZbO3jXZksexYJVFvkrFeLgbZjQkU4x8pRq6eg2MHXQhY0McQdqmmsxRWlVAHooA==", + "requires": { + "@types/connect": "*", + "@types/express": "*", + "@types/keygrip": "*", + "@types/node": "*" + } + }, "@types/cors": { "version": "2.8.12", "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.12.tgz", @@ -55,11 +130,94 @@ "integrity": "sha512-LfZwXoGUDo0C3me81HXgkBg5CTQYb6xzEl+fNmbO4JdRiSKQ8A0GD1OBBvKAIsbCUgoyAty7m99GqqMQe784ew==", "dev": true }, + "@types/express": { + "version": "4.17.13", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.13.tgz", + "integrity": "sha512-6bSZTPaTIACxn48l50SR+axgrqm6qXFIxrdAKaG6PaJk3+zuUr35hBlgT7vOmJcum+OEaIBLtHV/qloEAFITeA==", + "requires": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.18", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "@types/express-serve-static-core": { + "version": "4.17.24", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.24.tgz", + "integrity": "sha512-3UJuW+Qxhzwjq3xhwXm2onQcFHn76frIYVbTu+kn24LFxI+dEhdfISDFovPB8VpEgW8oQCTpRuCe+0zJxB7NEA==", + "requires": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*" + } + }, + "@types/http-assert": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@types/http-assert/-/http-assert-1.5.1.tgz", + "integrity": "sha512-PGAK759pxyfXE78NbKxyfRcWYA/KwW17X290cNev/qAsn9eQIxkH4shoNBafH37wewhDG/0p1cHPbK6+SzZjWQ==" + }, + "@types/http-errors": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-1.8.1.tgz", + "integrity": "sha512-e+2rjEwK6KDaNOm5Aa9wNGgyS9oSZU/4pfSMMPYNOfjvFI0WVXm29+ITRFr6aKDvvKo7uU1jV68MW4ScsfDi7Q==" + }, + "@types/keygrip": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@types/keygrip/-/keygrip-1.0.2.tgz", + "integrity": "sha512-GJhpTepz2udxGexqos8wgaBx4I/zWIDPh/KOGEwAqtuGDkOUJu5eFvwmdBX4AmB8Odsr+9pHCQqiAqDL/yKMKw==" + }, + "@types/koa": { + "version": "2.13.4", + "resolved": "https://registry.npmjs.org/@types/koa/-/koa-2.13.4.tgz", + "integrity": "sha512-dfHYMfU+z/vKtQB7NUrthdAEiSvnLebvBjwHtfFmpZmB7em2N3WVQdHgnFq+xvyVgxW5jKDmjWfLD3lw4g4uTw==", + "requires": { + "@types/accepts": "*", + "@types/content-disposition": "*", + "@types/cookies": "*", + "@types/http-assert": "*", + "@types/http-errors": "*", + "@types/keygrip": "*", + "@types/koa-compose": "*", + "@types/node": "*" + } + }, + "@types/koa-compose": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/@types/koa-compose/-/koa-compose-3.2.5.tgz", + "integrity": "sha512-B8nG/OoE1ORZqCkBVsup/AKcvjdgoHnfi4pZMn5UwAPCbhk/96xyv284eBYW8JlQbQ7zDmnpFr68I/40mFoIBQ==", + "requires": { + "@types/koa": "*" + } + }, + "@types/mime": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz", + "integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==" + }, "@types/node": { "version": "16.3.3", "resolved": "https://registry.npmjs.org/@types/node/-/node-16.3.3.tgz", "integrity": "sha512-8h7k1YgQKxKXWckzFCMfsIwn0Y61UK6tlD6y2lOb3hTOIMlK3t9/QwHOhc81TwU+RMf0As5fj7NPjroERCnejQ==" }, + "@types/qs": { + "version": "6.9.7", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz", + "integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==" + }, + "@types/range-parser": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.4.tgz", + "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==" + }, + "@types/serve-static": { + "version": "1.13.10", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.10.tgz", + "integrity": "sha512-nCkHGI4w7ZgAdNkrEu0bv+4xNV/XDqW+DydknebMOQwkpDGx8G+HTlj7R7ABI8i8nKxVw0wtKPi1D+lPOkh4YQ==", + "requires": { + "@types/mime": "^1", + "@types/node": "*" + } + }, "@vitejs/plugin-legacy": { "version": "1.4.4", "resolved": "https://registry.npmjs.org/@vitejs/plugin-legacy/-/plugin-legacy-1.4.4.tgz", @@ -467,6 +625,14 @@ "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==" }, + "basic-auth": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", + "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", + "requires": { + "safe-buffer": "5.1.2" + } + }, "bcrypt": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.0.1.tgz", @@ -557,6 +723,11 @@ "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", "dev": true }, + "bintrees": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/bintrees/-/bintrees-1.0.1.tgz", + "integrity": "sha1-DmVcm5wkNeqraL9AJyJtK1WjRSQ=" + }, "block-stream": { "version": "0.0.9", "resolved": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz", @@ -1131,6 +1302,14 @@ } } }, + "express-basic-auth": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/express-basic-auth/-/express-basic-auth-1.2.0.tgz", + "integrity": "sha512-iJ0h1Gk6fZRrFmO7tP9nIbxwNgCUJASfNj5fb0Hy15lGtbqqsxpt7609+wq+0XlByZjXmC/rslWQtnuSTVRIcg==", + "requires": { + "basic-auth": "^2.0.1" + } + }, "extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", @@ -2004,6 +2183,11 @@ "integrity": "sha1-soqmKIorn8ZRA1x3EfZathkDMaY=", "dev": true }, + "lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=" + }, "lodash.includes": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", @@ -2757,6 +2941,11 @@ "integrity": "sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==", "dev": true }, + "pkginfo": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/pkginfo/-/pkginfo-0.4.1.tgz", + "integrity": "sha1-tUGO8EOd5UJfxJlQQtztFPsqhP8=" + }, "posix-character-classes": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", @@ -2845,6 +3034,37 @@ "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" }, + "prom-client": { + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/prom-client/-/prom-client-13.1.0.tgz", + "integrity": "sha512-jT9VccZCWrJWXdyEtQddCDszYsiuWj5T0ekrPszi/WEegj3IZy6Mm09iOOVM86A4IKMWq8hZkT2dD9MaSe+sng==", + "requires": { + "tdigest": "^0.1.1" + } + }, + "prometheus-api-metrics": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/prometheus-api-metrics/-/prometheus-api-metrics-3.2.0.tgz", + "integrity": "sha512-JekPhtIBLGX8HxD2EndvBsLU6ZQ1JVVqyHWVfm5CposUOqgBHXnUVFW6x5Ux2gykpdej/5LLM3dU9V8Ma7GfkA==", + "requires": { + "@types/express": "^4.17.8", + "@types/express-serve-static-core": "^4.17.12", + "@types/koa": "^2.11.4", + "debug": "^3.2.6", + "lodash.get": "^4.4.2", + "pkginfo": "^0.4.1" + }, + "dependencies": { + "debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "requires": { + "ms": "^2.1.1" + } + } + } + }, "proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -3529,6 +3749,14 @@ "resolved": "https://registry.npmjs.org/tcp-ping/-/tcp-ping-0.1.1.tgz", "integrity": "sha1-At1/QrW/fXy3jVt6rO+hVf2PfAw=" }, + "tdigest": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/tdigest/-/tdigest-0.1.1.tgz", + "integrity": "sha1-Ljyyw56kSeVdHmzZEReszKRYgCE=", + "requires": { + "bintrees": "1.0.1" + } + }, "tildify": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/tildify/-/tildify-2.0.0.tgz", diff --git a/package.json b/package.json index 7069e3edd..7cfd9e408 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "uptime-kuma", - "version": "1.0.6", + "version": "1.0.7", "license": "MIT", "repository": { "type": "git", @@ -12,14 +12,18 @@ "update": "", "build": "vite build", "vite-preview-dist": "vite preview --host", - "build-docker": "docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma -t louislam/uptime-kuma:1 -t louislam/uptime-kuma:1.0.6 --target release . --push", + "build-docker": "docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma -t louislam/uptime-kuma:1 -t louislam/uptime-kuma:1.0.7 --target release . --push", "build-docker-nightly": "docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:nightly --target nightly . --push", "build-docker-nightly-amd64": "docker buildx build --platform linux/amd64 -t louislam/uptime-kuma:nightly-amd64 --target nightly . --push", - "setup": "git checkout 1.0.6 && npm install && npm run build", + "setup": "git checkout 1.0.7 && npm install && npm run build", "version-global-replace": "node extra/version-global-replace.js", "mark-as-nightly": "node extra/mark-as-nightly.js" }, "dependencies": { + "@fortawesome/fontawesome-svg-core": "^1.2.35", + "@fortawesome/free-regular-svg-icons": "^5.15.3", + "@fortawesome/free-solid-svg-icons": "^5.15.3", + "@fortawesome/vue-fontawesome": "^3.0.0-4", "@popperjs/core": "^2.9.2", "args-parser": "^1.3.0", "axios": "^0.21.1", @@ -28,11 +32,14 @@ "command-exists": "^1.2.9", "dayjs": "^1.10.6", "express": "^4.17.1", + "express-basic-auth": "^1.2.0", "form-data": "^4.0.0", "http-graceful-shutdown": "^3.1.2", "jsonwebtoken": "^8.5.1", "nodemailer": "^6.6.3", "password-hash": "^1.2.2", + "prom-client": "^13.1.0", + "prometheus-api-metrics": "^3.2.0", "redbean-node": "0.0.20", "socket.io": "^4.1.3", "socket.io-client": "^4.1.3", diff --git a/server/auth.js b/server/auth.js new file mode 100644 index 000000000..b4d13d68a --- /dev/null +++ b/server/auth.js @@ -0,0 +1,40 @@ +const basicAuth = require('express-basic-auth') +const passwordHash = require('./password-hash'); +const {R} = require("redbean-node"); + +/** + * + * @param username : string + * @param password : string + * @returns {Promise} + */ +exports.login = async function (username, password) { + let user = await R.findOne("user", " username = ? AND active = 1 ", [ + username + ]) + + if (user && passwordHash.verify(password, user.password)) { + // Upgrade the hash to bcrypt + if (passwordHash.needRehash(user.password)) { + await R.exec("UPDATE `user` SET password = ? WHERE id = ? ", [ + passwordHash.generate(password), + user.id + ]); + } + return user; + } else { + return null; + } +} + +function myAuthorizer(username, password, callback) { + exports.login(username, password).then((user) => { + callback(null, user != null) + }) +} + +exports.basicAuth = basicAuth({ + authorizer: myAuthorizer, + authorizeAsync: true, + challenge: true +}); diff --git a/server/database.js b/server/database.js index 49659e613..2c9b2f653 100644 --- a/server/database.js +++ b/server/database.js @@ -8,7 +8,7 @@ class Database { static templatePath = "./db/kuma.db" static path = './data/kuma.db'; - static latestVersion = 1; + static latestVersion = 3; static noReject = true; static async patch() { diff --git a/server/model/monitor.js b/server/model/monitor.js index 133088671..27f3f9bab 100644 --- a/server/model/monitor.js +++ b/server/model/monitor.js @@ -1,16 +1,23 @@ - +const https = require('https'); const dayjs = require("dayjs"); const utc = require('dayjs/plugin/utc') var timezone = require('dayjs/plugin/timezone') dayjs.extend(utc) dayjs.extend(timezone) const axios = require("axios"); -const {UP, DOWN, PENDING} = require("../util"); -const {tcping, ping} = require("../util-server"); +const {Prometheus} = require("../prometheus"); +const {debug, UP, DOWN, PENDING} = require("../util"); +const {tcping, ping, checkCertificate} = require("../util-server"); const {R} = require("redbean-node"); const {BeanModel} = require("redbean-node/dist/bean-model"); const {Notification} = require("../notification") +// Use Custom agent to disable session reuse +// https://github.com/nodejs/node/issues/3940 +const customAgent = new https.Agent({ + maxCachedSessions: 0 +}); + /** * status: * 0 = DOWN @@ -49,6 +56,8 @@ class Monitor extends BeanModel { let previousBeat = null; let retries = 0; + let prometheus = new Prometheus(this); + const beat = async () => { if (! previousBeat) { @@ -75,11 +84,25 @@ class Monitor extends BeanModel { if (this.type === "http" || this.type === "keyword") { let startTime = dayjs().valueOf(); let res = await axios.get(this.url, { - headers: { 'User-Agent':'Uptime-Kuma' } - }) + headers: { "User-Agent": "Uptime-Kuma" }, + httpsAgent: customAgent, + }); bean.msg = `${res.status} - ${res.statusText}` bean.ping = dayjs().valueOf() - startTime; + // Check certificate if https is used + + let certInfoStartTime = dayjs().valueOf(); + if (this.getUrl()?.protocol === "https:") { + try { + await this.updateTlsInfo(checkCertificate(res)); + } catch (e) { + console.error(e.message) + } + } + + debug("Cert Info Query Time: " + (dayjs().valueOf() - certInfoStartTime) + "ms") + if (this.type === "http") { bean.status = UP; } else { @@ -170,6 +193,7 @@ class Monitor extends BeanModel { bean.important = false; } + if (bean.status === UP) { console.info(`Monitor #${this.id} '${this.name}': Successful Response: ${bean.ping} ms | Interval: ${this.interval} seconds | Type: ${this.type}`) } else if (bean.status === PENDING) { @@ -178,6 +202,8 @@ class Monitor extends BeanModel { console.warn(`Monitor #${this.id} '${this.name}': Failing: ${bean.msg} | Type: ${this.type}`) } + prometheus.update(bean) + io.to(this.user_id).emit("heartbeat", bean.toJSON()); await R.store(bean) @@ -194,10 +220,35 @@ class Monitor extends BeanModel { clearInterval(this.heartbeatInterval) } + // Helper Method: + // returns URL object for further usage + // returns null if url is invalid + getUrl() { + try { + return new URL(this.url); + } catch (_) { + return null; + } + } + + // Store TLS info to database + async updateTlsInfo(checkCertificateResult) { + let tls_info_bean = await R.findOne("monitor_tls_info", "monitor_id = ?", [ + this.id + ]); + if (tls_info_bean == null) { + tls_info_bean = R.dispense("monitor_tls_info"); + tls_info_bean.monitor_id = this.id; + } + tls_info_bean.info_json = JSON.stringify(checkCertificateResult); + await R.store(tls_info_bean); + } + static async sendStats(io, monitorID, userID) { Monitor.sendAvgPing(24, io, monitorID, userID); Monitor.sendUptime(24, io, monitorID, userID); Monitor.sendUptime(24 * 30, io, monitorID, userID); + Monitor.sendCertInfo(io, monitorID, userID); } /** @@ -218,6 +269,15 @@ class Monitor extends BeanModel { io.to(userID).emit("avgPing", monitorID, avgPing); } + static async sendCertInfo(io, monitorID, userID) { + let tls_info = await R.findOne("monitor_tls_info", "monitor_id = ?", [ + monitorID + ]); + if (tls_info != null) { + io.to(userID).emit("certInfo", monitorID, tls_info.info_json); + } + } + /** * Uptime with calculation * Calculation based on: diff --git a/server/prometheus.js b/server/prometheus.js new file mode 100644 index 000000000..f60ec45a6 --- /dev/null +++ b/server/prometheus.js @@ -0,0 +1,59 @@ +const PrometheusClient = require('prom-client'); + +const commonLabels = [ + 'monitor_name', + 'monitor_type', + 'monitor_url', + 'monitor_hostname', + 'monitor_port', +] + +const monitor_response_time = new PrometheusClient.Gauge({ + name: 'monitor_response_time', + help: 'Monitor Response Time (ms)', + labelNames: commonLabels +}); + +const monitor_status = new PrometheusClient.Gauge({ + name: 'monitor_status', + help: 'Monitor Status (1 = UP, 0= DOWN)', + labelNames: commonLabels +}); + +class Prometheus { + monitorLabelValues = {} + + constructor(monitor) { + this.monitorLabelValues = { + monitor_name: monitor.name, + monitor_type: monitor.type, + monitor_url: monitor.url, + monitor_hostname: monitor.hostname, + monitor_port: monitor.port + } + } + + update(heartbeat) { + try { + monitor_status.set(this.monitorLabelValues, heartbeat.status) + } catch (e) { + console.error(e) + } + + try { + if (typeof heartbeat.ping === 'number') { + monitor_response_time.set(this.monitorLabelValues, heartbeat.ping) + } else { + // Is it good? + monitor_response_time.set(this.monitorLabelValues, -1) + } + } catch (e) { + console.error(e) + } + } + +} + +module.exports = { + Prometheus +} diff --git a/server/server.js b/server/server.js index 6bf48dc13..2014d927f 100644 --- a/server/server.js +++ b/server/server.js @@ -5,7 +5,6 @@ const http = require('http'); const { Server } = require("socket.io"); const dayjs = require("dayjs"); const {R} = require("redbean-node"); -const passwordHash = require('./password-hash'); const jwt = require('jsonwebtoken'); const Monitor = require("./model/monitor"); const fs = require("fs"); @@ -15,7 +14,9 @@ const gracefulShutdown = require('http-graceful-shutdown'); const Database = require("./database"); const {sleep} = require("./util"); const args = require('args-parser')(process.argv); - +const prometheusAPIMetrics = require('prometheus-api-metrics'); +const { basicAuth } = require("./auth"); +const {login} = require("./auth"); const version = require('../package.json').version; const hostname = args.host || "0.0.0.0" const port = args.port || 3001 @@ -27,6 +28,9 @@ const app = express(); const server = http.createServer(app); const io = new Server(server); app.use(express.json()) +const basicAuthRouter = express.Router(); +basicAuthRouter.use(basicAuth) +app.use(basicAuthRouter) /** * Total WebSocket client connected to server currently, no actual use @@ -56,13 +60,27 @@ let needSetup = false; await initDatabase(); console.log("Adding route") + + // Normal Router here + app.use('/', express.static("dist")); + // Basic Auth Router here + + // For testing + basicAuthRouter.get('/test-auth', (req, res) => { + res.end("OK") + }); + + // Prometheus API metrics /metrics + // With Basic Auth using the first user's username/password + basicAuthRouter.use(prometheusAPIMetrics()) + + // Universal Route Handler, must be at the end app.get('*', function(request, response, next) { response.sendFile(process.cwd() + '/dist/index.html'); }); - console.log("Adding socket handler") io.on('connection', async (socket) => { @@ -118,20 +136,9 @@ let needSetup = false; socket.on("login", async (data, callback) => { console.log("Login") - let user = await R.findOne("user", " username = ? AND active = 1 ", [ - data.username - ]) - - if (user && passwordHash.verify(data.password, user.password)) { - - // Upgrade the hash to bcrypt - if (passwordHash.needRehash(user.password)) { - await R.exec("UPDATE `user` SET password = ? WHERE id = ? ", [ - passwordHash.generate(data.password), - user.id - ]); - } + let user = await login(data.username, data.password) + if (user) { await afterLogin(socket, user) callback({ @@ -543,12 +550,12 @@ async function afterLogin(socket, user) { let monitorList = await sendMonitorList(socket) for (let monitorID in monitorList) { - await sendHeartbeatList(socket, monitorID); - await sendImportantHeartbeatList(socket, monitorID); - await Monitor.sendStats(io, monitorID, user.id) + sendHeartbeatList(socket, monitorID); + sendImportantHeartbeatList(socket, monitorID); + Monitor.sendStats(io, monitorID, user.id) } - await sendNotificationList(socket) + sendNotificationList(socket) } async function getMonitorJSONList(userID) { diff --git a/server/util-server.js b/server/util-server.js index b387f4c7c..f03823d3a 100644 --- a/server/util-server.js +++ b/server/util-server.js @@ -70,3 +70,52 @@ exports.getSettings = async function (type) { return result; } + + +// ssl-checker by @dyaa +// param: res - response object from axios +// return an object containing the certificate information + +const getDaysBetween = (validFrom, validTo) => + Math.round(Math.abs(+validFrom - +validTo) / 8.64e7); + +const getDaysRemaining = (validFrom, validTo) => { + const daysRemaining = getDaysBetween(validFrom, validTo); + if (new Date(validTo).getTime() < new Date().getTime()) { + return -daysRemaining; + } + return daysRemaining; +}; + +exports.checkCertificate = function (res) { + const { + valid_from, + valid_to, + subjectaltname, + issuer, + fingerprint, + } = res.request.res.socket.getPeerCertificate(false); + + if (!valid_from || !valid_to || !subjectaltname) { + throw { message: 'No TLS certificate in response' }; + } + + const valid = res.request.res.socket.authorized || false; + + const validTo = new Date(valid_to); + + const validFor = subjectaltname + .replace(/DNS:|IP Address:/g, "") + .split(", "); + + const daysRemaining = getDaysRemaining(new Date(), validTo); + + return { + valid, + validFor, + validTo, + daysRemaining, + issuer, + fingerprint, + }; +} \ No newline at end of file diff --git a/server/util.js b/server/util.js index 33b306b5c..081561bf9 100644 --- a/server/util.js +++ b/server/util.js @@ -18,3 +18,9 @@ exports.ucfirst = function (str) { return firstLetter.toUpperCase() + str.substr(1); } +exports.debug = (msg) => { + if (process.env.NODE_ENV === "development") { + console.log(msg) + } +} + diff --git a/src/components/Datetime.vue b/src/components/Datetime.vue index e84c877bc..3e5516597 100644 --- a/src/components/Datetime.vue +++ b/src/components/Datetime.vue @@ -14,12 +14,23 @@ dayjs.extend(relativeTime) export default { props: { value: String, + dateOnly: { + type: Boolean, + default: false, + }, }, computed: { displayText() { - let format = "YYYY-MM-DD HH:mm:ss"; - return dayjs.utc(this.value).tz(this.$root.timezone).format(format) + if (this.value !== undefined && this.value !== "") { + let format = "YYYY-MM-DD HH:mm:ss"; + if (this.dateOnly) { + format = "YYYY-MM-DD"; + } + return dayjs.utc(this.value).tz(this.$root.timezone).format(format); + } else { + return ""; + } }, } } diff --git a/src/icon.js b/src/icon.js new file mode 100644 index 000000000..02c5d8180 --- /dev/null +++ b/src/icon.js @@ -0,0 +1,12 @@ +import { library } from '@fortawesome/fontawesome-svg-core' +//import { fa } from '@fortawesome/free-regular-svg-icons' +import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome' +import { faCog, faTachometerAlt, faEdit, faPlus, faPause, faPlay, faTrash, faList } from '@fortawesome/free-solid-svg-icons' + +// Add Free Font Awesome Icons here +// https://fontawesome.com/v5.15/icons?d=gallery&p=2&s=solid&m=free +library.add(faCog, faTachometerAlt, faEdit, faPlus, faPause, faPlay, faTrash, faList) + +export { + FontAwesomeIcon +} diff --git a/src/layouts/Layout.vue b/src/layouts/Layout.vue index 85109e12b..bc99854ac 100644 --- a/src/layouts/Layout.vue +++ b/src/layouts/Layout.vue @@ -14,8 +14,8 @@ @@ -44,10 +44,27 @@
@@ -99,7 +116,7 @@ export default { box-shadow: 0 15px 47px 0 rgba(0, 0, 0, 0.05), 0 5px 14px 0 rgba(0, 0, 0, 0.05); text-align: center; white-space: nowrap; - padding: 0 35px; + padding: 0 10px; a { text-align: center; @@ -144,6 +161,7 @@ main { footer { color: #AAA; font-size: 13px; + margin-top: 10px; margin-bottom: 30px; margin-left: 10px; text-align: center; diff --git a/src/main.js b/src/main.js index a44bef87c..59672f8d6 100644 --- a/src/main.js +++ b/src/main.js @@ -15,6 +15,7 @@ import Toast from "vue-toastification"; import "vue-toastification/dist/index.css"; import "bootstrap" import Setup from "./pages/Setup.vue"; +import {FontAwesomeIcon} from "./icon.js" const routes = [ { @@ -88,5 +89,7 @@ const options = { app.use(Toast, options); +app.component('font-awesome-icon', FontAwesomeIcon) + app.mount('#app') diff --git a/src/mixins/socket.js b/src/mixins/socket.js index 0c4d68e15..f36a770e3 100644 --- a/src/mixins/socket.js +++ b/src/mixins/socket.js @@ -25,6 +25,7 @@ export default { importantHeartbeatList: { }, avgPingList: { }, uptimeList: { }, + certInfoList: {}, notificationList: [], windowWidth: window.innerWidth, showListMobile: false, @@ -58,7 +59,17 @@ export default { this.$router.push("/setup") }); - socket.on('monitorList', (data) => { + socket.on("monitorList", (data) => { + // Add Helper function + Object.entries(data).forEach(([monitorID, monitor]) => { + monitor.getUrl = () => { + try { + return new URL(monitor.url); + } catch (_) { + return null; + } + }; + }); this.monitorList = data; }); @@ -114,6 +125,10 @@ export default { this.uptimeList[`${monitorID}_${type}`] = data }); + socket.on('certInfo', (monitorID, data) => { + this.certInfoList[monitorID] = JSON.parse(data) + }); + socket.on('importantHeartbeatList', (monitorID, data) => { if (! (monitorID in this.importantHeartbeatList)) { this.importantHeartbeatList[monitorID] = data; diff --git a/src/pages/Dashboard.vue b/src/pages/Dashboard.vue index 7084c467a..d24d9fc07 100644 --- a/src/pages/Dashboard.vue +++ b/src/pages/Dashboard.vue @@ -4,7 +4,7 @@
- Add New Monitor + Add New Monitor
diff --git a/src/pages/Details.vue b/src/pages/Details.vue index f8c4879ad..b1f396132 100644 --- a/src/pages/Details.vue +++ b/src/pages/Details.vue @@ -11,10 +11,10 @@

- - - Edit - + + + Edit +
@@ -51,6 +51,46 @@

(30-day)

+ +
+

CertExp.

+

()

+ + {{certInfo.daysRemaining}} days + +
+
+
+ +
+
+
+

Certificate Info

+ + + + + + + + + + + + + + + + + + + + + + + +
Valid: {{ certInfo.valid }}
Valid To:
Days Remaining: {{ certInfo.daysRemaining }}
Issuer: {{ certInfo.issuer }}
Fingerprint: {{ certInfo.fingerprint }}
+
@@ -122,6 +162,7 @@ export default { page: 1, perPage: 25, heartBeatList: [], + toggleCertInfoBox: false, } }, computed: { @@ -180,6 +221,18 @@ export default { } }, + certInfo() { + if (this.$root.certInfoList[this.monitor.id]) { + return this.$root.certInfoList[this.monitor.id] + } else { + return null + } + }, + + showCertInfoBox() { + return this.certInfo != null && this.toggleCertInfoBox; + }, + displayedRecords() { const startIndex = this.perPage * (this.page - 1); const endIndex = startIndex + this.perPage; @@ -268,4 +321,12 @@ table { font-size: 13px; color: #AAA; } + +.stats { + padding: 10px; + + .col { + margin: 20px 0; + } +}