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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Sms template must contain parameters: ${name} ${time} ${status} ${msg}