diff --git a/.dockerignore b/.dockerignore index 62925caa..6e11b36b 100644 --- a/.dockerignore +++ b/.dockerignore @@ -28,6 +28,7 @@ install.sh SECURITY.md tsconfig.json .env +/tmp ### .gitignore content (commented rules are duplicated) diff --git a/.github/ISSUE_TEMPLATE/ask-for-help.md b/.github/ISSUE_TEMPLATE/ask-for-help.md index 8184f840..79ec21c6 100644 --- a/.github/ISSUE_TEMPLATE/ask-for-help.md +++ b/.github/ISSUE_TEMPLATE/ask-for-help.md @@ -9,9 +9,8 @@ assignees: '' **Is it a duplicate question?** Please search in Issues without filters: https://github.com/louislam/uptime-kuma/issues?q= - **Describe your problem** - +Please describe what you are asking for **Info** Uptime Kuma Version: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 93b03ac6..9c4d5dc4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -6,7 +6,7 @@ The project was created with vite.js (vue3). Then I created a sub-directory call The frontend code build into "dist" directory. The server (express.js) exposes the "dist" directory as root of the endpoint. This is how production is working. -# Key Technical Skills +## Key Technical Skills - Node.js (You should know what are promise, async/await and arrow function etc.) - Socket.io @@ -15,7 +15,7 @@ The frontend code build into "dist" directory. The server (express.js) exposes t - Bootstrap - SQLite -# Directories +## Directories - data (App data) - dist (Frontend build) @@ -25,50 +25,50 @@ The frontend code build into "dist" directory. The server (express.js) exposes t - src (Frontend source code) - test (unit test) -# Can I create a pull request for Uptime Kuma? +## 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 into 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 +### Pull Request Examples -### ✅ High - Medium Priority +#### ✅ High - Medium Priority - Add a new notification - Add a chart - Fix a bug - Translations -### *️⃣ Requires one more reviewer +#### *️⃣ Requires one more reviewer I do not have such knowledge to test it. - Add k8s supports -### *️⃣ Low Priority +#### *️⃣ Low Priority It changed my current workflow and require further studies. - Change my release approach -### ❌ Won't Merge +#### ❌ Won't Merge - Duplicated pull request - Buggy - Existing logic is completely modified or deleted - A function that is completely out of scope -# Project Styles +## Project Styles I personally do not like something need to learn so much and need to config so much before you can finally start the app. - Easy to install for non-Docker users, no native build dependency is needed (at least for x86_64), no extra config, no extra effort to get it run -- Single container for Docker users, no very complex docker-composer file. Just map the volume and expose the port, then good to go +- Single container for Docker users, no very complex docker-compose file. Just map the volume and expose the port, then good to go - Settings should be configurable in the frontend. Env var is not encouraged. - Easy to use -# Coding Styles +## Coding Styles - 4 spaces indentation - Follow `.editorconfig` @@ -80,20 +80,20 @@ I personally do not like something need to learn so much and need to config so m - SQLite: underscore_type - CSS/SCSS: dash-type -# Tools +## Tools - Node.js >= 14 - Git - IDE that supports ESLint and EditorConfig (I am using Intellji Idea) - A SQLite tool (SQLite Expert Personal is suggested) -# Install dependencies +## Install dependencies ```bash npm ci ``` -# How to start the Backend Dev Server +## How to start the Backend Dev Server (2021-09-23 Update) @@ -103,7 +103,7 @@ npm run start-server-dev It binds to `0.0.0.0:3001` by default. -## Backend Details +### Backend Details It is mainly a socket.io app + express.js. @@ -116,24 +116,26 @@ express.js is just used for serving the frontend built files (index.html, .js an - scoket-handler (Socket.io Handlers) - server.js (Server main logic) -# How to start the Frontend Dev Server +## How to start the Frontend Dev Server 1. Set the env var `NODE_ENV` to "development". 2. Start the frontend dev server by the following command. + ```bash npm run dev ``` + It binds to `0.0.0.0:3000` by default. You can use Vue.js devtools Chrome extension for debugging. -## Build the frontend +### Build the frontend ```bash npm run build ``` -## Frontend Details +### Frontend Details Uptime Kuma Frontend is a single page application (SPA). Most paths are handled by Vue Router. @@ -143,24 +145,23 @@ As you can see, most data in frontend is stored in root level, even though you c The data and socket logic are in `src/mixins/socket.js`. - -# Database Migration +## Database Migration 1. Create `patch-{name}.sql` in `./db/` 2. Add your patch filename in the `patchList` list in `./server/database.js` -# Unit Test +## Unit Test It is an end-to-end testing. It is using Jest and Puppeteer. -``` +```bash npm run build npm test ``` By default, the Chromium window will be shown up during the test. Specifying `HEADLESS_TEST=1` for terminal environments. -# Update Dependencies +## Update Dependencies Install `ncu` https://github.com/raineorshine/npm-check-updates @@ -170,10 +171,10 @@ ncu -u -t patch npm install ``` -Since previously updating vite 2.5.10 to 2.6.0 broke the application completely, from now on, it should update patch release version only. +Since previously updating vite 2.5.10 to 2.6.0 broke the application completely, from now on, it should update patch release version only. -Patch release = the third digit +Patch release = the third digit ([Semantic Versioning](https://semver.org/)) -# Translations +## Translations Please read: https://github.com/louislam/uptime-kuma/tree/master/src/languages diff --git a/README.md b/README.md index 75ee8b23..1dc492bf 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,6 @@ -
@@ -17,17 +16,20 @@ Try it! https://demo.uptime.kuma.pet -It is a 5 minutes live demo, all data will be deleted after that. The server is located at Tokyo, if you live far away from here, it may affact your experience. I suggest that you should install to try it. +It is a 10 minutes live demo, all data will be deleted after that. The server is located at Tokyo, if you live far away from here, it may affact your experience. I suggest that you should install to try it. VPS is sponsored by Uptime Kuma sponsors on [Open Collective](https://opencollective.com/uptime-kuma)! Thank you so much! ## ⭐ Features -* Monitoring uptime for HTTP(s) / TCP / Ping / DNS Record. +* Monitoring uptime for HTTP(s) / TCP / Ping / DNS Record / Push. * Fancy, Reactive, Fast UI/UX. -* Notifications via Telegram, Discord, Gotify, Slack, Pushover, Email (SMTP), and [70+ notification services, click here for the full list](https://github.com/louislam/uptime-kuma/issues/284). +* Notifications via Telegram, Discord, Gotify, Slack, Pushover, Email (SMTP), and [70+ notification services, click here for the full list](https://github.com/louislam/uptime-kuma/tree/master/src/components/notifications). * 20 seconds interval. * [Multi Languages](https://github.com/louislam/uptime-kuma/tree/master/src/languages) +* Simple Status Page +* Ping Chart +* Certificate Info ## 🔧 How to Install @@ -116,9 +118,11 @@ If you love this project, please consider giving me a ⭐. ## 🗣️ Discussion ### Issues Page + You can discuss or ask for help in [Issues](https://github.com/louislam/uptime-kuma/issues). ### Subreddit + My Reddit account: louislamlam You can mention me if you ask question on Reddit. https://www.reddit.com/r/UptimeKuma/ diff --git a/SECURITY.md b/SECURITY.md index d2f000ed..a0b2562f 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -5,12 +5,27 @@ Use this section to tell people about which versions of your project are currently being supported with security updates. +### Uptime Kuma Versions + | Version | Supported | | ------- | ------------------ | -| 1.7.X | :white_check_mark: | -| < 1.7 | ❌ | +| 1.8.X | :white_check_mark: | +| <= 1.7.X | ❌ | + +### Upgradable Docker Tags + +| Tag | Supported | +| ------- | ------------------ | +| 1 | :white_check_mark: | +| 1-debian | :white_check_mark: | +| 1-alpine | :white_check_mark: | +| latest | :white_check_mark: | +| debian | :white_check_mark: | +| alpine | :white_check_mark: | +| All other tags | ❌ | ## Reporting a Vulnerability + Please report security issues to uptime@kuma.pet. Do not use the issue tracker or discuss it in the public as it will cause more damage. diff --git a/babel.config.js b/babel.config.js index 70266c1f..d2ad8213 100644 --- a/babel.config.js +++ b/babel.config.js @@ -4,4 +4,8 @@ if (process.env.TEST_FRONTEND) { config.presets = ["@babel/preset-env"]; } +if (process.env.TEST_BACKEND) { + config.plugins = ["babel-plugin-rewire"]; +} + module.exports = config; diff --git a/config/jest-backend.config.js b/config/jest-backend.config.js new file mode 100644 index 00000000..1a88d9a6 --- /dev/null +++ b/config/jest-backend.config.js @@ -0,0 +1,5 @@ +module.exports = { + "rootDir": "..", + "testRegex": "./test/backend.spec.js", +}; + diff --git a/jest-frontend.config.js b/config/jest-frontend.config.js similarity index 76% rename from jest-frontend.config.js rename to config/jest-frontend.config.js index ec4ab8d8..ab6af7f1 100644 --- a/jest-frontend.config.js +++ b/config/jest-frontend.config.js @@ -1,5 +1,5 @@ module.exports = { - "rootDir": ".", + "rootDir": "..", "testRegex": "./test/frontend.spec.js", }; diff --git a/jest-puppeteer.config.js b/config/jest-puppeteer.config.js similarity index 100% rename from jest-puppeteer.config.js rename to config/jest-puppeteer.config.js diff --git a/jest.config.js b/config/jest.config.js similarity index 90% rename from jest.config.js rename to config/jest.config.js index 6ce5b90a..4baaa0fb 100644 --- a/jest.config.js +++ b/config/jest.config.js @@ -5,7 +5,7 @@ module.exports = { "__DEV__": true }, "testRegex": "./test/e2e.spec.js", - "rootDir": ".", + "rootDir": "..", "testTimeout": 30000, }; diff --git a/vite.config.js b/config/vite.config.js similarity index 62% rename from vite.config.js rename to config/vite.config.js index 6be31f5e..a9701d42 100644 --- a/vite.config.js +++ b/config/vite.config.js @@ -1,9 +1,9 @@ -import legacy from "@vitejs/plugin-legacy" -import vue from "@vitejs/plugin-vue" -import { defineConfig } from "vite" +import legacy from "@vitejs/plugin-legacy"; +import vue from "@vitejs/plugin-vue"; +import { defineConfig } from "vite"; -const postCssScss = require("postcss-scss") -const postcssRTLCSS = require('postcss-rtlcss'); +const postCssScss = require("postcss-scss"); +const postcssRTLCSS = require("postcss-rtlcss"); // https://vitejs.dev/config/ export default defineConfig({ @@ -20,5 +20,5 @@ export default defineConfig({ "map": false, "plugins": [postcssRTLCSS] } - }, -}) + }, +}); diff --git a/db/patch-http-monitor-method-body-and-headers.sql b/db/patch-http-monitor-method-body-and-headers.sql new file mode 100644 index 00000000..dc2526b4 --- /dev/null +++ b/db/patch-http-monitor-method-body-and-headers.sql @@ -0,0 +1,13 @@ +-- You should not modify if this have pushed to Github, unless it does serious wrong with the db. +BEGIN TRANSACTION; + +ALTER TABLE monitor + ADD method TEXT default 'GET' not null; + +ALTER TABLE monitor + ADD body TEXT default null; + +ALTER TABLE monitor + ADD headers TEXT default null; + +COMMIT; diff --git a/docker-compose.yml b/docker/docker-compose.yml similarity index 100% rename from docker-compose.yml rename to docker/docker-compose.yml diff --git a/dockerfile b/docker/dockerfile similarity index 94% rename from dockerfile rename to docker/dockerfile index b6c48b28..97655748 100644 --- a/dockerfile +++ b/docker/dockerfile @@ -4,9 +4,9 @@ WORKDIR /app ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=1 COPY . . -RUN npm install --legacy-peer-deps && \ +RUN npm ci && \ npm run build && \ - npm prune --production && \ + npm ci --production && \ chmod +x /app/extra/entrypoint.sh @@ -47,5 +47,5 @@ RUN chmod +x /app/extra/upload-github-release-asset.sh # Dist only RUN cd /app && tar -zcvf $DIST dist -RUN /app/extra/upload-github-release-asset.sh github_api_token=$GITHUB_TOKEN owner=louislam repo=uptime-kuma tag=$VERSION filename=$DIST +RUN /app/extra/upload-github-release-asset.sh github_api_token=$GITHUB_TOKEN owner=louislam repo=uptime-kuma tag=$VERSION filename=/app/$DIST diff --git a/dockerfile-alpine b/docker/dockerfile-alpine similarity index 89% rename from dockerfile-alpine rename to docker/dockerfile-alpine index 7604c40c..e883031a 100644 --- a/dockerfile-alpine +++ b/docker/dockerfile-alpine @@ -4,9 +4,9 @@ WORKDIR /app ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=1 COPY . . -RUN npm install --legacy-peer-deps && \ +RUN npm ci && \ npm run build && \ - npm prune --production && \ + npm ci --production && \ chmod +x /app/extra/entrypoint.sh diff --git a/ecosystem.config.js b/ecosystem.config.js new file mode 100644 index 00000000..5f403400 --- /dev/null +++ b/ecosystem.config.js @@ -0,0 +1,6 @@ +module.exports = { + apps: [{ + name: "uptime-kuma", + script: "./server/server.js", + }] +} diff --git a/extra/download-dist.js b/extra/download-dist.js new file mode 100644 index 00000000..0a08b7f9 --- /dev/null +++ b/extra/download-dist.js @@ -0,0 +1,57 @@ +console.log("Downloading dist"); +const https = require("https"); +const tar = require("tar"); + +const packageJSON = require("../package.json"); +const fs = require("fs"); +const version = packageJSON.version; + +const filename = "dist.tar.gz"; + +const url = `https://github.com/louislam/uptime-kuma/releases/download/${version}/${filename}`; +download(url); + +function download(url) { + console.log(url); + + https.get(url, (response) => { + if (response.statusCode === 200) { + console.log("Extracting dist..."); + + if (fs.existsSync("./dist")) { + + if (fs.existsSync("./dist-backup")) { + fs.rmdirSync("./dist-backup", { + recursive: true + }); + } + + fs.renameSync("./dist", "./dist-backup"); + } + + const tarStream = tar.x({ + cwd: "./", + }); + + tarStream.on("close", () => { + fs.rmdirSync("./dist-backup", { + recursive: true + }); + console.log("Done"); + }); + + tarStream.on("error", () => { + if (fs.existsSync("./dist-backup")) { + fs.renameSync("./dist-backup", "./dist"); + } + console.log("Done"); + }); + + response.pipe(tarStream); + } else if (response.statusCode === 302) { + download(response.headers.location); + } else { + console.log("dist not found"); + } + }); +} diff --git a/extra/reset-password.js b/extra/reset-password.js index be039589..1b48dffd 100644 --- a/extra/reset-password.js +++ b/extra/reset-password.js @@ -12,50 +12,59 @@ const rl = readline.createInterface({ output: process.stdout }); -(async () => { +const main = async () => { Database.init(args); await Database.connect(); try { - const user = await R.findOne("user"); - - if (! user) { - throw new Error("user not found, have you installed?"); - } + // No need to actually reset the password for testing, just make sure no connection problem. It is ok for now. + if (!process.env.TEST_BACKEND) { + const user = await R.findOne("user"); + if (! user) { + throw new Error("user not found, have you installed?"); + } - console.log("Found user: " + user.username); + console.log("Found user: " + user.username); - while (true) { - let password = await question("New Password: "); - let confirmPassword = await question("Confirm New Password: "); + while (true) { + let password = await question("New Password: "); + let confirmPassword = await question("Confirm New Password: "); - if (password === confirmPassword) { - await user.resetPassword(password); + if (password === confirmPassword) { + await user.resetPassword(password); - // Reset all sessions by reset jwt secret - await initJWTSecret(); + // Reset all sessions by reset jwt secret + await initJWTSecret(); - rl.close(); - break; - } else { - console.log("Passwords do not match, please try again."); + break; + } else { + console.log("Passwords do not match, please try again."); + } } + console.log("Password reset successfully."); } - - console.log("Password reset successfully."); } catch (e) { console.error("Error: " + e.message); } await Database.close(); + rl.close(); - console.log("Finished. You should restart the Uptime Kuma server.") -})(); + console.log("Finished."); +}; function question(question) { return new Promise((resolve) => { rl.question(question, (answer) => { resolve(answer); - }) + }); }); } + +if (!process.env.TEST_BACKEND) { + main(); +} + +module.exports = { + main, +}; diff --git a/package.json b/package.json index 944dbbd5..ca64dbe5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "uptime-kuma", - "version": "1.7.3", + "version": "1.8.0", "license": "MIT", "repository": { "type": "git", @@ -15,27 +15,29 @@ "lint:js": "eslint --ext \".js,.vue\" --ignore-path .gitignore .", "lint:style": "stylelint \"**/*.{vue,css,scss}\" --ignore-path .gitignore", "lint": "npm run lint:js && npm run lint:style", - "dev": "vite --host", + "dev": "vite --host --config ./config/vite.config.js", "start": "npm run start-server", "start-server": "node server/server.js", "start-server-dev": "cross-env NODE_ENV=development node server/server.js", - "build": "vite build", + "build": "vite build --config ./config/vite.config.js", "test": "node test/prepare-test-server.js && node server/server.js --port=3002 --data-dir=./data/test/ --test", "test-with-build": "npm run build && npm test", - "jest": "node test/prepare-jest.js && npm run jest-frontend && jest ", - "jest-frontend": "cross-env TEST_FRONTEND=1 jest --config=./jest-frontend.config.js", + "jest": "node test/prepare-jest.js && npm run jest-frontend && npm run jest-backend && jest --config=./config/jest.config.js", + "jest-frontend": "cross-env TEST_FRONTEND=1 jest --config=./config/jest-frontend.config.js", + "jest-backend": "cross-env TEST_BACKEND=1 jest --config=./config/jest-backend.config.js", "tsc": "tsc", - "vite-preview-dist": "vite preview --host", + "vite-preview-dist": "vite preview --host --config ./config/vite.config.js", "build-docker": "npm run build-docker-debian && npm run build-docker-alpine", "build-docker-alpine-base": "docker buildx build -f docker/alpine-base.dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:base-alpine . --push", "build-docker-debian-base": "docker buildx build -f docker/debian-base.dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:base-debian . --push", - "build-docker-alpine": "docker buildx build -f dockerfile-alpine --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:alpine -t louislam/uptime-kuma:1-alpine -t louislam/uptime-kuma:1.7.3-alpine --target release . --push", - "build-docker-debian": "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.7.3 -t louislam/uptime-kuma:debian -t louislam/uptime-kuma:1-debian -t louislam/uptime-kuma:1.7.3-debian --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-alpine": "docker buildx build -f docker/dockerfile-alpine --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:alpine -t louislam/uptime-kuma:1-alpine -t louislam/uptime-kuma:1.8.0-alpine --target release . --push", + "build-docker-debian": "docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma -t louislam/uptime-kuma:1 -t louislam/uptime-kuma:1.8.0 -t louislam/uptime-kuma:debian -t louislam/uptime-kuma:1-debian -t louislam/uptime-kuma:1.8.0-debian --target release . --push", + "build-docker-nightly": "docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:nightly --target nightly . --push", "build-docker-nightly-alpine": "docker buildx build -f dockerfile-alpine --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:nightly-alpine --target nightly . --push", - "build-docker-nightly-amd64": "docker buildx build --platform linux/amd64 -t louislam/uptime-kuma:nightly-amd64 --target nightly . --push --progress plain", + "build-docker-nightly-amd64": "docker buildx build -f docker/dockerfile --platform linux/amd64 -t louislam/uptime-kuma:nightly-amd64 --target nightly . --push --progress plain", "upload-artifacts": "docker buildx build --platform linux/amd64 -t louislam/uptime-kuma:upload-artifact --build-arg GITHUB_TOKEN --target upload-artifact . --progress plain", - "setup": "git checkout 1.7.3 && npm install --legacy-peer-deps && node node_modules/esbuild/install.js && npm run build && npm prune", + "setup": "git checkout 1.8.0 && npm ci --production && npm run download-dist", + "download-dist": "node extra/download-dist.js", "update-version": "node extra/update-version.js", "mark-as-nightly": "node extra/mark-as-nightly.js", "reset-password": "node extra/reset-password.js", @@ -60,6 +62,7 @@ "axios": "~0.21.4", "bcryptjs": "~2.4.3", "bootstrap": "~5.1.1", + "chardet": "^1.3.0", "bree": "~6.3.1", "chart.js": "~3.5.1", "chartjs-adapter-dayjs": "~1.0.0", @@ -70,6 +73,7 @@ "express-basic-auth": "~1.2.0", "form-data": "~4.0.0", "http-graceful-shutdown": "~3.1.4", + "iconv-lite": "^0.6.3", "jsonwebtoken": "~8.5.1", "nodemailer": "~6.6.5", "notp": "~2.0.3", @@ -82,6 +86,7 @@ "redbean-node": "0.1.2", "socket.io": "~4.2.0", "socket.io-client": "~4.2.0", + "tar": "^6.1.11", "tcp-ping": "~0.1.1", "thirty-two": "~1.0.2", "timezones-list": "~3.0.1", @@ -105,6 +110,7 @@ "@vitejs/plugin-legacy": "~1.6.1", "@vitejs/plugin-vue": "~1.9.2", "@vue/compiler-sfc": "~3.2.19", + "babel-plugin-rewire": "~1.2.0", "core-js": "~3.18.1", "cross-env": "~7.0.3", "dns2": "~2.0.1", diff --git a/server/config.js b/server/config.js new file mode 100644 index 00000000..24ccfaa1 --- /dev/null +++ b/server/config.js @@ -0,0 +1,7 @@ +const args = require("args-parser")(process.argv); +const demoMode = args["demo"] || false; + +module.exports = { + args, + demoMode +}; diff --git a/server/database.js b/server/database.js index 47eca283..297df655 100644 --- a/server/database.js +++ b/server/database.js @@ -49,6 +49,7 @@ class Database { "patch-incident-table.sql": true, "patch-group-table.sql": true, "patch-monitor-push_token.sql": true, + "patch-http-monitor-method-body-and-headers.sql": true, } /** diff --git a/server/model/monitor.js b/server/model/monitor.js index 9089b830..4049a993 100644 --- a/server/model/monitor.js +++ b/server/model/monitor.js @@ -11,6 +11,7 @@ const { tcping, ping, dnsResolve, checkCertificate, checkStatusCode, getTotalCli const { R } = require("redbean-node"); const { BeanModel } = require("redbean-node/dist/bean-model"); const { Notification } = require("../notification"); +const { demoMode } = require("../config"); const version = require("../../package.json").version; const apicache = require("../modules/apicache"); @@ -54,6 +55,9 @@ class Monitor extends BeanModel { id: this.id, name: this.name, url: this.url, + method: this.method, + body: this.body, + headers: this.headers, hostname: this.hostname, port: this.port, maxretries: this.maxretries, @@ -137,11 +141,15 @@ class Monitor extends BeanModel { // Do not do any queries/high loading things before the "bean.ping" let startTime = dayjs().valueOf(); - let res = await axios.get(this.url, { + const options = { + url: this.url, + method: (this.method || "get").toLowerCase(), + ...(this.body ? { data: JSON.parse(this.body) } : {}), timeout: this.interval * 1000 * 0.8, headers: { "Accept": "*/*", "User-Agent": "Uptime-Kuma/" + version, + ...(this.headers ? JSON.parse(this.headers) : {}), }, httpsAgent: new https.Agent({ maxCachedSessions: 0, // Use Custom agent to disable session reuse (https://github.com/nodejs/node/issues/3940) @@ -151,7 +159,8 @@ class Monitor extends BeanModel { validateStatus: (status) => { return checkStatusCode(status, this.getAcceptedStatuscodes()); }, - }); + }; + let res = await axios.request(options); bean.msg = `${res.status} - ${res.statusText}`; bean.ping = dayjs().valueOf() - startTime; @@ -171,6 +180,10 @@ class Monitor extends BeanModel { debug("Cert Info Query Time: " + (dayjs().valueOf() - certInfoStartTime) + "ms"); } + if (process.env.UPTIME_KUMA_LOG_RESPONSE_BODY_MONITOR_ID == this.id) { + console.log(res.data); + } + if (this.type === "http") { bean.status = UP; } else { @@ -291,54 +304,13 @@ class Monitor extends BeanModel { let beatInterval = this.interval; - // * ? -> ANY STATUS = important [isFirstBeat] - // UP -> PENDING = not important - // * UP -> DOWN = important - // UP -> UP = not important - // PENDING -> PENDING = not important - // * PENDING -> DOWN = important - // PENDING -> UP = not important - // DOWN -> PENDING = this case not exists - // DOWN -> DOWN = not important - // * DOWN -> UP = important - let isImportant = isFirstBeat || - (previousBeat.status === UP && bean.status === DOWN) || - (previousBeat.status === DOWN && bean.status === UP) || - (previousBeat.status === PENDING && bean.status === DOWN); + let isImportant = Monitor.isImportantBeat(isFirstBeat, previousBeat.status, bean.status); // Mark as important if status changed, ignore pending pings, // Don't notify if disrupted changes to up if (isImportant) { bean.important = true; - - // Send only if the first beat is DOWN - if (!isFirstBeat || bean.status === DOWN) { - let notificationList = await R.getAll("SELECT notification.* FROM notification, monitor_notification WHERE monitor_id = ? AND monitor_notification.notification_id = notification.id ", [ - this.id, - ]); - - let text; - if (bean.status === UP) { - text = "✅ Up"; - } else { - text = "🔴 Down"; - } - - let msg = `[${this.name}] [${text}] ${bean.msg}`; - - for (let notification of notificationList) { - 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.log(e); - } - } - - // Clear Status Page Cache - apicache.clear(); - } - + await Monitor.sendNotification(isFirstBeat, this, bean); } else { bean.important = false; } @@ -363,6 +335,14 @@ class Monitor extends BeanModel { previousBeat = bean; if (! this.isStop) { + + if (demoMode) { + if (beatInterval < 20) { + console.log("beat interval too low, reset to 20s"); + beatInterval = 20; + } + } + this.heartbeatInterval = setTimeout(beat, beatInterval * 1000); } @@ -537,6 +517,53 @@ class Monitor extends BeanModel { io.to(userID).emit("uptime", monitorID, duration, uptime); } + static isImportantBeat(isFirstBeat, previousBeatStatus, currentBeatStatus) { + // * ? -> ANY STATUS = important [isFirstBeat] + // UP -> PENDING = not important + // * UP -> DOWN = important + // UP -> UP = not important + // PENDING -> PENDING = not important + // * PENDING -> DOWN = important + // PENDING -> UP = not important + // DOWN -> PENDING = this case not exists + // DOWN -> DOWN = not important + // * DOWN -> UP = important + let isImportant = isFirstBeat || + (previousBeatStatus === UP && currentBeatStatus === DOWN) || + (previousBeatStatus === DOWN && currentBeatStatus === UP) || + (previousBeatStatus === PENDING && currentBeatStatus === DOWN); + return isImportant; + } + + static async sendNotification(isFirstBeat, monitor, bean) { + if (!isFirstBeat || bean.status === DOWN) { + let notificationList = await R.getAll("SELECT notification.* FROM notification, monitor_notification WHERE monitor_id = ? AND monitor_notification.notification_id = notification.id ", [ + monitor.id, + ]); + + let text; + if (bean.status === UP) { + text = "✅ Up"; + } else { + text = "🔴 Down"; + } + + let msg = `[${monitor.name}] [${text}] ${bean.msg}`; + + for (let notification of notificationList) { + try { + await Notification.send(JSON.parse(notification.config), msg, await monitor.toJSON(), bean.toJSON()); + } catch (e) { + console.error("Cannot send notification to " + notification.name); + console.log(e); + } + } + + // Clear Status Page Cache + apicache.clear(); + } + } + } module.exports = Monitor; diff --git a/server/notification-providers/aliyun-sms.js b/server/notification-providers/aliyun-sms.js new file mode 100644 index 00000000..6a206320 --- /dev/null +++ b/server/notification-providers/aliyun-sms.js @@ -0,0 +1,108 @@ +const NotificationProvider = require("./notification-provider"); +const { DOWN, UP } = require("../../src/util"); +const { default: axios } = require("axios"); +const Crypto = require("crypto"); +const qs = require("qs"); + +class AliyunSMS extends NotificationProvider { + name = "AliyunSMS"; + + async send(notification, msg, monitorJSON = null, heartbeatJSON = null) { + let okMsg = "Sent Successfully."; + + try { + if (heartbeatJSON != null) { + let msgBody = JSON.stringify({ + name: monitorJSON["name"], + time: heartbeatJSON["time"], + status: this.statusToString(heartbeatJSON["status"]), + msg: heartbeatJSON["msg"], + }); + if (this.sendSms(notification, msgBody)) { + return okMsg; + } + } else { + let msgBody = JSON.stringify({ + name: "", + time: "", + status: "", + msg: msg, + }); + if (this.sendSms(notification, msgBody)) { + return okMsg; + } + } + } catch (error) { + this.throwGeneralAxiosError(error); + } + } + + async sendSms(notification, msgbody) { + let params = { + PhoneNumbers: notification.phonenumber, + TemplateCode: notification.templateCode, + SignName: notification.signName, + TemplateParam: msgbody, + AccessKeyId: notification.accessKeyId, + Format: "JSON", + SignatureMethod: "HMAC-SHA1", + SignatureVersion: "1.0", + SignatureNonce: Math.random().toString(), + Timestamp: new Date().toISOString(), + Action: "SendSms", + Version: "2017-05-25", + }; + + params.Signature = this.sign(params, notification.secretAccessKey); + let config = { + method: "POST", + url: "http://dysmsapi.aliyuncs.com/", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + data: qs.stringify(params), + }; + + let result = await axios(config); + if (result.data.Message == "OK") { + return true; + } + return false; + } + + /** Aliyun request sign */ + sign(param, AccessKeySecret) { + let param2 = {}; + let data = []; + + let oa = Object.keys(param).sort(); + + for (let i = 0; i < oa.length; i++) { + let key = oa[i]; + param2[key] = param[key]; + } + + for (let key in param2) { + data.push(`${encodeURIComponent(key)}=${encodeURIComponent(param2[key])}`); + } + + let StringToSign = `POST&${encodeURIComponent("/")}&${encodeURIComponent(data.join("&"))}`; + return Crypto + .createHmac("sha1", `${AccessKeySecret}&`) + .update(Buffer.from(StringToSign)) + .digest("base64"); + } + + statusToString(status) { + switch (status) { + case DOWN: + return "DOWN"; + case UP: + return "UP"; + default: + return status; + } + } +} + +module.exports = AliyunSMS; diff --git a/server/notification-providers/dingding.js b/server/notification-providers/dingding.js new file mode 100644 index 00000000..f099192d --- /dev/null +++ b/server/notification-providers/dingding.js @@ -0,0 +1,79 @@ +const NotificationProvider = require("./notification-provider"); +const { DOWN, UP } = require("../../src/util"); +const { default: axios } = require("axios"); +const Crypto = require("crypto"); + +class DingDing extends NotificationProvider { + name = "DingDing"; + + async send(notification, msg, monitorJSON = null, heartbeatJSON = null) { + let okMsg = "Sent Successfully."; + + try { + if (heartbeatJSON != null) { + let params = { + msgtype: "markdown", + markdown: { + title: monitorJSON["name"], + text: `## [${this.statusToString(heartbeatJSON["status"])}] \n > ${heartbeatJSON["msg"]} \n > Time(UTC):${heartbeatJSON["time"]}`, + } + }; + if (this.sendToDingDing(notification, params)) { + return okMsg; + } + } else { + let params = { + msgtype: "text", + text: { + content: msg + } + }; + if (this.sendToDingDing(notification, params)) { + return okMsg; + } + } + } catch (error) { + this.throwGeneralAxiosError(error); + } + } + + async sendToDingDing(notification, params) { + let timestamp = Date.now(); + + let config = { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + url: `${notification.webHookUrl}×tamp=${timestamp}&sign=${encodeURIComponent(this.sign(timestamp, notification.secretKey))}`, + data: JSON.stringify(params), + }; + + let result = await axios(config); + if (result.data.errmsg == "ok") { + return true; + } + return false; + } + + /** DingDing sign */ + sign(timestamp, secretKey) { + return Crypto + .createHmac("sha256", Buffer.from(secretKey, "utf8")) + .update(Buffer.from(`${timestamp}\n${secretKey}`, "utf8")) + .digest("base64"); + } + + statusToString(status) { + switch (status) { + case DOWN: + return "DOWN"; + case UP: + return "UP"; + default: + return status; + } + } +} + +module.exports = DingDing; diff --git a/server/notification-providers/feishu.js b/server/notification-providers/feishu.js new file mode 100644 index 00000000..a3e34030 --- /dev/null +++ b/server/notification-providers/feishu.js @@ -0,0 +1,83 @@ +const NotificationProvider = require("./notification-provider"); +const axios = require("axios"); +const { DOWN, UP } = require("../../src/util"); + +class Feishu extends NotificationProvider { + name = "Feishu"; + + async send(notification, msg, monitorJSON = null, heartbeatJSON = null) { + let okMsg = "Sent Successfully."; + let feishuWebHookUrl = notification.feishuWebHookUrl; + + try { + if (heartbeatJSON == null) { + let testdata = { + msg_type: "text", + content: { + text: msg, + }, + }; + await axios.post(feishuWebHookUrl, testdata); + return okMsg; + } + + if (heartbeatJSON["status"] == DOWN) { + let downdata = { + msg_type: "post", + content: { + post: { + zh_cn: { + title: "UptimeKuma Alert: " + monitorJSON["name"], + content: [ + [ + { + tag: "text", + text: + "[Down] " + + heartbeatJSON["msg"] + + "\nTime (UTC): " + + heartbeatJSON["time"], + }, + ], + ], + }, + }, + }, + }; + await axios.post(feishuWebHookUrl, downdata); + return okMsg; + } + + if (heartbeatJSON["status"] == UP) { + let updata = { + msg_type: "post", + content: { + post: { + zh_cn: { + title: "UptimeKuma Alert: " + monitorJSON["name"], + content: [ + [ + { + tag: "text", + text: + "[Up] " + + heartbeatJSON["msg"] + + "\nTime (UTC): " + + heartbeatJSON["time"], + }, + ], + ], + }, + }, + }, + }; + await axios.post(feishuWebHookUrl, updata); + return okMsg; + } + } catch (error) { + this.throwGeneralAxiosError(error); + } + } +} + +module.exports = Feishu; diff --git a/server/notification-providers/slack.js b/server/notification-providers/slack.js index 5132ba97..b4dad6fe 100644 --- a/server/notification-providers/slack.js +++ b/server/notification-providers/slack.js @@ -39,8 +39,9 @@ class Slack extends NotificationProvider { } const time = heartbeatJSON["time"]; + const textMsg = "Uptime Kuma Alert"; let data = { - "text": "Uptime Kuma Alert", + "text": monitorJSON ? textMsg + `: ${monitorJSON.name}` : textMsg, "channel": notification.slackchannel, "username": notification.slackusername, "icon_emoji": notification.slackiconemo, diff --git a/server/notification-providers/smtp.js b/server/notification-providers/smtp.js index ecb583eb..60068eb7 100644 --- a/server/notification-providers/smtp.js +++ b/server/notification-providers/smtp.js @@ -1,5 +1,6 @@ const nodemailer = require("nodemailer"); const NotificationProvider = require("./notification-provider"); +const { DOWN, UP } = require("../../src/util"); class SMTP extends NotificationProvider { @@ -20,6 +21,56 @@ class SMTP extends NotificationProvider { pass: notification.smtpPassword, }; } + // Lets start with default subject and empty string for custom one + let subject = msg; + + // Change the subject if: + // - The msg ends with "Testing" or + // - Actual Up/Down Notification + if ((monitorJSON && heartbeatJSON) || msg.endsWith("Testing")) { + let customSubject = ""; + + // Our subject cannot end with whitespace it's often raise spam score + // Once I got "Cannot read property 'trim' of undefined", better be safe than sorry + if (notification.customSubject) { + customSubject = notification.customSubject.trim(); + } + + // If custom subject is not empty, change subject for notification + if (customSubject !== "") { + + // Replace "MACROS" with corresponding variable + let replaceName = new RegExp("{{NAME}}", "g"); + let replaceHostnameOrURL = new RegExp("{{HOSTNAME_OR_URL}}", "g"); + let replaceStatus = new RegExp("{{STATUS}}", "g"); + + // Lets start with dummy values to simplify code + let monitorName = "Test"; + let monitorHostnameOrURL = "testing.hostname"; + let serviceStatus = "⚠️ Test"; + + if (monitorJSON !== null) { + monitorName = monitorJSON["name"]; + + if (monitorJSON["type"] === "http" || monitorJSON["type"] === "keyword") { + monitorHostnameOrURL = monitorJSON["url"]; + } else { + monitorHostnameOrURL = monitorJSON["hostname"]; + } + } + + if (heartbeatJSON !== null) { + serviceStatus = (heartbeatJSON["status"] === DOWN) ? "🔴 Down" : "✅ Up"; + } + + // Break replace to one by line for better readability + customSubject = customSubject.replace(replaceStatus, serviceStatus); + customSubject = customSubject.replace(replaceName, monitorName); + customSubject = customSubject.replace(replaceHostnameOrURL, monitorHostnameOrURL); + + subject = customSubject; + } + } let transporter = nodemailer.createTransport(config); @@ -34,7 +85,7 @@ class SMTP extends NotificationProvider { cc: notification.smtpCC, bcc: notification.smtpBCC, to: notification.smtpTo, - subject: msg, + subject: subject, text: bodyTextContent, tls: { rejectUnauthorized: notification.smtpIgnoreTLSError || false, diff --git a/server/notification.js b/server/notification.js index 5b104bf8..658216f9 100644 --- a/server/notification.js +++ b/server/notification.js @@ -18,6 +18,9 @@ const SMTP = require("./notification-providers/smtp"); const Teams = require("./notification-providers/teams"); const Telegram = require("./notification-providers/telegram"); const Webhook = require("./notification-providers/webhook"); +const Feishu = require("./notification-providers/feishu"); +const AliyunSms = require("./notification-providers/aliyun-sms"); +const DingDing = require("./notification-providers/dingding"); class Notification { @@ -30,11 +33,14 @@ class Notification { const list = [ new Apprise(), + new AliyunSms(), + new DingDing(), new Discord(), new Teams(), new Gotify(), new Line(), new LunaSea(), + new Feishu(), new Mattermost(), new Matrix(), new Octopush(), diff --git a/server/ping-lite.js b/server/ping-lite.js index 0af0e970..b2d6405a 100644 --- a/server/ping-lite.js +++ b/server/ping-lite.js @@ -4,10 +4,7 @@ const net = require("net"); const spawn = require("child_process").spawn; const events = require("events"); const fs = require("fs"); -const WIN = /^win/.test(process.platform); -const LIN = /^linux/.test(process.platform); -const MAC = /^darwin/.test(process.platform); -const FBSD = /^freebsd/.test(process.platform); +const util = require("./util-server"); module.exports = Ping; @@ -23,12 +20,12 @@ function Ping(host, options) { const timeout = 10; - if (WIN) { + if (util.WIN) { this._bin = "c:/windows/system32/ping.exe"; this._args = (options.args) ? options.args : [ "-n", "1", "-w", timeout * 1000, host ]; this._regmatch = /[><=]([0-9.]+?)ms/; - } else if (LIN) { + } else if (util.LIN) { this._bin = "/bin/ping"; const defaultArgs = [ "-n", "-w", timeout, "-c", "1", host ]; @@ -40,7 +37,7 @@ function Ping(host, options) { this._args = (options.args) ? options.args : defaultArgs; this._regmatch = /=([0-9.]+?) ms/; - } else if (MAC) { + } else if (util.MAC) { if (net.isIPv6(host) || options.ipv6) { this._bin = "/sbin/ping6"; @@ -51,7 +48,7 @@ function Ping(host, options) { this._args = (options.args) ? options.args : [ "-n", "-t", timeout, "-c", "1", host ]; this._regmatch = /=([0-9.]+?) ms/; - } else if (FBSD) { + } else if (util.FBSD) { this._bin = "/sbin/ping"; const defaultArgs = [ "-n", "-t", timeout, "-c", "1", host ]; @@ -101,6 +98,9 @@ Ping.prototype.send = function (callback) { }); this._ping.stdout.on("data", function (data) { // log stdout + if (util.WIN) { + data = convertOutput(data); + } this._stdout = (this._stdout || "") + data; }); @@ -112,6 +112,9 @@ Ping.prototype.send = function (callback) { }); this._ping.stderr.on("data", function (data) { // log stderr + if (util.WIN) { + data = convertOutput(data); + } this._stderr = (this._stderr || "") + data; }); @@ -157,3 +160,19 @@ Ping.prototype.start = function (callback) { Ping.prototype.stop = function () { clearInterval(this._i); }; + +/** + * Try to convert to UTF-8 for Windows, as the ping's output on Windows is not UTF-8 and could be in other languages + * Thank @pemassi + * https://github.com/louislam/uptime-kuma/issues/570#issuecomment-941984094 + * @param data + * @returns {string} + */ +function convertOutput(data) { + if (util.WIN) { + if (data) { + return util.convertToUTF8(data); + } + } + return data; +} diff --git a/server/prometheus.js b/server/prometheus.js index c27f87f0..870581d2 100644 --- a/server/prometheus.js +++ b/server/prometheus.js @@ -6,7 +6,7 @@ const commonLabels = [ "monitor_url", "monitor_hostname", "monitor_port", -] +]; const monitor_cert_days_remaining = new PrometheusClient.Gauge({ name: "monitor_cert_days_remaining", @@ -41,45 +41,46 @@ class Prometheus { monitor_url: monitor.url, monitor_hostname: monitor.hostname, monitor_port: monitor.port - } + }; } update(heartbeat, tlsInfo) { + if (typeof tlsInfo !== "undefined") { try { - let is_valid = 0 + let is_valid = 0; if (tlsInfo.valid == true) { - is_valid = 1 + is_valid = 1; } else { - is_valid = 0 + is_valid = 0; } - monitor_cert_is_valid.set(this.monitorLabelValues, is_valid) + monitor_cert_is_valid.set(this.monitorLabelValues, is_valid); } catch (e) { - console.error(e) + console.error(e); } try { - monitor_cert_days_remaining.set(this.monitorLabelValues, tlsInfo.certInfo.daysRemaining) + monitor_cert_days_remaining.set(this.monitorLabelValues, tlsInfo.certInfo.daysRemaining); } catch (e) { - console.error(e) + console.error(e); } } try { - monitor_status.set(this.monitorLabelValues, heartbeat.status) + monitor_status.set(this.monitorLabelValues, heartbeat.status); } catch (e) { - console.error(e) + console.error(e); } try { if (typeof heartbeat.ping === "number") { - monitor_response_time.set(this.monitorLabelValues, heartbeat.ping) + monitor_response_time.set(this.monitorLabelValues, heartbeat.ping); } else { // Is it good? - monitor_response_time.set(this.monitorLabelValues, -1) + monitor_response_time.set(this.monitorLabelValues, -1); } } catch (e) { - console.error(e) + console.error(e); } } @@ -87,4 +88,4 @@ class Prometheus { module.exports = { Prometheus -} +}; diff --git a/server/routers/api-router.js b/server/routers/api-router.js index 0da1fd70..fbe8136e 100644 --- a/server/routers/api-router.js +++ b/server/routers/api-router.js @@ -5,7 +5,7 @@ const server = require("../server"); const apicache = require("../modules/apicache"); const Monitor = require("../model/monitor"); const dayjs = require("dayjs"); -const { UP } = require("../../src/util"); +const { UP, flipStatus, debug } = require("../../src/util"); let router = express.Router(); let cache = apicache.middleware; @@ -18,9 +18,10 @@ router.get("/api/entry-page", async (_, response) => { router.get("/api/push/:pushToken", async (request, response) => { try { + let pushToken = request.params.pushToken; let msg = request.query.msg || "OK"; - let ping = request.query.ping; + let ping = request.query.ping || null; let monitor = await R.findOne("monitor", " push_token = ? AND active = 1 ", [ pushToken @@ -30,12 +31,40 @@ router.get("/api/push/:pushToken", async (request, response) => { throw new Error("Monitor not found or not active."); } + const previousHeartbeat = await R.getRow(` + SELECT status, time FROM heartbeat + WHERE id = (select MAX(id) from heartbeat where monitor_id = ?) + `, [ + monitor.id + ]); + + let status = UP; + if (monitor.isUpsideDown()) { + status = flipStatus(status); + } + + let isFirstBeat = true; + let previousStatus = status; + let duration = 0; + let bean = R.dispense("heartbeat"); - bean.monitor_id = monitor.id; bean.time = R.isoDateTime(dayjs.utc()); - bean.status = UP; + + if (previousHeartbeat) { + isFirstBeat = false; + previousStatus = previousHeartbeat.status; + duration = dayjs(bean.time).diff(dayjs(previousHeartbeat.time), "second"); + } + + debug("PreviousStatus: " + previousStatus); + debug("Current Status: " + status); + + bean.important = Monitor.isImportantBeat(isFirstBeat, previousStatus, status); + bean.monitor_id = monitor.id; + bean.status = status; bean.msg = msg; bean.ping = ping; + bean.duration = duration; await R.store(bean); @@ -45,6 +74,11 @@ router.get("/api/push/:pushToken", async (request, response) => { response.json({ ok: true, }); + + if (bean.important) { + await Monitor.sendNotification(isFirstBeat, monitor, bean); + } + } catch (e) { response.json({ ok: false, diff --git a/server/server.js b/server/server.js index d78ab93b..64966991 100644 --- a/server/server.js +++ b/server/server.js @@ -1,4 +1,9 @@ console.log("Welcome to Uptime Kuma"); +const args = require("args-parser")(process.argv); +const { sleep, debug, getRandomInt, genSecret } = require("../src/util"); +const config = require("./config"); + +debug(args); if (! process.env.NODE_ENV) { process.env.NODE_ENV = "production"; @@ -6,8 +11,6 @@ if (! process.env.NODE_ENV) { console.log("Node Env: " + process.env.NODE_ENV); -const { sleep, debug, getRandomInt, genSecret } = require("../src/util"); - console.log("Importing Node libraries"); const fs = require("fs"); const http = require("http"); @@ -37,7 +40,7 @@ console.log("Importing this project modules"); debug("Importing Monitor"); const Monitor = require("./model/monitor"); debug("Importing Settings"); -const { getSettings, setSettings, setting, initJWTSecret, checkLogin, startUnitTest } = require("./util-server"); +const { getSettings, setSettings, setting, initJWTSecret, checkLogin, startUnitTest, FBSD } = require("./util-server"); debug("Importing Notification"); const { Notification } = require("./notification"); @@ -53,19 +56,33 @@ const { basicAuth } = require("./auth"); const { login } = require("./auth"); const passwordHash = require("./password-hash"); -const args = require("args-parser")(process.argv); - const checkVersion = require("./check-version"); console.info("Version: " + checkVersion.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); +let hostname = process.env.UPTIME_KUMA_HOST || args.host; + +// Also read HOST if not FreeBSD, as HOST is a system environment variable in FreeBSD +if (!hostname && !FBSD) { + hostname = process.env.HOST; +} + +if (hostname) { + console.log("Custom hostname: " + hostname); +} + +const port = parseInt(process.env.UPTIME_KUMA_PORT || process.env.PORT || args.port || 3001); // SSL -const sslKey = process.env.SSL_KEY || args["ssl-key"] || undefined; -const sslCert = process.env.SSL_CERT || args["ssl-cert"] || undefined; +const sslKey = process.env.UPTIME_KUMA_SSL_KEY || process.env.SSL_KEY || args["ssl-key"] || undefined; +const sslCert = process.env.UPTIME_KUMA_SSL_CERT || process.env.SSL_CERT || args["ssl-cert"] || undefined; + +// 2FA / notp verification defaults +const twofa_verification_opts = { + "window": 1, + "time": 30 +} /** * Run unit test after the server is ready @@ -73,6 +90,10 @@ const sslCert = process.env.SSL_CERT || args["ssl-cert"] || undefined; */ const testMode = !!args["test"] || false; +if (config.demoMode) { + console.log("==== Demo Mode ===="); +} + console.log("Creating express and socket.io instance"); const app = express(); @@ -260,7 +281,7 @@ exports.entryPage = "dashboard"; } if (data.token) { - let verify = notp.totp.verify(data.token, user.twofa_secret); + let verify = notp.totp.verify(data.token, user.twofa_secret, twofa_verification_opts); if (verify && verify.delta == 0) { callback({ @@ -378,7 +399,7 @@ exports.entryPage = "dashboard"; socket.userID, ]); - let verify = notp.totp.verify(token, user.twofa_secret); + let verify = notp.totp.verify(token, user.twofa_secret, twofa_verification_opts); if (verify && verify.delta == 0) { callback({ @@ -504,6 +525,9 @@ exports.entryPage = "dashboard"; bean.name = monitor.name; bean.type = monitor.type; bean.url = monitor.url; + bean.method = monitor.method; + bean.body = monitor.body; + bean.headers = monitor.headers; bean.interval = monitor.interval; bean.retryInterval = monitor.retryInterval; bean.hostname = monitor.hostname; @@ -1029,6 +1053,9 @@ exports.entryPage = "dashboard"; name: monitorListData[i].name, type: monitorListData[i].type, url: monitorListData[i].url, + method: monitorListData[i].method || "GET", + body: monitorListData[i].body, + headers: monitorListData[i].headers, interval: monitorListData[i].interval, retryInterval: retryInterval, hostname: monitorListData[i].hostname, diff --git a/server/util-server.js b/server/util-server.js index 5a486d3a..7be922dd 100644 --- a/server/util-server.js +++ b/server/util-server.js @@ -6,6 +6,14 @@ const passwordHash = require("./password-hash"); const dayjs = require("dayjs"); const { Resolver } = require("dns"); const child_process = require("child_process"); +const iconv = require("iconv-lite"); +const chardet = require("chardet"); + +// From ping-lite +exports.WIN = /^win/.test(process.platform); +exports.LIN = /^linux/.test(process.platform); +exports.MAC = /^darwin/.test(process.platform); +exports.FBSD = /^freebsd/.test(process.platform); /** * Init or reset JWT secret @@ -313,3 +321,14 @@ exports.startUnitTest = async () => { process.exit(code); }); }; + +/** + * @param body : Buffer + * @returns {string} + */ +exports.convertToUTF8 = (body) => { + const guessEncoding = chardet.detect(body); + //debug("Guess Encoding: " + guessEncoding); + const str = iconv.decode(body, guessEncoding); + return str.toString(); +}; diff --git a/src/assets/app.scss b/src/assets/app.scss index 2cbec5c0..e1a5d052 100644 --- a/src/assets/app.scss +++ b/src/assets/app.scss @@ -14,6 +14,10 @@ h2 { font-size: 26px; } +textarea.form-control { + border-radius: 19px; +} + ::-webkit-scrollbar { width: 10px; } diff --git a/src/assets/multiselect.scss b/src/assets/multiselect.scss index 30023076..53b47c16 100644 --- a/src/assets/multiselect.scss +++ b/src/assets/multiselect.scss @@ -21,7 +21,7 @@ } .multiselect__tag { - border-radius: 50rem; + border-radius: $border-radius; margin-bottom: 0; padding: 6px 26px 6px 10px; background: $primary !important; diff --git a/src/components/HeartbeatBar.vue b/src/components/HeartbeatBar.vue index 4dc2c712..e62b95df 100644 --- a/src/components/HeartbeatBar.vue +++ b/src/components/HeartbeatBar.vue @@ -186,7 +186,7 @@ export default { .beat { display: inline-block; background-color: $primary; - border-radius: 50rem; + border-radius: $border-radius; &.empty { background-color: aliceblue; diff --git a/src/components/notifications/AliyunSms.vue b/src/components/notifications/AliyunSms.vue new file mode 100644 index 00000000..2c25a3a9 --- /dev/null +++ b/src/components/notifications/AliyunSms.vue @@ -0,0 +1,25 @@ + diff --git a/src/components/notifications/DingDing.vue b/src/components/notifications/DingDing.vue new file mode 100644 index 00000000..713859ac --- /dev/null +++ b/src/components/notifications/DingDing.vue @@ -0,0 +1,16 @@ + diff --git a/src/components/notifications/Feishu.vue b/src/components/notifications/Feishu.vue new file mode 100644 index 00000000..6e00a314 --- /dev/null +++ b/src/components/notifications/Feishu.vue @@ -0,0 +1,15 @@ + diff --git a/src/components/notifications/SMTP.vue b/src/components/notifications/SMTP.vue index 72934cda..483917e3 100644 --- a/src/components/notifications/SMTP.vue +++ b/src/components/notifications/SMTP.vue @@ -57,6 +57,18 @@ + +
+ + +
+ (leave blank for default one)
+ {{NAME}}: Service Name
+ {{HOSTNAME_OR_URL}}: Hostname or URL
+ {{URL}}: URL
+ {{STATUS}}: Status
+
+
- diff --git a/src/pages/Settings.vue b/src/pages/Settings.vue index e55d442b..4e421c0a 100644 --- a/src/pages/Settings.vue +++ b/src/pages/Settings.vue @@ -342,6 +342,12 @@

Utilizzare con attenzione.

+ + + +