diff --git a/.do/deploy.template.yaml b/.do/deploy.template.yaml deleted file mode 100644 index 1023e85..0000000 --- a/.do/deploy.template.yaml +++ /dev/null @@ -1,8 +0,0 @@ -spec: - name: uptime-kuma - services: - - dockerfile_path: Dockerfile - git: - branch: main - repo_clone_url: https://github.com/philippdormann/uptime-kuma.git - name: uptime-kuma \ No newline at end of file diff --git a/.dockerignore b/.dockerignore index 618d711..df1f46a 100644 --- a/.dockerignore +++ b/.dockerignore @@ -17,6 +17,8 @@ README.md /.github package-lock.json app.json +CODE_OF_CONDUCT.md +CONTRIBUTING.md ### .gitignore content (commented rules are duplicated) diff --git a/.eslintrc.js b/.eslintrc.js index 41ad54b..fde74f6 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -16,6 +16,9 @@ module.exports = { requireConfigFile: false, }, rules: { + "camelcase": ["warn", { + "properties": "never" + }], // override/add rules settings here, such as: // 'vue/no-unused-vars': 'error' "no-unused-vars": "warn", @@ -36,6 +39,11 @@ module.exports = { "no-multi-spaces": ["error", { ignoreEOLComments: true, }], + "space-before-function-paren": ["error", { + "anonymous": "always", + "named": "never", + "asyncArrow": "always" + }], "curly": "error", "object-curly-spacing": ["error", "always"], "object-curly-newline": "off", @@ -62,12 +70,13 @@ module.exports = { exceptAfterSingleLine: true, }], "no-unneeded-ternary": "error", - "no-else-return": ["error", { - "allowElseIf": false, - }], "array-bracket-newline": ["error", "consistent"], "eol-last": ["error", "always"], //'prefer-template': 'error', "comma-dangle": ["warn", "only-multiline"], + "no-empty": ["error", { + "allowEmptyCatch": true + }], + "no-control-regex": "off" }, } diff --git a/.github/ISSUE_TEMPLATE/ask-for-help.md b/.github/ISSUE_TEMPLATE/ask-for-help.md index c365726..a89d942 100644 --- a/.github/ISSUE_TEMPLATE/ask-for-help.md +++ b/.github/ISSUE_TEMPLATE/ask-for-help.md @@ -6,5 +6,12 @@ labels: help assignees: '' --- +**Is it a duplicate question?** +Please search in Issues without filters: https://github.com/louislam/uptime-kuma/issues?q= +**Info** +Uptime Kuma Version: +Using Docker?: Yes/No +OS: +Browser: diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index cea1fc1..da89d15 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -7,6 +7,9 @@ assignees: '' --- +**Is it a duplicate question?** +Please search in Issues without filters: https://github.com/louislam/uptime-kuma/issues?q= + **Describe the bug** A clear and concise description of what the bug is. @@ -20,15 +23,16 @@ Steps to reproduce the behavior: **Expected behavior** A clear and concise description of what you expected to happen. + +**Info** +- Uptime Kuma Version: +- Using Docker?: Yes/No +- OS: +- Browser: + **Screenshots** If applicable, add screenshots to help explain your problem. -**Desktop (please complete the following information):** - - Uptime Kuma Version: - - Using Docker?: Yes/No - - OS: - - Browser: - +**Error Log** +It is easier for us to find out the problem. -**Additional context** -Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index 11fc491..9141130 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -6,6 +6,8 @@ labels: enhancement assignees: '' --- +**Is it a duplicate question?** +Please search in Issues without filters: https://github.com/louislam/uptime-kuma/issues?q= **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 [...] diff --git a/.stylelintrc b/.stylelintrc index 4d3d9d1..d981fe7 100644 --- a/.stylelintrc +++ b/.stylelintrc @@ -1,3 +1,10 @@ { - "extends": "stylelint-config-recommended", + "extends": "stylelint-config-standard", + "rules": { + "indentation": 4, + "no-descending-specificity": null, + "selector-list-comma-newline-after": null, + "declaration-empty-line-before": null, + "no-duplicate-selectors": null + } } diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b3308df..5bbf343 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -6,7 +6,38 @@ The project was created with vite.js (vue3). Then I created a sub-directory call The frontend code build into "dist" directory. The server uses "dist" as root. This is how production is working. -Your IDE should follow the config in ".editorconfig". The most special thing is I set it to 4 spaces indentation. I know 2 spaces indentation became a kind of standard nowadays for js, but my eyes is not so comfortable for this. In my opinion, there is no callback-hell nowadays, it is good to go back 4 spaces world again. +# Can I create a pull request for Uptime Kuma? + +Generally, if the pull request is working fine and it do not affect any existing logic, workflow and perfomance, I will merge to the master branch once it is tested. + +If you are not sure, feel free to create an empty pull request draft first. + +## Pull Request Examples + +### ✅ High - Medium Priority + +- Add a new notification +- Add a chart +- Fix a bug + +### *️⃣ Requires one more reviewer + +I do not have such knowledge to test it + +- Add k8s supports + +### *️⃣ Low Priority + +It chnaged my current workflow and require further studies. + +- Change my release approach + +### ❌ Won't Merge + +- Duplicated pull request +- Buggy +- Existing logic is completely modified or deleted +- A function that is completely out of scope # Project Styles @@ -19,16 +50,27 @@ For example, recently, because I am not a python expert, I spent a 2 hours to re - All settings in frontend. - Easy to use +# Coding Styles + +- Follow .editorconfig +- Follow eslint + +## Name convention + +- Javascript/Typescript: camelCaseType +- SQLite: underscore_type +- CSS/SCSS: dash-type + # Tools - Node.js >= 14 - Git -- IDE that supports .editorconfig (I am using Intellji Idea) +- IDE that supports .editorconfig and eslint (I am using Intellji Idea) - A SQLite tool (I am using SQLite Expert Personal) -# Prepare the dev +# Install dependencies ```bash -npm install +npm install --dev ``` # Backend Dev @@ -39,7 +81,6 @@ npm run start-server # Or node server/server.js - ``` It binds to 0.0.0.0:3001 by default. @@ -92,7 +133,8 @@ The data and socket logic in "src/mixins/socket.js" # Database Migration -TODO +1. create `patch{num}.sql` in `./db/` +1. update `latestVersion` in `./server/database.js` # Unit Test diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..47f0786 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,14 @@ +# Security Policy + +## Supported Versions + +Use this section to tell people about which versions of your project are +currently being supported with security updates. + +| Version | Supported | +| ------- | ------------------ | +| 1.x.x | :white_check_mark: | + +## Reporting a Vulnerability + +https://github.com/louislam/uptime-kuma/issues diff --git a/app.json b/app.json deleted file mode 100644 index ab64321..0000000 --- a/app.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "name": "Uptime Kuma", - "description": "A fancy self-hosted monitoring tool", - "repository": "https://github.com/louislam/uptime-kuma", - "logo": "https://raw.githubusercontent.com/louislam/uptime-kuma/master/public/icon.png", - "keywords": ["node", "express", "socket-io", "uptime-kuma", "uptime"] -} diff --git a/db/patch5.sql b/db/patch5.sql new file mode 100644 index 0000000..5730b2d --- /dev/null +++ b/db/patch5.sql @@ -0,0 +1,70 @@ +-- You should not modify if this have pushed to Github, unless it does serious wrong with the db. +PRAGMA foreign_keys = off; + +BEGIN TRANSACTION; + +create table monitor_dg_tmp ( + id INTEGER not null primary key autoincrement, + name VARCHAR(150), + active BOOLEAN default 1 not null, + user_id INTEGER references user on update cascade on delete + set + null, + interval INTEGER default 20 not null, + url TEXT, + type VARCHAR(20), + weight INTEGER default 2000, + hostname VARCHAR(255), + port INTEGER, + created_date DATETIME default (DATETIME('now')) not null, + keyword VARCHAR(255), + maxretries INTEGER NOT NULL DEFAULT 0, + ignore_tls BOOLEAN default 0 not null, + upside_down BOOLEAN default 0 not null +); + +insert into + monitor_dg_tmp( + id, + name, + active, + user_id, + interval, + url, + type, + weight, + hostname, + port, + keyword, + maxretries, + ignore_tls, + upside_down + ) +select + id, + name, + active, + user_id, + interval, + url, + type, + weight, + hostname, + port, + keyword, + maxretries, + ignore_tls, + upside_down +from + monitor; + +drop table monitor; + +alter table + monitor_dg_tmp rename to monitor; + +create index user_id on monitor (user_id); + +COMMIT; + +PRAGMA foreign_keys = on; diff --git a/db/patch6.sql b/db/patch6.sql new file mode 100644 index 0000000..4f539a2 --- /dev/null +++ b/db/patch6.sql @@ -0,0 +1,74 @@ +-- You should not modify if this have pushed to Github, unless it does serious wrong with the db. +PRAGMA foreign_keys = off; + +BEGIN TRANSACTION; + +create table monitor_dg_tmp ( + id INTEGER not null primary key autoincrement, + name VARCHAR(150), + active BOOLEAN default 1 not null, + user_id INTEGER references user on update cascade on delete + set + null, + interval INTEGER default 20 not null, + url TEXT, + type VARCHAR(20), + weight INTEGER default 2000, + hostname VARCHAR(255), + port INTEGER, + created_date DATETIME default (DATETIME('now')) not null, + keyword VARCHAR(255), + maxretries INTEGER NOT NULL DEFAULT 0, + ignore_tls BOOLEAN default 0 not null, + upside_down BOOLEAN default 0 not null, + maxredirects INTEGER default 10 not null, + accepted_statuscodes_json TEXT default '["200-299"]' not null +); + +insert into + monitor_dg_tmp( + id, + name, + active, + user_id, + interval, + url, + type, + weight, + hostname, + port, + created_date, + keyword, + maxretries, + ignore_tls, + upside_down + ) +select + id, + name, + active, + user_id, + interval, + url, + type, + weight, + hostname, + port, + created_date, + keyword, + maxretries, + ignore_tls, + upside_down +from + monitor; + +drop table monitor; + +alter table + monitor_dg_tmp rename to monitor; + +create index user_id on monitor (user_id); + +COMMIT; + +PRAGMA foreign_keys = on; diff --git a/extra/compile-install-script.ps1 b/extra/compile-install-script.ps1 new file mode 100644 index 0000000..dd44798 --- /dev/null +++ b/extra/compile-install-script.ps1 @@ -0,0 +1,2 @@ +# Must enable File Sharing in Docker Desktop +docker run -it --rm -v ${pwd}:/app louislam/batsh /usr/bin/batsh bash --output ./install.sh ./extra/install.batsh diff --git a/extra/healthcheck.js b/extra/healthcheck.js index b547fbc..c0b33b6 100644 --- a/extra/healthcheck.js +++ b/extra/healthcheck.js @@ -1,19 +1,19 @@ -var http = require("http"); -var options = { - host: "localhost", - port: "3001", - timeout: 2000, +let http = require("http"); +let options = { + host: "localhost", + port: "3001", + timeout: 2000, }; -var request = http.request(options, (res) => { - console.log(`STATUS: ${res.statusCode}`); - if (res.statusCode == 200) { - process.exit(0); - } else { - process.exit(1); - } +let request = http.request(options, (res) => { + console.log(`STATUS: ${res.statusCode}`); + if (res.statusCode == 200) { + process.exit(0); + } else { + process.exit(1); + } }); request.on("error", function (err) { - console.log("ERROR"); - process.exit(1); + console.log("ERROR"); + process.exit(1); }); request.end(); diff --git a/extra/install.batsh b/extra/install.batsh new file mode 100644 index 0000000..cf14d0a --- /dev/null +++ b/extra/install.batsh @@ -0,0 +1,245 @@ +// install.sh is generated by ./extra/install.batsh, do not modify it directly. +// "npm run compile-install-script" to compile install.sh +// The command is working on Windows PowerShell and Docker for Windows only. + + +// curl -o kuma_install.sh https://raw.githubusercontent.com/louislam/uptime-kuma/master/install.sh && sudo bash kuma_install.sh +println("====================="); +println("Uptime Kuma Installer"); +println("====================="); +println("Supported OS: CentOS 7/8, Ubuntu >= 16.04 and Debian"); +println("---------------------------------------"); +println("This script is designed for Linux and basic usage."); +println("For advanced usage, please go to https://github.com/louislam/uptime-kuma/wiki/Installation"); +println("---------------------------------------"); +println(""); +println("Local - Install Uptime Kuma in your current machine with git, Node.js 14 and pm2"); +println("Docker - Install Uptime Kuma Docker container"); +println(""); + +if ("$1" != "") { + type = "$1"; +} else { + call("read", "-p", "Which installation method do you prefer? [DOCKER/local]: ", "type"); +} + +defaultPort = "3001"; + +function checkNode() { + bash("nodeVersion=$(node -e 'console.log(process.versions.node.split(`.`)[0])')"); + println("Node Version: " ++ nodeVersion); + + if (nodeVersion < "12") { + println("Error: Required Node.js 14"); + call("exit", "1"); + } + + if (nodeVersion == "12") { + println("Warning: NodeJS " ++ nodeVersion ++ " is not tested."); + } +} + +function deb() { + bash("nodeCheck=$(node -v)"); + bash("apt --yes update"); + + if (nodeCheck != "") { + checkNode(); + } else { + + // Old nodejs binary name is "nodejs" + bash("check=$(nodejs --version)"); + if (check != "") { + println("Error: 'node' command is not found, but 'nodejs' command is found. Your NodeJS should be too old."); + bash("exit 1"); + } + + bash("curlCheck=$(curl --version)"); + if (curlCheck == "") { + println("Installing Curl"); + bash("apt --yes install curl"); + } + + println("Installing Node.js 14"); + bash("curl -sL https://deb.nodesource.com/setup_14.x | bash - > log.txt"); + bash("apt --yes install nodejs"); + bash("node -v"); + + bash("nodeCheckAgain=$(node -v)"); + + if (nodeCheckAgain == "") { + println("Error during Node.js installation"); + bash("exit 1"); + } + } + + bash("check=$(git --version)"); + if (check == "") { + println("Installing Git"); + bash("apt --yes install git"); + } +} + +if (type == "local") { + defaultInstallPath = "/opt/uptime-kuma"; + + if (exists("/etc/redhat-release")) { + os = call("cat", "/etc/redhat-release"); + distribution = "rhel"; + + } else if (exists("/etc/issue")) { + bash("os=$(head -n1 /etc/issue | cut -f 1 -d ' ')"); + if (os == "Ubuntu") { + distribution = "ubuntu"; + } + if (os == "Debian") { + distribution = "debian"; + } + } + + bash("arch=$(uname -i)"); + + println("Your OS: " ++ os); + println("Distribution: " ++ distribution); + println("Arch: " ++ arch); + + if ("$3" != "") { + port = "$3"; + } else { + call("read", "-p", "Listening Port [$defaultPort]: ", "port"); + + if (port == "") { + port = defaultPort; + } + } + + if ("$2" != "") { + installPath = "$2"; + } else { + call("read", "-p", "Installation Path [$defaultInstallPath]: ", "installPath"); + + if (installPath == "") { + installPath = defaultInstallPath; + } + } + + // CentOS + if (distribution == "rhel") { + bash("nodeCheck=$(node -v)"); + + if (nodeCheck != "") { + checkNode(); + } else { + + bash("curlCheck=$(curl --version)"); + if (curlCheck == "") { + println("Installing Curl"); + bash("yum -y -q install curl"); + } + + println("Installing Node.js 14"); + bash("curl -sL https://rpm.nodesource.com/setup_14.x | bash - > log.txt"); + bash("yum install -y -q nodejs"); + bash("node -v"); + + bash("nodeCheckAgain=$(node -v)"); + + if (nodeCheckAgain == "") { + println("Error during Node.js installation"); + bash("exit 1"); + } + } + + bash("check=$(git --version)"); + if (check == "") { + println("Installing Git"); + bash("yum -y -q install git"); + } + + // Ubuntu + } else if (distribution == "ubuntu") { + deb(); + + // Debian + } else if (distribution == "debian") { + deb(); + + } else { + // Unknown distribution + error = 0; + + bash("check=$(git --version)"); + if (check == "") { + error = 1; + println("Error: git is missing"); + } + + bash("check=$(node -v)"); + if (check == "") { + error = 1; + println("Error: node is missing"); + } + + if (error > 0) { + println("Please install above missing software"); + bash("exit 1"); + } + } + + bash("check=$(pm2 --version)"); + if (check == "") { + println("Installing PM2"); + bash("npm install pm2 -g"); + bash("pm2 startup"); + } + + bash("mkdir -p $installPath"); + bash("cd $installPath"); + bash("git clone https://github.com/louislam/uptime-kuma.git ."); + bash("npm run setup"); + + bash("pm2 start npm --name uptime-kuma -- run start-server -- --port=$port"); + +} else { + defaultVolume = "uptime-kuma"; + + bash("check=$(docker -v)"); + if (check == "") { + println("Error: docker is not found!"); + bash("exit 1"); + } + + bash("check=$(docker info)"); + + bash("if [[ \"$check\" == *\"Is the docker daemon running\"* ]]; then + echo \"Error: docker is not running\" + exit 1 + fi"); + + if ("$3" != "") { + port = "$3"; + } else { + call("read", "-p", "Expose Port [$defaultPort]: ", "port"); + + if (port == "") { + port = defaultPort; + } + } + + if ("$2" != "") { + volume = "$2"; + } else { + call("read", "-p", "Volume Name [$defaultVolume]: ", "volume"); + + if (volume == "") { + volume = defaultVolume; + } + } + + println("Port: $port"); + println("Volume: $volume"); + bash("docker volume create $volume"); + bash("docker run -d --restart=always -p $port:3001 -v $volume:/app/data --name uptime-kuma louislam/uptime-kuma:1"); +} + +println("http://localhost:$port"); diff --git a/extra/mark-as-nightly.js b/extra/mark-as-nightly.js index 2849651..0316596 100644 --- a/extra/mark-as-nightly.js +++ b/extra/mark-as-nightly.js @@ -1,25 +1,9 @@ -/** - * String.prototype.replaceAll() polyfill - * https://gomakethings.com/how-to-replace-a-section-of-a-string-with-another-one-with-vanilla-js/ - * @author Chris Ferdinandi - * @license MIT - */ -if (!String.prototype.replaceAll) { - String.prototype.replaceAll = function(str, newStr){ - - // If a regex pattern - if (Object.prototype.toString.call(str).toLowerCase() === '[object regexp]') { - return this.replace(str, newStr); - } +const pkg = require("../package.json"); +const fs = require("fs"); +const util = require("../src/util"); - // If a string - return this.replace(new RegExp(str, 'g'), newStr); +util.polyfill(); - }; -} - -const pkg = require('../package.json'); -const fs = require("fs"); const oldVersion = pkg.version const newVersion = oldVersion + "-nightly" @@ -35,6 +19,6 @@ if (newVersion) { // Process README.md if (fs.existsSync("README.md")) { - fs.writeFileSync("README.md", fs.readFileSync("README.md", 'utf8').replaceAll(oldVersion, newVersion)) + fs.writeFileSync("README.md", fs.readFileSync("README.md", "utf8").replaceAll(oldVersion, newVersion)) } } diff --git a/extra/reset-password.js b/extra/reset-password.js new file mode 100644 index 0000000..b849848 --- /dev/null +++ b/extra/reset-password.js @@ -0,0 +1,59 @@ +console.log("== Uptime Kuma Reset Password Tool =="); + +console.log("Loading the database"); + +const Database = require("../server/database"); +const { R } = require("redbean-node"); +const readline = require("readline"); +const { initJWTSecret } = require("../server/util-server"); +const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout +}); + +(async () => { + await Database.connect(); + + try { + const user = await R.findOne("user"); + + if (! user) { + throw new Error("user not found, have you installed?"); + } + + console.log("Found user: " + user.username); + + while (true) { + let password = await question("New Password: "); + let confirmPassword = await question("Confirm New Password: "); + + if (password === confirmPassword) { + await user.resetPassword(password); + + // Reset all sessions by reset jwt secret + await initJWTSecret(); + + rl.close(); + break; + } else { + console.log("Passwords do not match, please try again."); + } + } + + console.log("Password reset successfully."); + } catch (e) { + console.error("Error: " + e.message); + } + + await Database.close(); + + console.log("Finished. You should restart the Uptime Kuma server.") +})(); + +function question(question) { + return new Promise((resolve) => { + rl.question(question, (answer) => { + resolve(answer); + }) + }); +} diff --git a/extra/update-version.js b/extra/update-version.js new file mode 100644 index 0000000..697a640 --- /dev/null +++ b/extra/update-version.js @@ -0,0 +1,59 @@ +const pkg = require("../package.json"); +const fs = require("fs"); +const child_process = require("child_process"); +const util = require("../src/util"); + +util.polyfill(); + +const oldVersion = pkg.version; +const newVersion = process.argv[2]; + +console.log("Old Version: " + oldVersion); +console.log("New Version: " + newVersion); + +if (! newVersion) { + console.error("invalid version"); + process.exit(1); +} + +const exists = tagExists(newVersion); + +if (! exists) { + // Process package.json + pkg.version = newVersion; + pkg.scripts.setup = pkg.scripts.setup.replaceAll(oldVersion, newVersion); + pkg.scripts["build-docker"] = pkg.scripts["build-docker"].replaceAll(oldVersion, newVersion); + fs.writeFileSync("package.json", JSON.stringify(pkg, null, 4) + "\n"); + + commit(newVersion); + tag(newVersion); +} else { + console.log("version exists") +} + +function commit(version) { + let msg = "update to " + version; + + let res = child_process.spawnSync("git", ["commit", "-m", msg, "-a"]); + let stdout = res.stdout.toString().trim(); + console.log(stdout) + + if (stdout.includes("no changes added to commit")) { + throw new Error("commit error") + } +} + +function tag(version) { + let res = child_process.spawnSync("git", ["tag", version]); + console.log(res.stdout.toString().trim()) +} + +function tagExists(version) { + if (! version) { + throw new Error("invalid version"); + } + + let res = child_process.spawnSync("git", ["tag", "-l", version]); + + return res.stdout.toString().trim() === version; +} diff --git a/extra/version-global-replace.js b/extra/version-global-replace.js deleted file mode 100644 index bf91865..0000000 --- a/extra/version-global-replace.js +++ /dev/null @@ -1,39 +0,0 @@ -/** - * String.prototype.replaceAll() polyfill - * https://gomakethings.com/how-to-replace-a-section-of-a-string-with-another-one-with-vanilla-js/ - * @author Chris Ferdinandi - * @license MIT - */ -if (!String.prototype.replaceAll) { - String.prototype.replaceAll = function(str, newStr){ - - // If a regex pattern - if (Object.prototype.toString.call(str).toLowerCase() === '[object regexp]') { - return this.replace(str, newStr); - } - - // If a string - return this.replace(new RegExp(str, 'g'), newStr); - - }; -} - -const pkg = require('../package.json'); -const fs = require("fs"); -const oldVersion = pkg.version -const newVersion = process.argv[2] - -console.log("Old Version: " + oldVersion) -console.log("New Version: " + newVersion) - -if (newVersion) { - // Process package.json - pkg.version = newVersion - pkg.scripts.setup = pkg.scripts.setup.replaceAll(oldVersion, newVersion) - pkg.scripts["build-docker"] = pkg.scripts["build-docker"].replaceAll(oldVersion, newVersion) - fs.writeFileSync("package.json", JSON.stringify(pkg, null, 4) + "\n") - - // Process README.md - fs.writeFileSync("README.md", fs.readFileSync("README.md", 'utf8').replaceAll(oldVersion, newVersion)) -} - diff --git a/index.html b/index.html index 66d58c1..61d0f42 100644 --- a/index.html +++ b/index.html @@ -1,16 +1,16 @@ - + - - - + + + Uptime Kuma - - -
- - + + +
+ + diff --git a/install.sh b/install.sh new file mode 100644 index 0000000..a840bb2 --- /dev/null +++ b/install.sh @@ -0,0 +1,203 @@ +# install.sh is generated by ./extra/install.batsh, do not modify it directly. +# "npm run compile-install-script" to compile install.sh +# The command is working on Windows PowerShell and Docker for Windows only. +# curl -o kuma_install.sh https://raw.githubusercontent.com/louislam/uptime-kuma/master/install.sh && sudo bash kuma_install.sh +"echo" "-e" "=====================" +"echo" "-e" "Uptime Kuma Installer" +"echo" "-e" "=====================" +"echo" "-e" "Supported OS: CentOS 7/8, Ubuntu >= 16.04 and Debian" +"echo" "-e" "---------------------------------------" +"echo" "-e" "This script is designed for Linux and basic usage." +"echo" "-e" "For advanced usage, please go to https://github.com/louislam/uptime-kuma/wiki/Installation" +"echo" "-e" "---------------------------------------" +"echo" "-e" "" +"echo" "-e" "Local - Install Uptime Kuma in your current machine with git, Node.js 14 and pm2" +"echo" "-e" "Docker - Install Uptime Kuma Docker container" +"echo" "-e" "" +if [ "$1" != "" ]; then + type="$1" +else + "read" "-p" "Which installation method do you prefer? [DOCKER/local]: " "type" +fi +defaultPort="3001" +function checkNode { + local _0 + nodeVersion=$(node -e 'console.log(process.versions.node.split(`.`)[0])') + "echo" "-e" "Node Version: ""$nodeVersion" + _0="12" + if [ $(($nodeVersion < $_0)) == 1 ]; then + "echo" "-e" "Error: Required Node.js 14" + "exit" "1" +fi + if [ "$nodeVersion" == "12" ]; then + "echo" "-e" "Warning: NodeJS ""$nodeVersion"" is not tested." +fi +} +function deb { + nodeCheck=$(node -v) + apt --yes update + if [ "$nodeCheck" != "" ]; then + "checkNode" + else + # Old nodejs binary name is "nodejs" + check=$(nodejs --version) + if [ "$check" != "" ]; then + "echo" "-e" "Error: 'node' command is not found, but 'nodejs' command is found. Your NodeJS should be too old." + exit 1 +fi + curlCheck=$(curl --version) + if [ "$curlCheck" == "" ]; then + "echo" "-e" "Installing Curl" + apt --yes install curl +fi + "echo" "-e" "Installing Node.js 14" + curl -sL https://deb.nodesource.com/setup_14.x | bash - > log.txt + apt --yes install nodejs + node -v + nodeCheckAgain=$(node -v) + if [ "$nodeCheckAgain" == "" ]; then + "echo" "-e" "Error during Node.js installation" + exit 1 +fi + fi + check=$(git --version) + if [ "$check" == "" ]; then + "echo" "-e" "Installing Git" + apt --yes install git +fi +} +if [ "$type" == "local" ]; then + defaultInstallPath="/opt/uptime-kuma" + if [ -e "/etc/redhat-release" ]; then + os=$("cat" "/etc/redhat-release") + distribution="rhel" + else + if [ -e "/etc/issue" ]; then + os=$(head -n1 /etc/issue | cut -f 1 -d ' ') + if [ "$os" == "Ubuntu" ]; then + distribution="ubuntu" +fi + if [ "$os" == "Debian" ]; then + distribution="debian" +fi +fi + fi + arch=$(uname -i) + "echo" "-e" "Your OS: ""$os" + "echo" "-e" "Distribution: ""$distribution" + "echo" "-e" "Arch: ""$arch" + if [ "$3" != "" ]; then + port="$3" + else + "read" "-p" "Listening Port [$defaultPort]: " "port" + if [ "$port" == "" ]; then + port="$defaultPort" +fi + fi + if [ "$2" != "" ]; then + installPath="$2" + else + "read" "-p" "Installation Path [$defaultInstallPath]: " "installPath" + if [ "$installPath" == "" ]; then + installPath="$defaultInstallPath" +fi + fi + # CentOS + if [ "$distribution" == "rhel" ]; then + nodeCheck=$(node -v) + if [ "$nodeCheck" != "" ]; then + "checkNode" + else + curlCheck=$(curl --version) + if [ "$curlCheck" == "" ]; then + "echo" "-e" "Installing Curl" + yum -y -q install curl +fi + "echo" "-e" "Installing Node.js 14" + curl -sL https://rpm.nodesource.com/setup_14.x | bash - > log.txt + yum install -y -q nodejs + node -v + nodeCheckAgain=$(node -v) + if [ "$nodeCheckAgain" == "" ]; then + "echo" "-e" "Error during Node.js installation" + exit 1 +fi + fi + check=$(git --version) + if [ "$check" == "" ]; then + "echo" "-e" "Installing Git" + yum -y -q install git +fi + # Ubuntu + else + if [ "$distribution" == "ubuntu" ]; then + "deb" + # Debian + else + if [ "$distribution" == "debian" ]; then + "deb" + else + # Unknown distribution + error=$((0)) + check=$(git --version) + if [ "$check" == "" ]; then + error=$((1)) + "echo" "-e" "Error: git is missing" +fi + check=$(node -v) + if [ "$check" == "" ]; then + error=$((1)) + "echo" "-e" "Error: node is missing" +fi + if [ $(($error > 0)) == 1 ]; then + "echo" "-e" "Please install above missing software" + exit 1 +fi + fi + fi + fi + check=$(pm2 --version) + if [ "$check" == "" ]; then + "echo" "-e" "Installing PM2" + npm install pm2 -g + pm2 startup +fi + mkdir -p $installPath + cd $installPath + git clone https://github.com/louislam/uptime-kuma.git . + npm run setup + pm2 start npm --name uptime-kuma -- run start-server -- --port=$port +else + defaultVolume="uptime-kuma" + check=$(docker -v) + if [ "$check" == "" ]; then + "echo" "-e" "Error: docker is not found!" + exit 1 +fi + check=$(docker info) + if [[ "$check" == *"Is the docker daemon running"* ]]; then + echo "Error: docker is not running" + exit 1 + fi + if [ "$3" != "" ]; then + port="$3" + else + "read" "-p" "Expose Port [$defaultPort]: " "port" + if [ "$port" == "" ]; then + port="$defaultPort" +fi + fi + if [ "$2" != "" ]; then + volume="$2" + else + "read" "-p" "Volume Name [$defaultVolume]: " "volume" + if [ "$volume" == "" ]; then + volume="$defaultVolume" +fi + fi + "echo" "-e" "Port: $port" + "echo" "-e" "Volume: $volume" + docker volume create $volume + docker run -d --restart=always -p $port:3001 -v $volume:/app/data --name uptime-kuma louislam/uptime-kuma:1 +fi +"echo" "-e" "http://localhost:$port" diff --git a/kubernetes/README.md b/kubernetes/README.md new file mode 100644 index 0000000..26ab2f6 --- /dev/null +++ b/kubernetes/README.md @@ -0,0 +1,28 @@ +# Uptime-Kuma K8s Deployment +## How does it work? + +Kustomize is a tool which builds a complete deployment file for all config elements. +You can edit the files in the ```uptime-kuma``` folder except the ```kustomization.yml``` until you know what you're doing. +If you want to choose another namespace you can edit the ```kustomization.yml``` in the ```kubernetes```-Folder and change the ```namespace: uptime-kuma``` to something you like. + +It creates a certificate with the specified Issuer and creates the Ingress for the Uptime-Kuma ClusterIP-Service + +## What do i have to edit? +You have to edit the ```ingressroute.yml``` to your needs. +This ingressroute.yml is for the [nginx-ingress-controller](https://kubernetes.github.io/ingress-nginx/) in combination with the [cert-manager](https://cert-manager.io/). + +- host +- secrets and secret names +- (Cluster)Issuer (optional) +- the Version in the Deployment-File + - update: + - change to newer version and run the above commands, it will update the pods one after another + +## How To use: + +- install [kustomize](https://kubectl.docs.kubernetes.io/installation/kustomize/) +- Edit files mentioned above to your needs +- run ```kustomize build > apply.yml``` +- run ```kubectl apply -f apply.yml``` + +Now you should see some k8s magic and Uptime-Kuma should be available at the specified address. \ No newline at end of file diff --git a/kubernetes/kustomization.yml b/kubernetes/kustomization.yml new file mode 100644 index 0000000..0daf10f --- /dev/null +++ b/kubernetes/kustomization.yml @@ -0,0 +1,10 @@ +namespace: uptime-kuma +namePrefix: uptime-kuma- + +commonLabels: + app: uptime-kuma + +bases: + - uptime-kuma + + diff --git a/kubernetes/uptime-kuma/deployment.yml b/kubernetes/uptime-kuma/deployment.yml new file mode 100644 index 0000000..a122509 --- /dev/null +++ b/kubernetes/uptime-kuma/deployment.yml @@ -0,0 +1,42 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + component: uptime-kuma + name: deployment +spec: + selector: + matchLabels: + component: uptime-kuma + replicas: 1 + strategy: + type: Recreate + + template: + metadata: + labels: + component: uptime-kuma + spec: + containers: + - name: app + image: louislam/uptime-kuma:1 + ports: + - containerPort: 3001 + volumeMounts: + - mountPath: /app/data + name: storage + livenessProbe: + exec: + command: + - node + - extra/healthcheck.js + readinessProbe: + httpGet: + path: / + port: 3001 + scheme: HTTP + + volumes: + - name: storage + persistentVolumeClaim: + claimName: pvc diff --git a/kubernetes/uptime-kuma/ingressroute.yml b/kubernetes/uptime-kuma/ingressroute.yml new file mode 100644 index 0000000..71f7027 --- /dev/null +++ b/kubernetes/uptime-kuma/ingressroute.yml @@ -0,0 +1,39 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + annotations: + kubernetes.io/ingress.class: nginx + cert-manager.io/cluster-issuer: letsencrypt-prod + nginx.ingress.kubernetes.io/proxy-read-timeout: "3600" + nginx.ingress.kubernetes.io/proxy-send-timeout: "3600" + nginx.ingress.kubernetes.io/server-snippets: | + location / { + proxy_set_header Upgrade $http_upgrade; + proxy_http_version 1.1; + proxy_set_header X-Forwarded-Host $http_host; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-For $remote_addr; + proxy_set_header Host $host; + proxy_set_header Connection "upgrade"; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header Upgrade $http_upgrade; + proxy_cache_bypass $http_upgrade; + } + name: ingress +spec: + tls: + - hosts: + - example.com + secretName: example-com-tls + rules: + - host: example.com + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: service + port: + number: 3001 diff --git a/kubernetes/uptime-kuma/kustomization.yml b/kubernetes/uptime-kuma/kustomization.yml new file mode 100644 index 0000000..638a2ab --- /dev/null +++ b/kubernetes/uptime-kuma/kustomization.yml @@ -0,0 +1,5 @@ +resources: + - deployment.yml + - service.yml + - ingressroute.yml + - pvc.yml \ No newline at end of file diff --git a/kubernetes/uptime-kuma/pvc.yml b/kubernetes/uptime-kuma/pvc.yml new file mode 100644 index 0000000..eda3b8b --- /dev/null +++ b/kubernetes/uptime-kuma/pvc.yml @@ -0,0 +1,10 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: pvc +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 4Gi diff --git a/kubernetes/uptime-kuma/service.yml b/kubernetes/uptime-kuma/service.yml new file mode 100644 index 0000000..5fa812e --- /dev/null +++ b/kubernetes/uptime-kuma/service.yml @@ -0,0 +1,13 @@ +apiVersion: v1 +kind: Service +metadata: + name: service +spec: + selector: + component: uptime-kuma + type: ClusterIP + ports: + - name: http + port: 3001 + targetPort: 3001 + protocol: TCP diff --git a/public/apple-touch-icon-precomposed.png b/public/apple-touch-icon-precomposed.png new file mode 100644 index 0000000..8dae0df Binary files /dev/null and b/public/apple-touch-icon-precomposed.png differ diff --git a/public/apple-touch-icon.png b/public/apple-touch-icon.png index 0e9c109..f3c5854 100644 Binary files a/public/apple-touch-icon.png and b/public/apple-touch-icon.png differ diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000..dedf0cd Binary files /dev/null and b/public/favicon.ico differ diff --git a/public/robots.txt b/public/robots.txt deleted file mode 100644 index e9e57dc..0000000 --- a/public/robots.txt +++ /dev/null @@ -1,3 +0,0 @@ -# https://www.robotstxt.org/robotstxt.html -User-agent: * -Disallow: diff --git a/server/check-version.js b/server/check-version.js new file mode 100644 index 0000000..96e8aec --- /dev/null +++ b/server/check-version.js @@ -0,0 +1,44 @@ +const { setSetting } = require("./util-server"); +const axios = require("axios"); +const { isDev } = require("../src/util"); + +exports.version = require("../package.json").version; +exports.latestVersion = null; + +let interval; + +exports.startInterval = () => { + let check = async () => { + try { + const res = await axios.get("https://raw.githubusercontent.com/louislam/uptime-kuma/master/package.json"); + + if (typeof res.data === "string") { + res.data = JSON.parse(res.data); + } + + // For debug + if (process.env.TEST_CHECK_VERSION === "1") { + res.data.version = "1000.0.0" + } + + exports.latestVersion = res.data.version; + console.log("Latest Version: " + exports.latestVersion); + } catch (_) { } + + }; + + check(); + interval = setInterval(check, 3600 * 1000 * 48); +}; + +exports.enableCheckUpdate = async (value) => { + await setSetting("checkUpdate", value); + + clearInterval(interval); + + if (value) { + exports.startInterval(); + } +}; + +exports.socket = null; diff --git a/server/database.js b/server/database.js index 04f764e..9b83fe7 100644 --- a/server/database.js +++ b/server/database.js @@ -1,16 +1,45 @@ const fs = require("fs"); -const { sleep } = require("../src/util"); const { R } = require("redbean-node"); -const { - setSetting, setting, -} = require("./util-server"); +const { setSetting, setting } = require("./util-server"); class Database { static templatePath = "./db/kuma.db" static path = "./data/kuma.db"; - static latestVersion = 4; + static latestVersion = 6; static noReject = true; + static sqliteInstance = null; + + static async connect() { + const acquireConnectionTimeout = 120 * 1000; + + R.useBetterSQLite3 = true; + R.betterSQLite3Options.timeout = acquireConnectionTimeout; + + R.setup("sqlite", { + filename: Database.path, + useNullAsDefault: true, + acquireConnectionTimeout: acquireConnectionTimeout, + }, { + min: 1, + max: 1, + idleTimeoutMillis: 120 * 1000, + propagateCreateError: false, + acquireTimeoutMillis: acquireConnectionTimeout, + }); + + if (process.env.SQL_LOG === "1") { + R.debug(true); + } + + // Auto map the model to a bean object + R.freeze(true) + await R.autoloadModels("./server/model"); + + // Change to WAL + await R.exec("PRAGMA journal_mode = WAL"); + console.log(await R.getAll("PRAGMA journal_mode")); + } static async patch() { let version = parseInt(await setting("database_version")); @@ -24,6 +53,8 @@ class Database { if (version === this.latestVersion) { console.info("Database no need to patch"); + } else if (version > this.latestVersion) { + console.info("Warning: Database version is newer than expected"); } else { console.info("Database patch is needed") @@ -31,6 +62,16 @@ class Database { const backupPath = "./data/kuma.db.bak" + version; fs.copyFileSync(Database.path, backupPath); + const shmPath = Database.path + "-shm"; + if (fs.existsSync(shmPath)) { + fs.copyFileSync(shmPath, shmPath + ".bak" + version); + } + + const walPath = Database.path + "-wal"; + if (fs.existsSync(walPath)) { + fs.copyFileSync(walPath, walPath + ".bak" + version); + } + // Try catch anything here, if gone wrong, restore the backup try { for (let i = version + 1; i <= this.latestVersion; i++) { @@ -83,37 +124,27 @@ class Database { return statement !== ""; }) + // Use better-sqlite3 to run, prevent "This statement does not return data. Use run() instead" + const db = await this.getBetterSQLite3Database(); + for (let statement of statements) { - await R.exec(statement); + db.prepare(statement).run(); } } + static getBetterSQLite3Database() { + return R.knex.client.acquireConnection(); + } + /** * Special handle, because tarn.js throw a promise reject that cannot be caught * @returns {Promise} */ static async close() { - const listener = (reason, p) => { - Database.noReject = false; - }; - process.addListener("unhandledRejection", listener); - - console.log("Closing DB") - - while (true) { - Database.noReject = true; - await R.close() - await sleep(2000) - - if (Database.noReject) { - break; - } else { - console.log("Waiting to close the db") - } + if (this.sqliteInstance) { + this.sqliteInstance.close(); } - console.log("SQLite closed") - - process.removeListener("unhandledRejection", listener); + console.log("Stopped database"); } } diff --git a/server/model/monitor.js b/server/model/monitor.js index 49fcfb3..17ab277 100644 --- a/server/model/monitor.js +++ b/server/model/monitor.js @@ -6,11 +6,12 @@ dayjs.extend(utc) dayjs.extend(timezone) const axios = require("axios"); const { Prometheus } = require("../prometheus"); -const { debug, UP, DOWN, PENDING, flipStatus } = require("../../src/util"); -const { tcping, ping, checkCertificate } = require("../util-server"); +const { debug, UP, DOWN, PENDING, flipStatus, TimeLogger } = require("../../src/util"); +const { tcping, ping, checkCertificate, checkStatusCode } = require("../util-server"); const { R } = require("redbean-node"); const { BeanModel } = require("redbean-node/dist/bean-model"); const { Notification } = require("../notification") +const version = require("../../package.json").version; /** * status: @@ -45,6 +46,8 @@ class Monitor extends BeanModel { keyword: this.keyword, ignoreTls: this.getIgnoreTls(), upsideDown: this.isUpsideDown(), + maxredirects: this.maxredirects, + accepted_statuscodes: this.getAcceptedStatuscodes(), notificationIDList, }; } @@ -65,6 +68,10 @@ class Monitor extends BeanModel { return Boolean(this.upsideDown); } + getAcceptedStatuscodes() { + return JSON.parse(this.accepted_statuscodes_json); + } + start(io) { let previousBeat = null; let retries = 0; @@ -73,6 +80,10 @@ class Monitor extends BeanModel { const beat = async () => { + // Expose here for prometheus update + // undefined if not https + let tlsInfo = undefined; + if (! previousBeat) { previousBeat = await R.findOne("heartbeat", " monitor_id = ? ORDER BY time DESC", [ this.id, @@ -99,30 +110,36 @@ class Monitor extends BeanModel { try { if (this.type === "http" || this.type === "keyword") { + // Do not do any queries/high loading things before the "bean.ping" let startTime = dayjs().valueOf(); - // Use Custom agent to disable session reuse - // https://github.com/nodejs/node/issues/3940 let res = await axios.get(this.url, { + timeout: this.interval * 1000 * 0.8, headers: { - "User-Agent": "Uptime-Kuma", + "Accept": "*/*", + "User-Agent": "Uptime-Kuma/" + version, }, httpsAgent: new https.Agent({ - maxCachedSessions: 0, + maxCachedSessions: 0, // Use Custom agent to disable session reuse (https://github.com/nodejs/node/issues/3940) rejectUnauthorized: ! this.getIgnoreTls(), }), + maxRedirects: this.maxredirects, + validateStatus: (status) => { + return checkStatusCode(status, this.getAcceptedStatuscodes()); + }, }); 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)); + tlsInfo = await this.updateTlsInfo(checkCertificate(res)); } catch (e) { - console.error(e.message) + if (e.message !== "No TLS certificate in response") { + console.error(e.message) + } } } @@ -223,7 +240,8 @@ class Monitor extends BeanModel { try { await Notification.send(JSON.parse(notification.config), msg, await this.toJSON(), bean.toJSON()) } catch (e) { - console.error("Cannot send notification to " + notification.name) + console.error("Cannot send notification to " + notification.name); + console.log(e); } } } @@ -240,22 +258,22 @@ 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) Monitor.sendStats(io, this.id, this.user_id) + await R.store(bean); + prometheus.update(bean, tlsInfo); + previousBeat = bean; + + this.heartbeatInterval = setTimeout(beat, this.interval * 1000); } beat(); - this.heartbeatInterval = setInterval(beat, this.interval * 1000); } stop() { - clearInterval(this.heartbeatInterval) + clearTimeout(this.heartbeatInterval); } /** @@ -275,7 +293,7 @@ class Monitor extends BeanModel { /** * Store TLS info to database * @param checkCertificateResult - * @returns {Promise} + * @returns {Promise} */ async updateTlsInfo(checkCertificateResult) { let tls_info_bean = await R.findOne("monitor_tls_info", "monitor_id = ?", [ @@ -287,13 +305,15 @@ class Monitor extends BeanModel { } tls_info_bean.info_json = JSON.stringify(checkCertificateResult); await R.store(tls_info_bean); + + return checkCertificateResult; } 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); + await Monitor.sendAvgPing(24, io, monitorID, userID); + await Monitor.sendUptime(24, io, monitorID, userID); + await Monitor.sendUptime(24 * 30, io, monitorID, userID); + await Monitor.sendCertInfo(io, monitorID, userID); } /** @@ -301,6 +321,8 @@ class Monitor extends BeanModel { * @param duration : int Hours */ static async sendAvgPing(duration, io, monitorID, userID) { + const timeLogger = new TimeLogger(); + let avgPing = parseInt(await R.getCell(` SELECT AVG(ping) FROM heartbeat @@ -311,6 +333,8 @@ class Monitor extends BeanModel { monitorID, ])); + timeLogger.print(`[Monitor: ${monitorID}] avgPing`); + io.to(userID).emit("avgPing", monitorID, avgPing); } @@ -330,6 +354,8 @@ class Monitor extends BeanModel { * @param duration : int Hours */ static async sendUptime(duration, io, monitorID, userID) { + const timeLogger = new TimeLogger(); + let sec = duration * 3600; let heartbeatList = await R.getAll(` @@ -341,6 +367,8 @@ class Monitor extends BeanModel { monitorID, ]); + timeLogger.print(`[Monitor: ${monitorID}][${duration}] sendUptime`); + let downtime = 0; let total = 0; let uptime; diff --git a/server/model/user.js b/server/model/user.js new file mode 100644 index 0000000..d1d3d20 --- /dev/null +++ b/server/model/user.js @@ -0,0 +1,21 @@ +const { BeanModel } = require("redbean-node/dist/bean-model"); +const passwordHash = require("../password-hash"); +const { R } = require("redbean-node"); + +class User extends BeanModel { + + /** + * Direct execute, no need R.store() + * @param newPassword + * @returns {Promise} + */ + async resetPassword(newPassword) { + await R.exec("UPDATE `user` SET password = ? WHERE id = ? ", [ + passwordHash.generate(newPassword), + this.id + ]); + this.password = newPassword; + } +} + +module.exports = User; diff --git a/server/notification.js b/server/notification.js index b250ac3..ae8891b 100644 --- a/server/notification.js +++ b/server/notification.js @@ -84,40 +84,78 @@ class Notification { } else if (notification.type === "discord") { try { + const discordDisplayName = notification.discordUsername || "Uptime Kuma"; + // If heartbeatJSON is null, assume we're testing. if (heartbeatJSON == null) { - let data = { - username: "Uptime-Kuma", + let discordtestdata = { + username: discordDisplayName, content: msg, } - await axios.post(notification.discordWebhookUrl, data) + await axios.post(notification.discordWebhookUrl, discordtestdata) return okMsg; } // If heartbeatJSON is not null, we go into the normal alerting loop. if (heartbeatJSON["status"] == 0) { - var alertColor = "16711680"; + let discorddowndata = { + username: discordDisplayName, + embeds: [{ + title: "❌ One of your services went down. ❌", + color: 16711680, + timestamp: heartbeatJSON["time"], + fields: [ + { + name: "Service Name", + value: monitorJSON["name"], + }, + { + name: "Service URL", + value: monitorJSON["url"], + }, + { + name: "Time (UTC)", + value: heartbeatJSON["time"], + }, + { + name: "Error", + value: heartbeatJSON["msg"], + }, + ], + }], + } + await axios.post(notification.discordWebhookUrl, discorddowndata) + return okMsg; + } else if (heartbeatJSON["status"] == 1) { - var alertColor = "65280"; - } - let data = { - username: "Uptime-Kuma", - embeds: [{ - title: "Uptime-Kuma Alert", - color: alertColor, - fields: [ - { - name: "Time (UTC)", - value: heartbeatJSON["time"], - }, - { - name: "Message", - value: msg, - }, - ], - }], + let discordupdata = { + username: discordDisplayName, + embeds: [{ + title: "✅ Your service " + monitorJSON["name"] + " is up! ✅", + color: 65280, + timestamp: heartbeatJSON["time"], + fields: [ + { + name: "Service Name", + value: monitorJSON["name"], + }, + { + name: "Service URL", + value: "[Visit Service](" + monitorJSON["url"] + ")", + }, + { + name: "Time (UTC)", + value: heartbeatJSON["time"], + }, + { + name: "Ping", + value: heartbeatJSON["ping"] + "ms", + }, + ], + }], + } + await axios.post(notification.discordWebhookUrl, discordupdata) + return okMsg; } - await axios.post(notification.discordWebhookUrl, data) - return okMsg; } catch (error) { throwGeneralAxiosError(error) } @@ -136,6 +174,7 @@ class Notification { } catch (error) { throwGeneralAxiosError(error) } + } else if (notification.type === "pushy") { try { await axios.post(`https://api.pushy.me/push?api_key=${notification.pushyAPIKey}`, { @@ -154,6 +193,34 @@ class Notification { console.log(error) return false; } + } else if (notification.type === "octopush") { + try { + let config = { + headers: { + "api-key": notification.octopushAPIKey, + "api-login": notification.octopushLogin, + "cache-control": "no-cache" + } + }; + let data = { + "recipients": [ + { + "phone_number": notification.octopushPhoneNumber + } + ], + //octopush not supporting non ascii char + "text": msg.replace(/[^\x00-\x7F]/g, ""), + "type": notification.octopushSMSType, + "purpose": "alert", + "sender": notification.octopushSenderName + }; + + await axios.post("https://api.octopush.com/v1/public/sms-campaign/send", data, config) + return true; + } catch (error) { + console.log(error) + return false; + } } else if (notification.type === "slack") { try { if (heartbeatJSON == null) { @@ -248,10 +315,6 @@ class Notification { throwGeneralAxiosError(error) } - } else if (notification.type === "apprise") { - - return Notification.apprise(notification, msg) - } else if (notification.type === "lunasea") { let lunaseadevice = "https://notify.lunasea.app/v1/custom/device/" + notification.lunaseaDevice @@ -287,6 +350,88 @@ class Notification { throwGeneralAxiosError(error) } + } else if (notification.type === "pushbullet") { + try { + let pushbulletUrl = "https://api.pushbullet.com/v2/pushes"; + let config = { + headers: { + "Access-Token": notification.pushbulletAccessToken, + "Content-Type": "application/json" + } + }; + if (heartbeatJSON == null) { + let testdata = { + "type": "note", + "title": "Uptime Kuma Alert", + "body": "Testing Successful.", + } + await axios.post(pushbulletUrl, testdata, config) + } else if (heartbeatJSON["status"] == 0) { + let downdata = { + "type": "note", + "title": "UptimeKuma Alert:" + monitorJSON["name"], + "body": "[🔴 Down]" + heartbeatJSON["msg"] + "\nTime (UTC):" + heartbeatJSON["time"], + } + await axios.post(pushbulletUrl, downdata, config) + } else if (heartbeatJSON["status"] == 1) { + let updata = { + "type": "note", + "title": "UptimeKuma Alert:" + monitorJSON["name"], + "body": "[✅ Up]" + heartbeatJSON["msg"] + "\nTime (UTC):" + heartbeatJSON["time"], + } + await axios.post(pushbulletUrl, updata, config) + } + return okMsg; + } catch (error) { + throwGeneralAxiosError(error) + } + } else if (notification.type === "line") { + try { + let lineAPIUrl = "https://api.line.me/v2/bot/message/push"; + let config = { + headers: { + "Content-Type": "application/json", + "Authorization": "Bearer " + notification.lineChannelAccessToken + } + }; + if (heartbeatJSON == null) { + let testMessage = { + "to": notification.lineUserID, + "messages": [ + { + "type": "text", + "text": "Test Successful!" + } + ] + } + await axios.post(lineAPIUrl, testMessage, config) + } else if (heartbeatJSON["status"] == 0) { + let downMessage = { + "to": notification.lineUserID, + "messages": [ + { + "type": "text", + "text": "UptimeKuma Alert: [🔴 Down]\n" + "Name: " + monitorJSON["name"] + " \n" + heartbeatJSON["msg"] + "\nTime (UTC): " + heartbeatJSON["time"] + } + ] + } + await axios.post(lineAPIUrl, downMessage, config) + } else if (heartbeatJSON["status"] == 1) { + let upMessage = { + "to": notification.lineUserID, + "messages": [ + { + "type": "text", + "text": "UptimeKuma Alert: [✅ Up]\n" + "Name: " + monitorJSON["name"] + " \n" + heartbeatJSON["msg"] + "\nTime (UTC): " + heartbeatJSON["time"] + } + ] + } + await axios.post(lineAPIUrl, upMessage, config) + } + return okMsg; + } catch (error) { + throwGeneralAxiosError(error) + } } else { throw new Error("Notification type is not supported") } @@ -330,15 +475,21 @@ class Notification { static async smtp(notification, msg) { - let transporter = nodemailer.createTransport({ + const config = { host: notification.smtpHost, port: notification.smtpPort, secure: notification.smtpSecure, - auth: { + }; + + // Should fix the issue in https://github.com/louislam/uptime-kuma/issues/26#issuecomment-896373904 + if (notification.smtpUsername || notification.smtpPassword) { + config.auth = { user: notification.smtpUsername, pass: notification.smtpPassword, - }, - }); + }; + } + + let transporter = nodemailer.createTransport(config); // send mail with defined transport object await transporter.sendMail({ @@ -351,29 +502,6 @@ class Notification { return "Sent Successfully."; } - static async apprise(notification, msg) { - let s = child_process.spawnSync("apprise", [ "-vv", "-b", msg, notification.appriseURL]) - - let output = (s.stdout) ? s.stdout.toString() : "ERROR: maybe apprise not found"; - - if (output) { - - if (! output.includes("ERROR")) { - return "Sent Successfully"; - } - - throw new Error(output) - } else { - return "" - } - } - - static checkApprise() { - let commandExistsSync = require("command-exists").sync; - let exists = commandExistsSync("apprise"); - return exists; - } - } function throwGeneralAxiosError(error) { diff --git a/server/ping-lite.js b/server/ping-lite.js index 0b9a740..42a704e 100644 --- a/server/ping-lite.js +++ b/server/ping-lite.js @@ -1,12 +1,14 @@ // https://github.com/ben-bradley/ping-lite/blob/master/ping-lite.js // Fixed on Windows - -let spawn = require("child_process").spawn, +const net = require("net"); +const spawn = require("child_process").spawn, events = require("events"), fs = require("fs"), WIN = /^win/.test(process.platform), LIN = /^linux/.test(process.platform), MAC = /^darwin/.test(process.platform); + FBSD = /^freebsd/.test(process.platform); +const { debug } = require("../src/util"); module.exports = Ping; @@ -24,14 +26,42 @@ function Ping(host, options) { this._bin = "c:/windows/system32/ping.exe"; this._args = (options.args) ? options.args : [ "-n", "1", "-w", "5000", host ]; this._regmatch = /[><=]([0-9.]+?)ms/; + } else if (LIN) { this._bin = "/bin/ping"; - this._args = (options.args) ? options.args : [ "-n", "-w", "2", "-c", "1", host ]; - this._regmatch = /=([0-9.]+?) ms/; // need to verify this + + const defaultArgs = [ "-n", "-w", "2", "-c", "1", host ]; + + if (net.isIPv6(host) || options.ipv6) { + defaultArgs.unshift("-6"); + } + + this._args = (options.args) ? options.args : defaultArgs; + this._regmatch = /=([0-9.]+?) ms/; + } else if (MAC) { - this._bin = "/sbin/ping"; + + if (net.isIPv6(host) || options.ipv6) { + this._bin = "/sbin/ping6"; + } else { + this._bin = "/sbin/ping"; + } + this._args = (options.args) ? options.args : [ "-n", "-t", "2", "-c", "1", host ]; this._regmatch = /=([0-9.]+?) ms/; + + } else if (FBSD) { + this._bin = "/sbin/ping"; + + const defaultArgs = [ "-n", "-t", "2", "-c", "1", host ]; + + if (net.isIPv6(host) || options.ipv6) { + defaultArgs.unshift("-6"); + } + + this._args = (options.args) ? options.args : defaultArgs; + this._regmatch = /=([0-9.]+?) ms/; + } else { throw new Error("Could not detect your ping binary."); } @@ -49,9 +79,9 @@ Ping.prototype.__proto__ = events.EventEmitter.prototype; // SEND A PING // =========== -Ping.prototype.send = function(callback) { +Ping.prototype.send = function (callback) { let self = this; - callback = callback || function(err, ms) { + callback = callback || function (err, ms) { if (err) { return self.emit("error", err); } @@ -62,27 +92,27 @@ Ping.prototype.send = function(callback) { this._ping = spawn(this._bin, this._args); // spawn the binary - this._ping.on("error", function(err) { // handle binary errors + this._ping.on("error", function (err) { // handle binary errors _errored = true; callback(err); }); - this._ping.stdout.on("data", function(data) { // log stdout + this._ping.stdout.on("data", function (data) { // log stdout this._stdout = (this._stdout || "") + data; }); - this._ping.stdout.on("end", function() { + this._ping.stdout.on("end", function () { _ended = true; if (_exited && !_errored) { onEnd.call(self._ping); } }); - this._ping.stderr.on("data", function(data) { // log stderr + this._ping.stderr.on("data", function (data) { // log stderr this._stderr = (this._stderr || "") + data; }); - this._ping.on("exit", function(code) { // handle complete + this._ping.on("exit", function (code) { // handle complete _exited = true; if (_ended && !_errored) { onEnd.call(self._ping); @@ -105,15 +135,15 @@ Ping.prototype.send = function(callback) { ms = stdout.match(self._regmatch); // parse out the ##ms response ms = (ms && ms[1]) ? Number(ms[1]) : ms; - callback(null, ms); + callback(null, ms, stdout); } }; // CALL Ping#send(callback) ON A TIMER // =================================== -Ping.prototype.start = function(callback) { +Ping.prototype.start = function (callback) { let self = this; - this._i = setInterval(function() { + this._i = setInterval(function () { self.send(callback); }, (self._options.interval || 5000)); self.send(callback); @@ -121,6 +151,6 @@ Ping.prototype.start = function(callback) { // STOP SENDING PINGS // ================== -Ping.prototype.stop = function() { +Ping.prototype.stop = function () { clearInterval(this._i); }; diff --git a/server/prometheus.js b/server/prometheus.js index f60ec45..3e4767b 100644 --- a/server/prometheus.js +++ b/server/prometheus.js @@ -1,22 +1,33 @@ -const PrometheusClient = require('prom-client'); +const PrometheusClient = require("prom-client"); const commonLabels = [ - 'monitor_name', - 'monitor_type', - 'monitor_url', - 'monitor_hostname', - 'monitor_port', + "monitor_name", + "monitor_type", + "monitor_url", + "monitor_hostname", + "monitor_port", ] +const monitor_cert_days_remaining = new PrometheusClient.Gauge({ + name: "monitor_cert_days_remaining", + help: "The number of days remaining until the certificate expires", + labelNames: commonLabels +}); + +const monitor_cert_is_valid = new PrometheusClient.Gauge({ + name: "monitor_cert_is_valid", + help: "Is the certificate still valid? (1 = Yes, 0= No)", + labelNames: commonLabels +}); const monitor_response_time = new PrometheusClient.Gauge({ - name: 'monitor_response_time', - help: 'Monitor Response Time (ms)', + 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)', + name: "monitor_status", + help: "Monitor Status (1 = UP, 0= DOWN)", labelNames: commonLabels }); @@ -33,7 +44,27 @@ class Prometheus { } } - update(heartbeat) { + update(heartbeat, tlsInfo) { + if (typeof tlsInfo !== "undefined") { + try { + let is_valid = 0 + if (tlsInfo.valid == true) { + is_valid = 1 + } else { + is_valid = 0 + } + monitor_cert_is_valid.set(this.monitorLabelValues, is_valid) + } catch (e) { + console.error(e) + } + + try { + monitor_cert_days_remaining.set(this.monitorLabelValues, tlsInfo.daysRemaining) + } catch (e) { + console.error(e) + } + } + try { monitor_status.set(this.monitorLabelValues, heartbeat.status) } catch (e) { @@ -41,7 +72,7 @@ class Prometheus { } try { - if (typeof heartbeat.ping === 'number') { + if (typeof heartbeat.ping === "number") { monitor_response_time.set(this.monitorLabelValues, heartbeat.ping) } else { // Is it good? diff --git a/server/server.js b/server/server.js index b03fcc0..6a55488 100644 --- a/server/server.js +++ b/server/server.js @@ -1,6 +1,7 @@ -console.log("Welcome to Uptime Kuma") +console.log("Welcome to Uptime Kuma"); +console.log("Node Env: " + process.env.NODE_ENV); -const { sleep, debug } = require("../src/util"); +const { sleep, debug, TimeLogger, getRandomInt } = require("../src/util"); console.log("Importing Node libraries") const fs = require("fs"); @@ -11,8 +12,6 @@ debug("Importing express"); const express = require("express"); debug("Importing socket.io"); const { Server } = require("socket.io"); -debug("Importing dayjs"); -const dayjs = require("dayjs"); debug("Importing redbean-node"); const { R } = require("redbean-node"); debug("Importing jsonwebtoken"); @@ -26,7 +25,7 @@ console.log("Importing this project modules"); debug("Importing Monitor"); const Monitor = require("./model/monitor"); debug("Importing Settings"); -const { getSettings, setSettings, setting } = require("./util-server"); +const { getSettings, setSettings, setting, initJWTSecret } = require("./util-server"); debug("Importing Notification"); const { Notification } = require("./notification"); debug("Importing Database"); @@ -38,11 +37,13 @@ const passwordHash = require("./password-hash"); const args = require("args-parser")(process.argv); -const version = require("../package.json").version; -const hostname = process.env.HOST || args.host || "0.0.0.0" -const port = parseInt(process.env.PORT || args.port || 3001); +const checkVersion = require("./check-version"); +console.info("Version: " + checkVersion.version); -console.info("Version: " + version) +// If host is omitted, the server will accept connections on the unspecified IPv6 address (::) when IPv6 is available and the unspecified IPv4 address (0.0.0.0) otherwise. +// Dual-stack support for (::) +const hostname = process.env.HOST || args.host; +const port = parseInt(process.env.PORT || args.port || 3001); console.log("Creating express and socket.io instance") const app = express(); @@ -87,24 +88,35 @@ let indexHTML = fs.readFileSync("./dist/index.html").toString(); // Normal Router here - app.use("/", express.static("dist")); + // Robots.txt + app.get("/robots.txt", async (_request, response) => { + let txt = "User-agent: *\nDisallow:"; + if (! await setting("searchEngineIndex")) { + txt += " /"; + } + response.setHeader("Content-Type", "text/plain"); + response.send(txt); + }); // Basic Auth Router here // Prometheus API metrics /metrics // With Basic Auth using the first user's username/password - app.get("/metrics", basicAuth, prometheusAPIMetrics()) + app.get("/metrics", basicAuth, prometheusAPIMetrics()); + + app.use("/", express.static("dist")); // Universal Route Handler, must be at the end - app.get("*", function(request, response, next) { - response.end(indexHTML) + app.get("*", async (_request, response) => { + response.send(indexHTML); }); console.log("Adding socket handler") io.on("connection", async (socket) => { socket.emit("info", { - version, + version: checkVersion.version, + latestVersion: checkVersion.latestVersion, }) totalClient++; @@ -114,11 +126,6 @@ let indexHTML = fs.readFileSync("./dist/index.html").toString(); socket.emit("setup") } - if (await setting("disableAuth")) { - console.log("Disabled Auth: auto login to admin") - await afterLogin(socket, await R.findOne("user", " username = 'admin' ")) - } - socket.on("disconnect", () => { totalClient--; }); @@ -139,7 +146,11 @@ let indexHTML = fs.readFileSync("./dist/index.html").toString(); ]) if (user) { - await afterLogin(socket, user) + debug("afterLogin") + + afterLogin(socket, user) + + debug("afterLogin ok") callback({ ok: true, @@ -165,7 +176,7 @@ let indexHTML = fs.readFileSync("./dist/index.html").toString(); let user = await login(data.username, data.password) if (user) { - await afterLogin(socket, user) + afterLogin(socket, user) callback({ ok: true, @@ -231,6 +242,9 @@ let indexHTML = fs.readFileSync("./dist/index.html").toString(); let notificationIDList = monitor.notificationIDList; delete monitor.notificationIDList; + monitor.accepted_statuscodes_json = JSON.stringify(monitor.accepted_statuscodes); + delete monitor.accepted_statuscodes; + bean.import(monitor) bean.user_id = socket.userID await R.store(bean) @@ -275,6 +289,8 @@ let indexHTML = fs.readFileSync("./dist/index.html").toString(); bean.keyword = monitor.keyword; bean.ignoreTls = monitor.ignoreTls; bean.upsideDown = monitor.upsideDown; + bean.maxredirects = monitor.maxredirects; + bean.accepted_statuscodes_json = JSON.stringify(monitor.accepted_statuscodes); await R.store(bean) @@ -409,10 +425,7 @@ let indexHTML = fs.readFileSync("./dist/index.html").toString(); if (user && passwordHash.verify(password.currentPassword, user.password)) { - await R.exec("UPDATE `user` SET password = ? WHERE id = ? ", [ - passwordHash.generate(password.newPassword), - socket.userID, - ]); + user.resetPassword(password.newPassword); callback({ ok: true, @@ -527,18 +540,45 @@ let indexHTML = fs.readFileSync("./dist/index.html").toString(); }); } }); + + debug("added all socket handlers") + + // *************************** + // Better do anything after added all socket handlers here + // *************************** + + debug("check auto login") + if (await setting("disableAuth")) { + console.log("Disabled Auth: auto login to admin") + afterLogin(socket, await R.findOne("user")) + socket.emit("autoLogin") + } else { + debug("need auth") + } + + }); + + console.log("Init the server") + + server.once("error", async (err) => { + console.error("Cannot listen: " + err.message); + await Database.close(); }); - console.log("Init") server.listen(port, hostname, () => { - console.log(`Listening on ${hostname}:${port}`); + if (hostname) { + console.log(`Listening on ${hostname}:${port}`); + } else { + console.log(`Listening on ${port}`); + } startMonitors(); + checkVersion.startInterval(); }); })(); async function updateMonitorNotification(monitorID, notificationIDList) { - R.exec("DELETE FROM monitor_notification WHERE monitor_id = ? ", [ + await R.exec("DELETE FROM monitor_notification WHERE monitor_id = ? ", [ monitorID, ]) @@ -570,6 +610,8 @@ async function sendMonitorList(socket) { } async function sendNotificationList(socket) { + const timeLogger = new TimeLogger(); + let result = []; let list = await R.find("notification", " user_id = ? ", [ socket.userID, @@ -580,6 +622,9 @@ async function sendNotificationList(socket) { } io.to(socket.userID).emit("notificationList", result) + + timeLogger.print("Send Notification List"); + return list; } @@ -588,22 +633,27 @@ async function afterLogin(socket, user) { socket.join(user.id) let monitorList = await sendMonitorList(socket) + sendNotificationList(socket) + + await sleep(500); for (let monitorID in monitorList) { - sendHeartbeatList(socket, monitorID); - sendImportantHeartbeatList(socket, monitorID); - Monitor.sendStats(io, monitorID, user.id) + await sendHeartbeatList(socket, monitorID); } - sendNotificationList(socket) + for (let monitorID in monitorList) { + await sendImportantHeartbeatList(socket, monitorID); + } - socket.emit("autoLogin") + for (let monitorID in monitorList) { + await Monitor.sendStats(io, monitorID, user.id) + } } async function getMonitorJSONList(userID) { let result = {}; - let monitorList = await R.find("monitor", " user_id = ? ", [ + let monitorList = await R.find("monitor", " user_id = ? ORDER BY weight DESC, name", [ userID, ]) @@ -627,32 +677,22 @@ async function initDatabase() { } console.log("Connecting to Database") - R.setup("sqlite", { - filename: Database.path, - }); + await Database.connect(); console.log("Connected") // Patch the database await Database.patch() - // Auto map the model to a bean object - R.freeze(true) - await R.autoloadModels("./server/model"); - let jwtSecretBean = await R.findOne("setting", " `key` = ? ", [ "jwtSecret", ]); if (! jwtSecretBean) { - console.log("JWT secret is not found, generate one.") - jwtSecretBean = R.dispense("setting") - jwtSecretBean.key = "jwtSecret" - - jwtSecretBean.value = passwordHash.generate(dayjs() + "") - await R.store(jwtSecretBean) - console.log("Stored JWT secret into database") + console.log("JWT secret is not found, generate one."); + jwtSecretBean = await initJWTSecret(); + console.log("Stored JWT secret into database"); } else { - console.log("Load JWT secret from database.") + console.log("Load JWT secret from database."); } // If there is no record in user table, it is a new Uptime Kuma instance, need to setup @@ -712,15 +752,22 @@ async function startMonitors() { let list = await R.find("monitor", " active = 1 ") for (let monitor of list) { - monitor.start(io) monitorList[monitor.id] = monitor; } + + for (let monitor of list) { + monitor.start(io); + // Give some delays, so all monitors won't make request at the same moment when just start the server. + await sleep(getRandomInt(300, 1000)); + } } /** * Send Heartbeat History list to socket */ async function sendHeartbeatList(socket, monitorID) { + const timeLogger = new TimeLogger(); + let list = await R.find("heartbeat", ` monitor_id = ? ORDER BY time DESC @@ -736,9 +783,13 @@ async function sendHeartbeatList(socket, monitorID) { } socket.emit("heartbeatList", monitorID, result) + + timeLogger.print(`[Monitor: ${monitorID}] sendHeartbeatList`) } async function sendImportantHeartbeatList(socket, monitorID) { + const timeLogger = new TimeLogger(); + let list = await R.find("heartbeat", ` monitor_id = ? AND important = 1 @@ -748,6 +799,8 @@ async function sendImportantHeartbeatList(socket, monitorID) { monitorID, ]) + timeLogger.print(`[Monitor: ${monitorID}] sendImportantHeartbeatList`); + socket.emit("importantHeartbeatList", monitorID, list) } @@ -762,11 +815,10 @@ async function shutdownFunction(signal) { } await sleep(2000); await Database.close(); - console.log("Stopped DB") } function finalFunction() { - console.log("Graceful Shutdown Done") + console.log("Graceful shutdown successfully!"); } gracefulShutdown(server, { @@ -777,3 +829,9 @@ gracefulShutdown(server, { onShutdown: shutdownFunction, // shutdown function (async) - e.g. for cleanup DB, ... finally: finalFunction, // finally function (sync) - e.g. for logging }); + +// Catch unexpected errors here +process.addListener("unhandledRejection", (error, promise) => { + console.trace(error); + console.error("If you keep encountering errors, please report to https://github.com/louislam/uptime-kuma/issues"); +}); diff --git a/server/util-server.js b/server/util-server.js index 1411a3f..8a2f038 100644 --- a/server/util-server.js +++ b/server/util-server.js @@ -2,6 +2,27 @@ const tcpp = require("tcp-ping"); const Ping = require("./ping-lite"); const { R } = require("redbean-node"); const { debug } = require("../src/util"); +const passwordHash = require("./password-hash"); +const dayjs = require("dayjs"); + +/** + * Init or reset JWT secret + * @returns {Promise} + */ +exports.initJWTSecret = async () => { + let jwtSecretBean = await R.findOne("setting", " `key` = ? ", [ + "jwtSecret", + ]); + + if (! jwtSecretBean) { + jwtSecretBean = R.dispense("setting"); + jwtSecretBean.key = "jwtSecret"; + } + + jwtSecretBean.value = passwordHash.generate(dayjs() + ""); + await R.store(jwtSecretBean); + return jwtSecretBean; +} exports.tcping = function (hostname, port) { return new Promise((resolve, reject) => { @@ -9,7 +30,7 @@ exports.tcping = function (hostname, port) { address: hostname, port: port, attempts: 1, - }, function(err, data) { + }, function (err, data) { if (err) { reject(err); @@ -24,15 +45,30 @@ exports.tcping = function (hostname, port) { }); } -exports.ping = function (hostname) { +exports.ping = async (hostname) => { + try { + return await exports.pingAsync(hostname); + } catch (e) { + // If the host cannot be resolved, try again with ipv6 + if (e.message.includes("service not known")) { + return await exports.pingAsync(hostname, true); + } else { + throw e; + } + } +} + +exports.pingAsync = function (hostname, ipv6 = false) { return new Promise((resolve, reject) => { - const ping = new Ping(hostname); + const ping = new Ping(hostname, { + ipv6 + }); - ping.send(function(err, ms) { + ping.send(function (err, ms, stdout) { if (err) { - reject(err) + reject(err); } else if (ms === null) { - reject(new Error("timeout")) + reject(new Error(stdout)) } else { resolve(Math.round(ms)) } @@ -58,7 +94,7 @@ exports.setSetting = async function (key, value) { let bean = await R.findOne("setting", " `key` = ? ", [ key, ]) - if (! bean) { + if (!bean) { bean = R.dispense("setting") bean.key = key; } @@ -158,3 +194,32 @@ exports.checkCertificate = function (res) { fingerprint, }; } + +// Check if the provided status code is within the accepted ranges +// Param: status - the status code to check +// Param: accepted_codes - an array of accepted status codes +// Return: true if the status code is within the accepted ranges, false otherwise +// Will throw an error if the provided status code is not a valid range string or code string + +exports.checkStatusCode = function (status, accepted_codes) { + if (accepted_codes == null || accepted_codes.length === 0) { + return false; + } + + for (const code_range of accepted_codes) { + const code_range_split = code_range.split("-").map(string => parseInt(string)); + if (code_range_split.length === 1) { + if (status === code_range_split[0]) { + return true; + } + } else if (code_range_split.length === 2) { + if (status >= code_range_split[0] && status <= code_range_split[1]) { + return true; + } + } else { + throw new Error("Invalid status code range"); + } + } + + return false; +} diff --git a/src/components/Confirm.vue b/src/components/Confirm.vue index b235824..391155f 100644 --- a/src/components/Confirm.vue +++ b/src/components/Confirm.vue @@ -4,7 +4,7 @@ @@ -21,7 +21,10 @@ export default { type: String, default: "big", }, - monitorId: Number, + monitorId: { + type: Number, + required: true, + }, }, data() { return { @@ -36,14 +39,15 @@ export default { computed: { beatList() { - if (! (this.monitorId in this.$root.heartbeatList)) { - this.$root.heartbeatList[this.monitorId] = []; - } return this.$root.heartbeatList[this.monitorId] }, shortBeatList() { - let placeholders = [] + if (! this.beatList) { + return []; + } + + let placeholders = []; let start = this.beatList.length - this.maxBeat; @@ -113,6 +117,11 @@ export default { unmounted() { window.removeEventListener("resize", this.resize); }, + beforeMount() { + if (! (this.monitorId in this.$root.heartbeatList)) { + this.$root.heartbeatList[this.monitorId] = []; + } + }, mounted() { if (this.size === "small") { this.beatWidth = 5.6; @@ -129,11 +138,15 @@ export default { this.maxBeat = Math.floor(this.$refs.wrap.clientWidth / (this.beatWidth + this.beatMargin * 2)) } }, + + getBeatTitle(beat) { + return `${this.$root.datetime(beat.time)} - ${beat.msg}`; + } }, } - diff --git a/src/components/Login.vue b/src/components/Login.vue index 4b08de0..bd51759 100644 --- a/src/components/Login.vue +++ b/src/components/Login.vue @@ -6,12 +6,12 @@
- +
- +
@@ -19,12 +19,12 @@