mirror of
				https://github.com/louislam/uptime-kuma.git
				synced 2025-11-01 03:49:24 +08:00 
			
		
		
		
	Merge branch 'master' of https://github.com/louislam/uptime-kuma into status-page-expiry
This commit is contained in:
		
							
								
								
									
										7
									
								
								.github/workflows/auto-test.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										7
									
								
								.github/workflows/auto-test.yml
									
									
									
									
										vendored
									
									
								
							| @@ -1,4 +1,4 @@ | ||||
| # This workflow will do a clean install of node dependencies, cache/restore them, build the source code and run tests across different versions of node | ||||
| # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node | ||||
| # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions | ||||
|  | ||||
| name: Auto Test | ||||
| @@ -33,7 +33,6 @@ jobs: | ||||
|       uses: actions/setup-node@v3 | ||||
|       with: | ||||
|         node-version: ${{ matrix.node }} | ||||
|         cache: 'npm' | ||||
|     - run: npm install npm@latest -g | ||||
|     - run: npm install | ||||
|     - run: npm run build | ||||
| @@ -62,7 +61,6 @@ jobs: | ||||
|         uses: actions/setup-node@v3 | ||||
|         with: | ||||
|           node-version: ${{ matrix.node }} | ||||
|           cache: 'npm' | ||||
|       - run: npm install npm@latest -g | ||||
|       - run: npm ci --production | ||||
|  | ||||
| @@ -77,7 +75,6 @@ jobs: | ||||
|       uses: actions/setup-node@v3 | ||||
|       with: | ||||
|         node-version: 14 | ||||
|         cache: 'npm' | ||||
|     - run: npm install | ||||
|     - run: npm run lint | ||||
|  | ||||
| @@ -92,7 +89,6 @@ jobs: | ||||
|       uses: actions/setup-node@v3 | ||||
|       with: | ||||
|         node-version: 14 | ||||
|         cache: 'npm' | ||||
|     - run: npm install | ||||
|     - run: npm run build | ||||
|     - run: npm run cy:test | ||||
| @@ -108,7 +104,6 @@ jobs: | ||||
|       uses: actions/setup-node@v3 | ||||
|       with: | ||||
|         node-version: 14 | ||||
|         cache: 'npm' | ||||
|     - run: npm install | ||||
|     - run: npm run build | ||||
|     - run: npm run cy:run:unit | ||||
|   | ||||
| @@ -23,7 +23,7 @@ It is a temporary live demo, all data will be deleted after 10 minutes. Use the | ||||
|  | ||||
| ## ⭐ Features | ||||
|  | ||||
| * Monitoring uptime for HTTP(s) / TCP / HTTP(s) Keyword / Ping / DNS Record / Push / Steam Game Server / Docker Containers | ||||
| * Monitoring uptime for HTTP(s) / TCP / HTTP(s) Keyword / HTTP(s) Json Query / Ping / DNS Record / Push / Steam Game Server / Docker Containers | ||||
| * Fancy, Reactive, Fast UI/UX | ||||
| * Notifications via Telegram, Discord, Gotify, Slack, Pushover, Email (SMTP), and [90+ notification services, click here for the full list](https://github.com/louislam/uptime-kuma/tree/master/src/components/notifications) | ||||
| * 20 second intervals | ||||
|   | ||||
							
								
								
									
										7
									
								
								db/patch-add-invert-keyword.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								db/patch-add-invert-keyword.sql
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,7 @@ | ||||
| -- You should not modify if this have pushed to Github, unless it does serious wrong with the db. | ||||
| BEGIN TRANSACTION; | ||||
|  | ||||
| ALTER TABLE monitor | ||||
|     ADD invert_keyword BOOLEAN default 0 not null; | ||||
|  | ||||
| COMMIT; | ||||
							
								
								
									
										10
									
								
								db/patch-added-json-query.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								db/patch-added-json-query.sql
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,10 @@ | ||||
| -- You should not modify if this have pushed to Github, unless it does serious wrong with the db. | ||||
| BEGIN TRANSACTION; | ||||
|  | ||||
| ALTER TABLE monitor | ||||
| 	ADD json_path TEXT; | ||||
|  | ||||
| ALTER TABLE monitor | ||||
| 	ADD expected_value VARCHAR(255); | ||||
|  | ||||
| COMMIT; | ||||
							
								
								
									
										9
									
								
								extra/test-docker.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								extra/test-docker.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | ||||
| // Check if docker is running | ||||
| const { exec } = require("child_process"); | ||||
|  | ||||
| exec("docker ps", (err, stdout, stderr) => { | ||||
|     if (err) { | ||||
|         console.error("Docker is not running. Please start docker and try again."); | ||||
|         process.exit(1); | ||||
|     } | ||||
| }); | ||||
							
								
								
									
										5383
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										5383
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										34
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										34
									
								
								package.json
									
									
									
									
									
								
							| @@ -1,6 +1,6 @@ | ||||
| { | ||||
|     "name": "uptime-kuma", | ||||
|     "version": "1.22.0", | ||||
|     "version": "1.22.1", | ||||
|     "license": "MIT", | ||||
|     "repository": { | ||||
|         "type": "git", | ||||
| @@ -34,12 +34,12 @@ | ||||
|         "build-docker-builder-go": "docker buildx build -f docker/builder-go.dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:builder-go . --push", | ||||
|         "build-docker-alpine": "node ./extra/env2arg.js 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:$VERSION-alpine --target release . --push", | ||||
|         "build-docker-debian": "node ./extra/env2arg.js 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:$VERSION -t louislam/uptime-kuma:debian -t louislam/uptime-kuma:1-debian -t louislam/uptime-kuma:$VERSION-debian --target release . --push", | ||||
|         "build-docker-nightly": "npm run build && 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": "node ./extra/test-docker.js && npm run build && 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 docker/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 -f docker/dockerfile --platform linux/amd64 -t louislam/uptime-kuma:nightly-amd64 --target nightly . --push --progress plain", | ||||
|         "build-docker-pr-test": "docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64 -t louislam/uptime-kuma:pr-test --target pr-test . --push", | ||||
|         "upload-artifacts": "docker buildx build -f docker/dockerfile --platform linux/amd64 -t louislam/uptime-kuma:upload-artifact --build-arg VERSION --build-arg GITHUB_TOKEN --target upload-artifact . --progress plain", | ||||
|         "setup": "git checkout 1.22.0 && npm ci --production && npm run download-dist", | ||||
|         "setup": "git checkout 1.22.1 && npm ci --production && npm run download-dist", | ||||
|         "download-dist": "node extra/download-dist.js", | ||||
|         "mark-as-nightly": "node extra/mark-as-nightly.js", | ||||
|         "reset-password": "node extra/reset-password.js", | ||||
| @@ -54,8 +54,8 @@ | ||||
|         "simple-mqtt-server": "node extra/simple-mqtt-server.js", | ||||
|         "update-language-files": "cd extra/update-language-files && node index.js && cross-env-shell eslint ../../src/languages/$npm_config_language.js --fix", | ||||
|         "ncu-patch": "npm-check-updates -u -t patch", | ||||
|         "release-final": "node extra/update-version.js && npm run build-docker && node ./extra/press-any-key.js && npm run upload-artifacts && node ./extra/update-wiki-version.js", | ||||
|         "release-beta": "node extra/beta/update-version.js && npm run build && node ./extra/env2arg.js docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:$VERSION -t louislam/uptime-kuma:beta .  --target release --push && node ./extra/press-any-key.js && npm run upload-artifacts", | ||||
|         "release-final": "node ./extra/test-docker.js && node extra/update-version.js && npm run build-docker && node ./extra/press-any-key.js && npm run upload-artifacts && node ./extra/update-wiki-version.js", | ||||
|         "release-beta": "node ./extra/test-docker.js && node extra/beta/update-version.js && npm run build && node ./extra/env2arg.js docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:$VERSION -t louislam/uptime-kuma:beta .  --target release --push && node ./extra/press-any-key.js && npm run upload-artifacts", | ||||
|         "git-remove-tag": "git tag -d", | ||||
|         "build-dist-and-restart": "npm run build && npm run start-server-dev", | ||||
|         "start-pr-test": "node extra/checkout-pr.js && npm install && npm run dev", | ||||
| @@ -97,9 +97,11 @@ | ||||
|         "https-proxy-agent": "~5.0.1", | ||||
|         "iconv-lite": "~0.6.3", | ||||
|         "jsesc": "~3.0.2", | ||||
|         "jsonata": "^2.0.3", | ||||
|         "jsonwebtoken": "~9.0.0", | ||||
|         "jwt-decode": "~3.1.2", | ||||
|         "limiter": "~2.1.0", | ||||
|         "liquidjs": "^10.7.0", | ||||
|         "mongodb": "~4.14.0", | ||||
|         "mqtt": "~4.3.7", | ||||
|         "mssql": "~8.1.4", | ||||
| @@ -115,7 +117,7 @@ | ||||
|         "playwright-core": "~1.35.1", | ||||
|         "prom-client": "~13.2.0", | ||||
|         "prometheus-api-metrics": "~3.2.1", | ||||
|         "protobufjs": "~7.1.1", | ||||
|         "protobufjs": "~7.2.4", | ||||
|         "qs": "~6.10.4", | ||||
|         "redbean-node": "~0.3.0", | ||||
|         "redis": "~4.5.1", | ||||
| @@ -128,7 +130,7 @@ | ||||
|     }, | ||||
|     "devDependencies": { | ||||
|         "@actions/github": "~5.0.1", | ||||
|         "@babel/eslint-parser": "~7.17.0", | ||||
|         "@babel/eslint-parser": "^7.22.7", | ||||
|         "@babel/preset-env": "^7.15.8", | ||||
|         "@fortawesome/fontawesome-svg-core": "~1.2.36", | ||||
|         "@fortawesome/free-regular-svg-icons": "~5.15.4", | ||||
| @@ -136,9 +138,9 @@ | ||||
|         "@fortawesome/vue-fontawesome": "~3.0.0-5", | ||||
|         "@popperjs/core": "~2.10.2", | ||||
|         "@types/bootstrap": "~5.1.9", | ||||
|         "@vitejs/plugin-legacy": "~2.1.0", | ||||
|         "@vitejs/plugin-vue": "~3.1.0", | ||||
|         "@vue/compiler-sfc": "~3.2.36", | ||||
|         "@vitejs/plugin-legacy": "~4.1.0", | ||||
|         "@vitejs/plugin-vue": "~4.2.3", | ||||
|         "@vue/compiler-sfc": "~3.3.4", | ||||
|         "@vuepic/vue-datepicker": "~3.4.8", | ||||
|         "aedes": "^0.46.3", | ||||
|         "babel-plugin-rewire": "~1.2.0", | ||||
| @@ -149,16 +151,16 @@ | ||||
|         "core-js": "~3.26.1", | ||||
|         "cronstrue": "~2.24.0", | ||||
|         "cross-env": "~7.0.3", | ||||
|         "cypress": "^10.1.0", | ||||
|         "cypress": "^12.17.0", | ||||
|         "delay": "^5.0.0", | ||||
|         "dns2": "~2.0.1", | ||||
|         "dompurify": "~2.4.3", | ||||
|         "eslint": "~8.14.0", | ||||
|         "eslint-plugin-vue": "~8.7.1", | ||||
|         "favico.js": "~0.3.10", | ||||
|         "jest": "~27.2.5", | ||||
|         "jest": "~29.6.1", | ||||
|         "marked": "~4.2.5", | ||||
|         "node-ssh": "~13.0.1", | ||||
|         "node-ssh": "~13.1.0", | ||||
|         "postcss-html": "~1.5.0", | ||||
|         "postcss-rtlcss": "~3.7.2", | ||||
|         "postcss-scss": "~4.0.4", | ||||
| @@ -166,15 +168,15 @@ | ||||
|         "qrcode": "~1.5.0", | ||||
|         "rollup-plugin-visualizer": "^5.6.0", | ||||
|         "sass": "~1.42.1", | ||||
|         "stylelint": "~15.9.0", | ||||
|         "stylelint": "^15.10.1", | ||||
|         "stylelint-config-standard": "~25.0.0", | ||||
|         "terser": "~5.15.0", | ||||
|         "timezones-list": "~3.0.1", | ||||
|         "typescript": "~4.4.4", | ||||
|         "v-pagination-3": "~0.1.7", | ||||
|         "vite": "~3.2.7", | ||||
|         "vite": "~4.4.1", | ||||
|         "vite-plugin-compression": "^0.5.1", | ||||
|         "vue": "~3.2.47", | ||||
|         "vue": "~3.3.4", | ||||
|         "vue-chartjs": "~5.2.0", | ||||
|         "vue-confirm-dialog": "~1.0.2", | ||||
|         "vue-contenteditable": "~3.0.4", | ||||
|   | ||||
| @@ -1,27 +1,33 @@ | ||||
| const { setSetting, setting } = require("./util-server"); | ||||
| const axios = require("axios"); | ||||
| const compareVersions = require("compare-versions"); | ||||
| const { log } = require("../src/util"); | ||||
|  | ||||
| exports.version = require("../package.json").version; | ||||
| exports.latestVersion = null; | ||||
|  | ||||
| // How much time in ms to wait between update checks | ||||
| const UPDATE_CHECKER_INTERVAL_MS = 1000 * 60 * 60 * 48; | ||||
| const UPDATE_CHECKER_LATEST_VERSION_URL = "https://uptime.kuma.pet/version"; | ||||
|  | ||||
| let interval; | ||||
|  | ||||
| /** Start 48 hour check interval */ | ||||
| exports.startInterval = () => { | ||||
|     let check = async () => { | ||||
|         if (await setting("checkUpdate") === false) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         log.debug("update-checker", "Retrieving latest versions"); | ||||
|  | ||||
|         try { | ||||
|             const res = await axios.get("https://uptime.kuma.pet/version"); | ||||
|             const res = await axios.get(UPDATE_CHECKER_LATEST_VERSION_URL); | ||||
|  | ||||
|             // For debug | ||||
|             if (process.env.TEST_CHECK_VERSION === "1") { | ||||
|                 res.data.slow = "1000.0.0"; | ||||
|             } | ||||
|  | ||||
|             if (await setting("checkUpdate") === false) { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             let checkBeta = await setting("checkBeta"); | ||||
|  | ||||
|             if (checkBeta && res.data.beta) { | ||||
| @@ -35,12 +41,14 @@ exports.startInterval = () => { | ||||
|                 exports.latestVersion = res.data.slow; | ||||
|             } | ||||
|  | ||||
|         } catch (_) { } | ||||
|         } catch (_) { | ||||
|             log.info("update-checker", "Failed to check for new versions"); | ||||
|         } | ||||
|  | ||||
|     }; | ||||
|  | ||||
|     check(); | ||||
|     interval = setInterval(check, 3600 * 1000 * 48); | ||||
|     interval = setInterval(check, UPDATE_CHECKER_INTERVAL_MS); | ||||
| }; | ||||
|  | ||||
| /** | ||||
|   | ||||
| @@ -3,7 +3,6 @@ const { R } = require("redbean-node"); | ||||
| const { setSetting, setting } = require("./util-server"); | ||||
| const { log, sleep } = require("../src/util"); | ||||
| const knex = require("knex"); | ||||
| const { PluginsManager } = require("./plugins-manager"); | ||||
|  | ||||
| /** | ||||
|  * Database & App Data Folder | ||||
| @@ -72,6 +71,8 @@ class Database { | ||||
|         "patch-monitor-tls.sql": true, | ||||
|         "patch-maintenance-cron.sql": true, | ||||
|         "patch-add-parent-monitor.sql": true, | ||||
|         "patch-add-invert-keyword.sql": true, | ||||
|         "patch-added-json-query.sql": true, | ||||
|         "patch-add-certificate-expiry-status-page.sql": true, | ||||
|     }; | ||||
|  | ||||
| @@ -91,12 +92,6 @@ class Database { | ||||
|         // Data Directory (must be end with "/") | ||||
|         Database.dataDir = process.env.DATA_DIR || args["data-dir"] || "./data/"; | ||||
|  | ||||
|         // Plugin feature is working only if the dataDir = "./data"; | ||||
|         if (Database.dataDir !== "./data/") { | ||||
|             log.warn("PLUGIN", "Warning: In order to enable plugin feature, you need to use the default data directory: ./data/"); | ||||
|             PluginsManager.disable = true; | ||||
|         } | ||||
|  | ||||
|         Database.path = Database.dataDir + "kuma.db"; | ||||
|         if (! fs.existsSync(Database.dataDir)) { | ||||
|             fs.mkdirSync(Database.dataDir, { recursive: true }); | ||||
|   | ||||
| @@ -1,24 +0,0 @@ | ||||
| const childProcess = require("child_process"); | ||||
|  | ||||
| class Git { | ||||
|  | ||||
|     static clone(repoURL, cwd, targetDir = ".") { | ||||
|         let result = childProcess.spawnSync("git", [ | ||||
|             "clone", | ||||
|             repoURL, | ||||
|             targetDir, | ||||
|         ], { | ||||
|             cwd: cwd, | ||||
|         }); | ||||
|  | ||||
|         if (result.status !== 0) { | ||||
|             throw new Error(result.stderr.toString("utf-8")); | ||||
|         } else { | ||||
|             return result.stdout.toString("utf-8") + result.stderr.toString("utf-8"); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| module.exports = { | ||||
|     Git, | ||||
| }; | ||||
| @@ -1,5 +1,6 @@ | ||||
| const { UptimeKumaServer } = require("./uptime-kuma-server"); | ||||
| const { clearOldData } = require("./jobs/clear-old-data"); | ||||
| const { incrementalVacuum } = require("./jobs/incremental-vacuum"); | ||||
| const Cron = require("croner"); | ||||
|  | ||||
| const jobs = [ | ||||
| @@ -9,6 +10,12 @@ const jobs = [ | ||||
|         jobFunc: clearOldData, | ||||
|         croner: null, | ||||
|     }, | ||||
|     { | ||||
|         name: "incremental-vacuum", | ||||
|         interval: "*/5 * * * *", | ||||
|         jobFunc: incrementalVacuum, | ||||
|         croner: null, | ||||
|     } | ||||
| ]; | ||||
|  | ||||
| /** | ||||
|   | ||||
| @@ -39,6 +39,8 @@ const clearOldData = async () => { | ||||
|                 "DELETE FROM heartbeat WHERE time < DATETIME('now', '-' || ? || ' days') ", | ||||
|                 [ parsedPeriod ] | ||||
|             ); | ||||
|  | ||||
|             await R.exec("PRAGMA optimize;"); | ||||
|         } catch (e) { | ||||
|             log.error("clearOldData", `Failed to clear old data: ${e.message}`); | ||||
|         } | ||||
|   | ||||
							
								
								
									
										21
									
								
								server/jobs/incremental-vacuum.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								server/jobs/incremental-vacuum.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,21 @@ | ||||
| const { R } = require("redbean-node"); | ||||
| const { log } = require("../../src/util"); | ||||
|  | ||||
| /** | ||||
|  * Run incremental_vacuum and checkpoint the WAL. | ||||
|  * @return {Promise<void>} A promise that resolves when the process is finished. | ||||
|  */ | ||||
|  | ||||
| const incrementalVacuum = async () => { | ||||
|     try { | ||||
|         log.debug("incrementalVacuum", "Running incremental_vacuum and wal_checkpoint(PASSIVE)..."); | ||||
|         await R.exec("PRAGMA incremental_vacuum(200)"); | ||||
|         await R.exec("PRAGMA wal_checkpoint(PASSIVE)"); | ||||
|     } catch (e) { | ||||
|         log.error("incrementalVacuum", `Failed: ${e.message}`); | ||||
|     } | ||||
| }; | ||||
|  | ||||
| module.exports = { | ||||
|     incrementalVacuum, | ||||
| }; | ||||
| @@ -20,6 +20,7 @@ const { CacheableDnsHttpAgent } = require("../cacheable-dns-http-agent"); | ||||
| const { DockerHost } = require("../docker"); | ||||
| const { UptimeCacheList } = require("../uptime-cache-list"); | ||||
| const Gamedig = require("gamedig"); | ||||
| const jsonata = require("jsonata"); | ||||
| const jwt = require("jsonwebtoken"); | ||||
|  | ||||
| /** | ||||
| @@ -104,6 +105,7 @@ class Monitor extends BeanModel { | ||||
|             retryInterval: this.retryInterval, | ||||
|             resendInterval: this.resendInterval, | ||||
|             keyword: this.keyword, | ||||
|             invertKeyword: this.isInvertKeyword(), | ||||
|             expiryNotification: this.isEnabledExpiryNotification(), | ||||
|             ignoreTls: this.getIgnoreTls(), | ||||
|             upsideDown: this.isUpsideDown(), | ||||
| @@ -132,6 +134,8 @@ class Monitor extends BeanModel { | ||||
|             radiusCallingStationId: this.radiusCallingStationId, | ||||
|             game: this.game, | ||||
|             httpBodyEncoding: this.httpBodyEncoding, | ||||
|             jsonPath: this.jsonPath, | ||||
|             expectedValue: this.expectedValue, | ||||
|             screenshot, | ||||
|         }; | ||||
|  | ||||
| @@ -239,6 +243,14 @@ class Monitor extends BeanModel { | ||||
|         return Boolean(this.upsideDown); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Parse to boolean | ||||
|      * @returns {boolean} | ||||
|      */ | ||||
|     isInvertKeyword() { | ||||
|         return Boolean(this.invertKeyword); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Parse to boolean | ||||
|      * @returns {boolean} | ||||
| @@ -343,7 +355,7 @@ class Monitor extends BeanModel { | ||||
|                         bean.msg = "Group empty"; | ||||
|                     } | ||||
|  | ||||
|                 } else if (this.type === "http" || this.type === "keyword") { | ||||
|                 } else if (this.type === "http" || this.type === "keyword" || this.type === "json-query") { | ||||
|                     // Do not do any queries/high loading things before the "bean.ping" | ||||
|                     let startTime = dayjs().valueOf(); | ||||
|  | ||||
| @@ -471,7 +483,7 @@ class Monitor extends BeanModel { | ||||
|  | ||||
|                     if (this.type === "http") { | ||||
|                         bean.status = UP; | ||||
|                     } else { | ||||
|                     } else if (this.type === "keyword") { | ||||
|  | ||||
|                         let data = res.data; | ||||
|  | ||||
| @@ -480,17 +492,37 @@ class Monitor extends BeanModel { | ||||
|                             data = JSON.stringify(data); | ||||
|                         } | ||||
|  | ||||
|                         if (data.includes(this.keyword)) { | ||||
|                             bean.msg += ", keyword is found"; | ||||
|                         let keywordFound = data.includes(this.keyword); | ||||
|                         if (keywordFound === !this.isInvertKeyword()) { | ||||
|                             bean.msg += ", keyword " + (keywordFound ? "is" : "not") + " found"; | ||||
|                             bean.status = UP; | ||||
|                         } else { | ||||
|                             data = data.replace(/<[^>]*>?|[\n\r]|\s+/gm, " ").trim(); | ||||
|                             if (data.length > 50) { | ||||
|                                 data = data.substring(0, 47) + "..."; | ||||
|                             } | ||||
|                             throw new Error(bean.msg + ", but keyword is not in [" + data + "]"); | ||||
|                             throw new Error(bean.msg + ", but keyword is " + | ||||
|                                 (keywordFound ? "present" : "not") + " in [" + data + "]"); | ||||
|                         } | ||||
|  | ||||
|                     } else if (this.type === "json-query") { | ||||
|                         let data = res.data; | ||||
|  | ||||
|                         // convert data to object | ||||
|                         if (typeof data === "string") { | ||||
|                             data = JSON.parse(data); | ||||
|                         } | ||||
|  | ||||
|                         let expression = jsonata(this.jsonPath); | ||||
|  | ||||
|                         let result = await expression.evaluate(data); | ||||
|  | ||||
|                         if (result.toString() === this.expectedValue) { | ||||
|                             bean.msg += ", expected value is found"; | ||||
|                             bean.status = UP; | ||||
|                         } else { | ||||
|                             throw new Error(bean.msg + ", but value is not equal to expected value, value was: [" + result + "]"); | ||||
|                         } | ||||
|                     } | ||||
|  | ||||
|                 } else if (this.type === "port") { | ||||
| @@ -565,7 +597,7 @@ class Monitor extends BeanModel { | ||||
|                             // No need to insert successful heartbeat for push type, so end here | ||||
|                             retries = 0; | ||||
|                             log.debug("monitor", `[${this.name}] timeout = ${timeout}`); | ||||
|                             this.heartbeatInterval = setTimeout(beat, timeout); | ||||
|                             this.heartbeatInterval = setTimeout(safeBeat, timeout); | ||||
|                             return; | ||||
|                         } | ||||
|                     } else { | ||||
| @@ -695,7 +727,6 @@ class Monitor extends BeanModel { | ||||
|                         grpcEnableTls: this.grpcEnableTls, | ||||
|                         grpcMethod: this.grpcMethod, | ||||
|                         grpcBody: this.grpcBody, | ||||
|                         keyword: this.keyword | ||||
|                     }; | ||||
|                     const response = await grpcQuery(options); | ||||
|                     bean.ping = dayjs().valueOf() - startTime; | ||||
| @@ -708,13 +739,14 @@ class Monitor extends BeanModel { | ||||
|                         bean.status = DOWN; | ||||
|                         bean.msg = `Error in send gRPC ${response.code} ${response.errorMessage}`; | ||||
|                     } else { | ||||
|                         if (response.data.toString().includes(this.keyword)) { | ||||
|                         let keywordFound = response.data.toString().includes(this.keyword); | ||||
|                         if (keywordFound === !this.isInvertKeyword()) { | ||||
|                             bean.status = UP; | ||||
|                             bean.msg = `${responseData}, keyword [${this.keyword}] is found`; | ||||
|                             bean.msg = `${responseData}, keyword [${this.keyword}] ${keywordFound ? "is" : "not"} found`; | ||||
|                         } else { | ||||
|                             log.debug("monitor:", `GRPC response [${response.data}] + ", but keyword [${this.keyword}] is not in [" + ${response.data} + "]"`); | ||||
|                             log.debug("monitor:", `GRPC response [${response.data}] + ", but keyword [${this.keyword}] is ${keywordFound ? "present" : "not"} in [" + ${response.data} + "]"`); | ||||
|                             bean.status = DOWN; | ||||
|                             bean.msg = `, but keyword [${this.keyword}] is not in [" + ${responseData} + "]`; | ||||
|                             bean.msg = `, but keyword [${this.keyword}] is ${keywordFound ? "present" : "not"} in [" + ${responseData} + "]`; | ||||
|                         } | ||||
|                     } | ||||
|                 } else if (this.type === "postgres") { | ||||
| @@ -761,7 +793,8 @@ class Monitor extends BeanModel { | ||||
|                             this.radiusCalledStationId, | ||||
|                             this.radiusCallingStationId, | ||||
|                             this.radiusSecret, | ||||
|                             port | ||||
|                             port, | ||||
|                             this.interval * 1000 * 0.8, | ||||
|                         ); | ||||
|                         if (resp.code) { | ||||
|                             bean.msg = resp.code; | ||||
|   | ||||
| @@ -7,9 +7,60 @@ const childProcess = require("child_process"); | ||||
| const path = require("path"); | ||||
| const Database = require("../database"); | ||||
| const jwt = require("jsonwebtoken"); | ||||
| const config = require("../config"); | ||||
|  | ||||
| let browser = null; | ||||
|  | ||||
| let allowedList = []; | ||||
| let lastAutoDetectChromeExecutable = null; | ||||
|  | ||||
| if (process.platform === "win32") { | ||||
|     allowedList.push(process.env.LOCALAPPDATA + "\\Google\\Chrome\\Application\\chrome.exe"); | ||||
|     allowedList.push(process.env.PROGRAMFILES + "\\Google\\Chrome\\Application\\chrome.exe"); | ||||
|     allowedList.push(process.env["ProgramFiles(x86)"] + "\\Google\\Chrome\\Application\\chrome.exe"); | ||||
|  | ||||
|     // Allow Chromium too | ||||
|     allowedList.push(process.env.LOCALAPPDATA + "\\Chromium\\Application\\chrome.exe"); | ||||
|     allowedList.push(process.env.PROGRAMFILES + "\\Chromium\\Application\\chrome.exe"); | ||||
|     allowedList.push(process.env["ProgramFiles(x86)"] + "\\Chromium\\Application\\chrome.exe"); | ||||
|  | ||||
|     // For Loop A to Z | ||||
|     for (let i = 65; i <= 90; i++) { | ||||
|         let drive = String.fromCharCode(i); | ||||
|         allowedList.push(drive + ":\\Program Files\\Google\\Chrome\\Application\\chrome.exe"); | ||||
|         allowedList.push(drive + ":\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe"); | ||||
|     } | ||||
|  | ||||
| } else if (process.platform === "linux") { | ||||
|     allowedList = [ | ||||
|         "chromium", | ||||
|         "chromium-browser", | ||||
|         "google-chrome", | ||||
|  | ||||
|         "/usr/bin/chromium", | ||||
|         "/usr/bin/chromium-browser", | ||||
|         "/usr/bin/google-chrome", | ||||
|     ]; | ||||
| } else if (process.platform === "darwin") { | ||||
|     // TODO: Generated by GitHub Copilot, but not sure if it's correct | ||||
|     allowedList = [ | ||||
|         "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome", | ||||
|         "/Applications/Chromium.app/Contents/MacOS/Chromium", | ||||
|     ]; | ||||
| } | ||||
|  | ||||
| log.debug("chrome", allowedList); | ||||
|  | ||||
| async function isAllowedChromeExecutable(executablePath) { | ||||
|     console.log(config.args); | ||||
|     if (config.args["allow-all-chrome-exec"] || process.env.UPTIME_KUMA_ALLOW_ALL_CHROME_EXEC === "1") { | ||||
|         return true; | ||||
|     } | ||||
|  | ||||
|     // Check if the executablePath is in the list of allowed executables | ||||
|     return allowedList.includes(executablePath); | ||||
| } | ||||
|  | ||||
| async function getBrowser() { | ||||
|     if (!browser) { | ||||
|         let executablePath = await Settings.get("chromeExecutable"); | ||||
| @@ -27,6 +78,7 @@ async function getBrowser() { | ||||
| async function prepareChromeExecutable(executablePath) { | ||||
|     // Special code for using the playwright_chromium | ||||
|     if (typeof executablePath === "string" && executablePath.toLocaleLowerCase() === "#playwright_chromium") { | ||||
|         // Set to undefined = use playwright_chromium | ||||
|         executablePath = undefined; | ||||
|     } else if (!executablePath) { | ||||
|         if (process.env.UPTIME_KUMA_IS_CONTAINER) { | ||||
| @@ -56,30 +108,30 @@ async function prepareChromeExecutable(executablePath) { | ||||
|                 }); | ||||
|             } | ||||
|  | ||||
|         } else if (process.platform === "win32") { | ||||
|             executablePath = findChrome([ | ||||
|                 "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe", | ||||
|                 "C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe", | ||||
|                 "D:\\Program Files\\Google\\Chrome\\Application\\chrome.exe", | ||||
|                 "D:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe", | ||||
|                 "E:\\Program Files\\Google\\Chrome\\Application\\chrome.exe", | ||||
|                 "E:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe", | ||||
|             ]); | ||||
|         } else if (process.platform === "linux") { | ||||
|             executablePath = findChrome([ | ||||
|                 "chromium-browser", | ||||
|                 "chromium", | ||||
|                 "google-chrome", | ||||
|             ]); | ||||
|         } else { | ||||
|             executablePath = findChrome(allowedList); | ||||
|         } | ||||
|     } else { | ||||
|         // User specified a path | ||||
|         // Check if the executablePath is in the list of allowed | ||||
|         if (!await isAllowedChromeExecutable(executablePath)) { | ||||
|             throw new Error("This Chromium executable path is not allowed by default. If you are sure this is safe, please add an environment variable UPTIME_KUMA_ALLOW_ALL_CHROME_EXEC=1 to allow it."); | ||||
|         } | ||||
|         // TODO: Mac?? | ||||
|     } | ||||
|     return executablePath; | ||||
| } | ||||
|  | ||||
| function findChrome(executables) { | ||||
|     // Use the last working executable, so we don't have to search for it again | ||||
|     if (lastAutoDetectChromeExecutable) { | ||||
|         if (commandExistsSync(lastAutoDetectChromeExecutable)) { | ||||
|             return lastAutoDetectChromeExecutable; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     for (let executable of executables) { | ||||
|         if (commandExistsSync(executable)) { | ||||
|             lastAutoDetectChromeExecutable = executable; | ||||
|             return executable; | ||||
|         } | ||||
|     } | ||||
|   | ||||
| @@ -67,7 +67,7 @@ class SMTP extends NotificationProvider { | ||||
|                 if (monitorJSON !== null) { | ||||
|                     monitorName = monitorJSON["name"]; | ||||
|  | ||||
|                     if (monitorJSON["type"] === "http" || monitorJSON["type"] === "keyword") { | ||||
|                     if (monitorJSON["type"] === "http" || monitorJSON["type"] === "keyword" || monitorJSON["type"] === "json-query") { | ||||
|                         monitorHostnameOrURL = monitorJSON["url"]; | ||||
|                     } else { | ||||
|                         monitorHostnameOrURL = monitorJSON["hostname"]; | ||||
|   | ||||
| @@ -10,6 +10,7 @@ class Twilio extends NotificationProvider { | ||||
|         let okMsg = "Sent Successfully."; | ||||
|  | ||||
|         let accountSID = notification.twilioAccountSID; | ||||
|         let apiKey = notification.twilioApiKey ? notification.twilioApiKey : accountSID; | ||||
|         let authToken = notification.twilioAuthToken; | ||||
|  | ||||
|         try { | ||||
| @@ -17,7 +18,7 @@ class Twilio extends NotificationProvider { | ||||
|             let config = { | ||||
|                 headers: { | ||||
|                     "Content-Type": "application/x-www-form-urlencoded;charset=utf-8", | ||||
|                     "Authorization": "Basic " + Buffer.from(accountSID + ":" + authToken).toString("base64"), | ||||
|                     "Authorization": "Basic " + Buffer.from(apiKey + ":" + authToken).toString("base64"), | ||||
|                 } | ||||
|             }; | ||||
|  | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| const NotificationProvider = require("./notification-provider"); | ||||
| const axios = require("axios"); | ||||
| const FormData = require("form-data"); | ||||
| const { Liquid } = require("liquidjs"); | ||||
|  | ||||
| class Webhook extends NotificationProvider { | ||||
|  | ||||
| @@ -15,17 +16,27 @@ class Webhook extends NotificationProvider { | ||||
|                 monitor: monitorJSON, | ||||
|                 msg, | ||||
|             }; | ||||
|             let finalData; | ||||
|             let config = { | ||||
|                 headers: {} | ||||
|             }; | ||||
|  | ||||
|             if (notification.webhookContentType === "form-data") { | ||||
|                 finalData = new FormData(); | ||||
|                 finalData.append("data", JSON.stringify(data)); | ||||
|                 config.headers = finalData.getHeaders(); | ||||
|             } else { | ||||
|                 finalData = data; | ||||
|                 const formData = new FormData(); | ||||
|                 formData.append("data", JSON.stringify(data)); | ||||
|                 config.headers = formData.getHeaders(); | ||||
|                 data = formData; | ||||
|             } else if (notification.webhookContentType === "custom") { | ||||
|                 // Initialize LiquidJS and parse the custom Body Template | ||||
|                 const engine = new Liquid(); | ||||
|                 const tpl = engine.parse(notification.webhookCustomBody); | ||||
|  | ||||
|                 // Insert templated values into Body | ||||
|                 data = await engine.render(tpl, | ||||
|                     { | ||||
|                         msg, | ||||
|                         heartbeatJSON, | ||||
|                         monitorJSON | ||||
|                     }); | ||||
|             } | ||||
|  | ||||
|             if (notification.webhookAdditionalHeaders) { | ||||
| @@ -39,7 +50,7 @@ class Webhook extends NotificationProvider { | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             await axios.post(notification.webhookURL, finalData, config); | ||||
|             await axios.post(notification.webhookURL, data, config); | ||||
|             return okMsg; | ||||
|  | ||||
|         } catch (error) { | ||||
|   | ||||
| @@ -1,13 +0,0 @@ | ||||
| class Plugin { | ||||
|     async load() { | ||||
|  | ||||
|     } | ||||
|  | ||||
|     async unload() { | ||||
|  | ||||
|     } | ||||
| } | ||||
|  | ||||
| module.exports = { | ||||
|     Plugin, | ||||
| }; | ||||
| @@ -1,256 +0,0 @@ | ||||
| const fs = require("fs"); | ||||
| const { log } = require("../src/util"); | ||||
| const path = require("path"); | ||||
| const axios = require("axios"); | ||||
| const { Git } = require("./git"); | ||||
| const childProcess = require("child_process"); | ||||
|  | ||||
| class PluginsManager { | ||||
|  | ||||
|     static disable = false; | ||||
|  | ||||
|     /** | ||||
|      * Plugin List | ||||
|      * @type {PluginWrapper[]} | ||||
|      */ | ||||
|     pluginList = []; | ||||
|  | ||||
|     /** | ||||
|      * Plugins Dir | ||||
|      */ | ||||
|     pluginsDir; | ||||
|  | ||||
|     server; | ||||
|  | ||||
|     /** | ||||
|      * | ||||
|      * @param {UptimeKumaServer} server | ||||
|      */ | ||||
|     constructor(server) { | ||||
|         this.server = server; | ||||
|  | ||||
|         if (!PluginsManager.disable) { | ||||
|             this.pluginsDir = "./data/plugins/"; | ||||
|  | ||||
|             if (! fs.existsSync(this.pluginsDir)) { | ||||
|                 fs.mkdirSync(this.pluginsDir, { recursive: true }); | ||||
|             } | ||||
|  | ||||
|             log.debug("plugin", "Scanning plugin directory"); | ||||
|             let list = fs.readdirSync(this.pluginsDir); | ||||
|  | ||||
|             this.pluginList = []; | ||||
|             for (let item of list) { | ||||
|                 this.loadPlugin(item); | ||||
|             } | ||||
|  | ||||
|         } else { | ||||
|             log.warn("PLUGIN", "Skip scanning plugin directory"); | ||||
|         } | ||||
|  | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Install a Plugin | ||||
|      */ | ||||
|     async loadPlugin(name) { | ||||
|         log.info("plugin", "Load " + name); | ||||
|         let plugin = new PluginWrapper(this.server, this.pluginsDir + name); | ||||
|  | ||||
|         try { | ||||
|             await plugin.load(); | ||||
|             this.pluginList.push(plugin); | ||||
|         } catch (e) { | ||||
|             log.error("plugin", "Failed to load plugin: " + this.pluginsDir + name); | ||||
|             log.error("plugin", "Reason: " + e.message); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Download a Plugin | ||||
|      * @param {string} repoURL Git repo url | ||||
|      * @param {string} name Directory name, also known as plugin unique name | ||||
|      */ | ||||
|     downloadPlugin(repoURL, name) { | ||||
|         if (fs.existsSync(this.pluginsDir + name)) { | ||||
|             log.info("plugin", "Plugin folder already exists? Removing..."); | ||||
|             fs.rmSync(this.pluginsDir + name, { | ||||
|                 recursive: true | ||||
|             }); | ||||
|         } | ||||
|         log.info("plugin", "Installing plugin: " + name + " " + repoURL); | ||||
|         let result = Git.clone(repoURL, this.pluginsDir, name); | ||||
|         log.info("plugin", "Install result: " + result); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Remove a plugin | ||||
|      * @param {string} name | ||||
|      */ | ||||
|     async removePlugin(name) { | ||||
|         log.info("plugin", "Removing plugin: " + name); | ||||
|         for (let plugin of this.pluginList) { | ||||
|             if (plugin.info.name === name) { | ||||
|                 await plugin.unload(); | ||||
|  | ||||
|                 // Delete the plugin directory | ||||
|                 fs.rmSync(this.pluginsDir + name, { | ||||
|                     recursive: true | ||||
|                 }); | ||||
|  | ||||
|                 this.pluginList.splice(this.pluginList.indexOf(plugin), 1); | ||||
|                 return; | ||||
|             } | ||||
|         } | ||||
|         log.warn("plugin", "Plugin not found: " + name); | ||||
|         throw new Error("Plugin not found: " + name); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * TODO: Update a plugin | ||||
|      * Only available for plugins which were downloaded from the official list | ||||
|      * @param pluginID | ||||
|      */ | ||||
|     updatePlugin(pluginID) { | ||||
|  | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Get the plugin list from server + local installed plugin list | ||||
|      * Item will be merged if the `name` is the same. | ||||
|      * @returns {Promise<[]>} | ||||
|      */ | ||||
|     async fetchPluginList() { | ||||
|         let remotePluginList; | ||||
|         try { | ||||
|             const res = await axios.get("https://uptime.kuma.pet/c/plugins.json"); | ||||
|             remotePluginList = res.data.pluginList; | ||||
|         } catch (e) { | ||||
|             log.error("plugin", "Failed to fetch plugin list: " + e.message); | ||||
|             remotePluginList = []; | ||||
|         } | ||||
|  | ||||
|         for (let plugin of this.pluginList) { | ||||
|             let find = false; | ||||
|             // Try to merge | ||||
|             for (let remotePlugin of remotePluginList) { | ||||
|                 if (remotePlugin.name === plugin.info.name) { | ||||
|                     find = true; | ||||
|                     remotePlugin.installed = true; | ||||
|                     remotePlugin.name = plugin.info.name; | ||||
|                     remotePlugin.fullName = plugin.info.fullName; | ||||
|                     remotePlugin.description = plugin.info.description; | ||||
|                     remotePlugin.version = plugin.info.version; | ||||
|                     break; | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             // Local plugin | ||||
|             if (!find) { | ||||
|                 plugin.info.local = true; | ||||
|                 remotePluginList.push(plugin.info); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         // Sort Installed first, then sort by name | ||||
|         return remotePluginList.sort((a, b) => { | ||||
|             if (a.installed === b.installed) { | ||||
|                 if (a.fullName < b.fullName) { | ||||
|                     return -1; | ||||
|                 } | ||||
|                 if (a.fullName > b.fullName) { | ||||
|                     return 1; | ||||
|                 } | ||||
|                 return 0; | ||||
|             } else if (a.installed) { | ||||
|                 return -1; | ||||
|             } else { | ||||
|                 return 1; | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
| } | ||||
|  | ||||
| class PluginWrapper { | ||||
|  | ||||
|     server = undefined; | ||||
|     pluginDir = undefined; | ||||
|  | ||||
|     /** | ||||
|      * Must be an `new-able` class. | ||||
|      * @type {function} | ||||
|      */ | ||||
|     pluginClass = undefined; | ||||
|  | ||||
|     /** | ||||
|      * | ||||
|      * @type {Plugin} | ||||
|      */ | ||||
|     object = undefined; | ||||
|     info = {}; | ||||
|  | ||||
|     /** | ||||
|      * | ||||
|      * @param {UptimeKumaServer} server | ||||
|      * @param {string} pluginDir | ||||
|      */ | ||||
|     constructor(server, pluginDir) { | ||||
|         this.server = server; | ||||
|         this.pluginDir = pluginDir; | ||||
|     } | ||||
|  | ||||
|     async load() { | ||||
|         let indexFile = this.pluginDir + "/index.js"; | ||||
|         let packageJSON = this.pluginDir + "/package.json"; | ||||
|  | ||||
|         log.info("plugin", "Installing dependencies"); | ||||
|  | ||||
|         if (fs.existsSync(indexFile)) { | ||||
|             // Install dependencies | ||||
|             let result = childProcess.spawnSync("npm", [ "install" ], { | ||||
|                 cwd: this.pluginDir, | ||||
|                 env: { | ||||
|                     ...process.env, | ||||
|                     PLAYWRIGHT_BROWSERS_PATH: "../../browsers",    // Special handling for read-browser-monitor | ||||
|                 } | ||||
|             }); | ||||
|  | ||||
|             if (result.stdout) { | ||||
|                 log.info("plugin", "Install dependencies result: " + result.stdout.toString("utf-8")); | ||||
|             } else { | ||||
|                 log.warn("plugin", "Install dependencies result: no output"); | ||||
|             } | ||||
|  | ||||
|             this.pluginClass = require(path.join(process.cwd(), indexFile)); | ||||
|  | ||||
|             let pluginClassType = typeof this.pluginClass; | ||||
|  | ||||
|             if (pluginClassType === "function") { | ||||
|                 this.object = new this.pluginClass(this.server); | ||||
|                 await this.object.load(); | ||||
|             } else { | ||||
|                 throw new Error("Invalid plugin, it does not export a class"); | ||||
|             } | ||||
|  | ||||
|             if (fs.existsSync(packageJSON)) { | ||||
|                 this.info = require(path.join(process.cwd(), packageJSON)); | ||||
|             } else { | ||||
|                 this.info.fullName = this.pluginDir; | ||||
|                 this.info.name = "[unknown]"; | ||||
|                 this.info.version = "[unknown-version]"; | ||||
|             } | ||||
|  | ||||
|             this.info.installed = true; | ||||
|             log.info("plugin", `${this.info.fullName} v${this.info.version} loaded`); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     async unload() { | ||||
|         await this.object.unload(); | ||||
|     } | ||||
| } | ||||
|  | ||||
| module.exports = { | ||||
|     PluginsManager, | ||||
|     PluginWrapper | ||||
| }; | ||||
| @@ -5,6 +5,8 @@ const StatusPage = require("../model/status_page"); | ||||
| const { allowDevAllOrigin, sendHttpError } = require("../util-server"); | ||||
| const { R } = require("redbean-node"); | ||||
| const Monitor = require("../model/monitor"); | ||||
| const { badgeConstants } = require("../config"); | ||||
| const { makeBadge } = require("badge-maker"); | ||||
|  | ||||
| let router = express.Router(); | ||||
|  | ||||
| @@ -139,4 +141,100 @@ router.get("/api/status-page/:slug/manifest.json", cache("1440 minutes"), async | ||||
|     } | ||||
| }); | ||||
|  | ||||
| // overall status-page status badge | ||||
| router.get("/api/status-page/:slug/badge", cache("5 minutes"), async (request, response) => { | ||||
|     allowDevAllOrigin(response); | ||||
|     const slug = request.params.slug; | ||||
|     const statusPageID = await StatusPage.slugToID(slug); | ||||
|     const { | ||||
|         label, | ||||
|         upColor = badgeConstants.defaultUpColor, | ||||
|         downColor = badgeConstants.defaultDownColor, | ||||
|         partialColor = "#F6BE00", | ||||
|         maintenanceColor = "#808080", | ||||
|         style = badgeConstants.defaultStyle | ||||
|     } = request.query; | ||||
|  | ||||
|     try { | ||||
|         let monitorIDList = await R.getCol(` | ||||
|             SELECT monitor_group.monitor_id FROM monitor_group, \`group\` | ||||
|             WHERE monitor_group.group_id = \`group\`.id | ||||
|             AND public = 1 | ||||
|             AND \`group\`.status_page_id = ? | ||||
|         `, [ | ||||
|             statusPageID | ||||
|         ]); | ||||
|  | ||||
|         let hasUp = false; | ||||
|         let hasDown = false; | ||||
|         let hasMaintenance = false; | ||||
|  | ||||
|         for (let monitorID of monitorIDList) { | ||||
|             // retrieve the latest heartbeat | ||||
|             let beat = await R.getAll(` | ||||
|                     SELECT * FROM heartbeat | ||||
|                     WHERE monitor_id = ? | ||||
|                     ORDER BY time DESC | ||||
|                     LIMIT 1 | ||||
|             `, [ | ||||
|                 monitorID, | ||||
|             ]); | ||||
|  | ||||
|             // to be sure, when corresponding monitor not found | ||||
|             if (beat.length === 0) { | ||||
|                 continue; | ||||
|             } | ||||
|             // handle status of beat | ||||
|             if (beat[0].status === 3) { | ||||
|                 hasMaintenance = true; | ||||
|             } else if (beat[0].status === 2) { | ||||
|                 // ignored | ||||
|             } else if (beat[0].status === 1) { | ||||
|                 hasUp = true; | ||||
|             } else { | ||||
|                 hasDown = true; | ||||
|             } | ||||
|  | ||||
|         } | ||||
|  | ||||
|         const badgeValues = { style }; | ||||
|  | ||||
|         if (!hasUp && !hasDown && !hasMaintenance) { | ||||
|             // return a "N/A" badge in naColor (grey), if monitor is not public / not available / non exsitant | ||||
|  | ||||
|             badgeValues.message = "N/A"; | ||||
|             badgeValues.color = badgeConstants.naColor; | ||||
|  | ||||
|         } else { | ||||
|             if (hasMaintenance) { | ||||
|                 badgeValues.label = label ? label : ""; | ||||
|                 badgeValues.color = maintenanceColor; | ||||
|                 badgeValues.message = "Maintenance"; | ||||
|             } else if (hasUp && !hasDown) { | ||||
|                 badgeValues.label = label ? label : ""; | ||||
|                 badgeValues.color = upColor; | ||||
|                 badgeValues.message = "Up"; | ||||
|             } else if (hasUp && hasDown) { | ||||
|                 badgeValues.label = label ? label : ""; | ||||
|                 badgeValues.color = partialColor; | ||||
|                 badgeValues.message = "Degraded"; | ||||
|             } else { | ||||
|                 badgeValues.label = label ? label : ""; | ||||
|                 badgeValues.color = downColor; | ||||
|                 badgeValues.message = "Down"; | ||||
|             } | ||||
|  | ||||
|         } | ||||
|  | ||||
|         // build the svg based on given values | ||||
|         const svg = makeBadge(badgeValues); | ||||
|  | ||||
|         response.type("image/svg+xml"); | ||||
|         response.send(svg); | ||||
|  | ||||
|     } catch (error) { | ||||
|         sendHttpError(response, error.message); | ||||
|     } | ||||
| }); | ||||
|  | ||||
| module.exports = router; | ||||
|   | ||||
| @@ -147,7 +147,6 @@ const { apiKeySocketHandler } = require("./socket-handlers/api-key-socket-handle | ||||
| const { generalSocketHandler } = require("./socket-handlers/general-socket-handler"); | ||||
| const { Settings } = require("./settings"); | ||||
| const { CacheableDnsHttpAgent } = require("./cacheable-dns-http-agent"); | ||||
| const { pluginsHandler } = require("./socket-handlers/plugins-handler"); | ||||
| const apicache = require("./modules/apicache"); | ||||
| const { resetChrome } = require("./monitor-types/real-browser-monitor-type"); | ||||
|  | ||||
| @@ -172,7 +171,6 @@ let needSetup = false; | ||||
|     Database.init(args); | ||||
|     await initDatabase(testMode); | ||||
|     await server.initAfterDatabaseReady(); | ||||
|     server.loadPlugins(); | ||||
|     server.entryPage = await Settings.get("entryPage"); | ||||
|     await StatusPage.loadDomainMappingList(); | ||||
|  | ||||
| @@ -210,6 +208,7 @@ let needSetup = false; | ||||
|     }); | ||||
|  | ||||
|     if (isDev) { | ||||
|         app.use(express.urlencoded({ extended: true })); | ||||
|         app.post("/test-webhook", async (request, response) => { | ||||
|             log.debug("test", request.headers); | ||||
|             log.debug("test", request.body); | ||||
| @@ -714,6 +713,7 @@ let needSetup = false; | ||||
|                 bean.maxretries = monitor.maxretries; | ||||
|                 bean.port = parseInt(monitor.port); | ||||
|                 bean.keyword = monitor.keyword; | ||||
|                 bean.invertKeyword = monitor.invertKeyword; | ||||
|                 bean.ignoreTls = monitor.ignoreTls; | ||||
|                 bean.expiryNotification = monitor.expiryNotification; | ||||
|                 bean.upsideDown = monitor.upsideDown; | ||||
| @@ -748,6 +748,8 @@ let needSetup = false; | ||||
|                 bean.radiusCallingStationId = monitor.radiusCallingStationId; | ||||
|                 bean.radiusSecret = monitor.radiusSecret; | ||||
|                 bean.httpBodyEncoding = monitor.httpBodyEncoding; | ||||
|                 bean.expectedValue = monitor.expectedValue; | ||||
|                 bean.jsonPath = monitor.jsonPath; | ||||
|  | ||||
|                 bean.validate(); | ||||
|  | ||||
| @@ -1378,6 +1380,7 @@ let needSetup = false; | ||||
|                                 maxretries: monitorListData[i].maxretries, | ||||
|                                 port: monitorListData[i].port, | ||||
|                                 keyword: monitorListData[i].keyword, | ||||
|                                 invertKeyword: monitorListData[i].invertKeyword, | ||||
|                                 ignoreTls: monitorListData[i].ignoreTls, | ||||
|                                 upsideDown: monitorListData[i].upsideDown, | ||||
|                                 maxredirects: monitorListData[i].maxredirects, | ||||
| @@ -1546,7 +1549,6 @@ let needSetup = false; | ||||
|         maintenanceSocketHandler(socket); | ||||
|         apiKeySocketHandler(socket); | ||||
|         generalSocketHandler(socket, server); | ||||
|         pluginsHandler(socket, server); | ||||
|  | ||||
|         log.debug("server", "added all socket handlers"); | ||||
|  | ||||
|   | ||||
| @@ -1,69 +0,0 @@ | ||||
| const { checkLogin } = require("../util-server"); | ||||
| const { PluginsManager } = require("../plugins-manager"); | ||||
| const { log } = require("../../src/util.js"); | ||||
|  | ||||
| /** | ||||
|  * Handlers for plugins | ||||
|  * @param {Socket} socket Socket.io instance | ||||
|  * @param {UptimeKumaServer} server | ||||
|  */ | ||||
| module.exports.pluginsHandler = (socket, server) => { | ||||
|  | ||||
|     const pluginManager = server.getPluginManager(); | ||||
|  | ||||
|     // Get Plugin List | ||||
|     socket.on("getPluginList", async (callback) => { | ||||
|         try { | ||||
|             checkLogin(socket); | ||||
|  | ||||
|             log.debug("plugin", "PluginManager.disable: " + PluginsManager.disable); | ||||
|  | ||||
|             if (PluginsManager.disable) { | ||||
|                 throw new Error("Plugin Disabled: In order to enable plugin feature, you need to use the default data directory: ./data/"); | ||||
|             } | ||||
|  | ||||
|             let pluginList = await pluginManager.fetchPluginList(); | ||||
|             callback({ | ||||
|                 ok: true, | ||||
|                 pluginList, | ||||
|             }); | ||||
|         } catch (error) { | ||||
|             log.warn("plugin", "Error: " + error.message); | ||||
|             callback({ | ||||
|                 ok: false, | ||||
|                 msg: error.message, | ||||
|             }); | ||||
|         } | ||||
|     }); | ||||
|  | ||||
|     socket.on("installPlugin", async (repoURL, name, callback) => { | ||||
|         try { | ||||
|             checkLogin(socket); | ||||
|             pluginManager.downloadPlugin(repoURL, name); | ||||
|             await pluginManager.loadPlugin(name); | ||||
|             callback({ | ||||
|                 ok: true, | ||||
|             }); | ||||
|         } catch (error) { | ||||
|             callback({ | ||||
|                 ok: false, | ||||
|                 msg: error.message, | ||||
|             }); | ||||
|         } | ||||
|     }); | ||||
|  | ||||
|     socket.on("uninstallPlugin", async (name, callback) => { | ||||
|         try { | ||||
|             checkLogin(socket); | ||||
|             await pluginManager.removePlugin(name); | ||||
|             callback({ | ||||
|                 ok: true, | ||||
|             }); | ||||
|         } catch (error) { | ||||
|             callback({ | ||||
|                 ok: false, | ||||
|                 msg: error.message, | ||||
|             }); | ||||
|         } | ||||
|     }); | ||||
| }; | ||||
| @@ -10,7 +10,6 @@ const util = require("util"); | ||||
| const { CacheableDnsHttpAgent } = require("./cacheable-dns-http-agent"); | ||||
| const { Settings } = require("./settings"); | ||||
| const dayjs = require("dayjs"); | ||||
| const { PluginsManager } = require("./plugins-manager"); | ||||
| // DO NOT IMPORT HERE IF THE MODULES USED `UptimeKumaServer.getInstance()` | ||||
|  | ||||
| /** | ||||
| @@ -47,12 +46,6 @@ class UptimeKumaServer { | ||||
|      */ | ||||
|     indexHTML = ""; | ||||
|  | ||||
|     /** | ||||
|      * Plugins Manager | ||||
|      * @type {PluginsManager} | ||||
|      */ | ||||
|     pluginsManager = null; | ||||
|  | ||||
|     /** | ||||
|      * | ||||
|      * @type {{}} | ||||
| @@ -301,46 +294,6 @@ class UptimeKumaServer { | ||||
|     async stop() { | ||||
|  | ||||
|     } | ||||
|  | ||||
|     loadPlugins() { | ||||
|         this.pluginsManager = new PluginsManager(this); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * | ||||
|      * @returns {PluginsManager} | ||||
|      */ | ||||
|     getPluginManager() { | ||||
|         return this.pluginsManager; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * | ||||
|      * @param {MonitorType} monitorType | ||||
|      */ | ||||
|     addMonitorType(monitorType) { | ||||
|         if (monitorType instanceof MonitorType && monitorType.name) { | ||||
|             if (monitorType.name in UptimeKumaServer.monitorTypeList) { | ||||
|                 log.error("", "Conflict Monitor Type name"); | ||||
|             } | ||||
|             UptimeKumaServer.monitorTypeList[monitorType.name] = monitorType; | ||||
|         } else { | ||||
|             log.error("", "Invalid Monitor Type: " + monitorType.name); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * | ||||
|      * @param {MonitorType} monitorType | ||||
|      */ | ||||
|     removeMonitorType(monitorType) { | ||||
|         if (UptimeKumaServer.monitorTypeList[monitorType.name] === monitorType) { | ||||
|             delete UptimeKumaServer.monitorTypeList[monitorType.name]; | ||||
|         } else { | ||||
|             log.error("", "Remove MonitorType failed: " + monitorType.name); | ||||
|         } | ||||
|     } | ||||
|  | ||||
| } | ||||
|  | ||||
| module.exports = { | ||||
|   | ||||
| @@ -378,6 +378,7 @@ exports.mongodbPing = async function (connectionString) { | ||||
|  * @param {string} callingStationId ID of calling station | ||||
|  * @param {string} secret Secret to use | ||||
|  * @param {number} [port=1812] Port to contact radius server on | ||||
|  * @param {number} [timeout=2500] Timeout for connection to use | ||||
|  * @returns {Promise<any>} | ||||
|  */ | ||||
| exports.radius = function ( | ||||
| @@ -388,10 +389,12 @@ exports.radius = function ( | ||||
|     callingStationId, | ||||
|     secret, | ||||
|     port = 1812, | ||||
|     timeout = 2500, | ||||
| ) { | ||||
|     const client = new radiusClient({ | ||||
|         host: hostname, | ||||
|         hostPort: port, | ||||
|         timeout: timeout, | ||||
|         dictionaries: [ file ], | ||||
|     }); | ||||
|  | ||||
|   | ||||
| @@ -69,6 +69,7 @@ | ||||
|     .multiselect__content-wrapper { | ||||
|         background-color: $dark-bg2; | ||||
|         border-color: $dark-border-color; | ||||
|         z-index: 150; | ||||
|     } | ||||
|  | ||||
|     .multiselect--above .multiselect__content-wrapper { | ||||
|   | ||||
| @@ -104,7 +104,7 @@ export default { | ||||
|             // We must check if there are any elements in monitorList to | ||||
|             // prevent undefined errors if it hasn't been loaded yet | ||||
|             if (this.$parent.editMode && ignoreSendUrl && Object.keys(this.$root.monitorList).length) { | ||||
|                 return this.$root.monitorList[monitor.element.id].type === "http" || this.$root.monitorList[monitor.element.id].type === "keyword"; | ||||
|                 return this.$root.monitorList[monitor.element.id].type === "http" || this.$root.monitorList[monitor.element.id].type === "keyword" || this.$root.monitorList[monitor.element.id].type === "json-query"; | ||||
|             } | ||||
|             return monitor.element.sendUrl && monitor.element.url && monitor.element.url !== "https://" && !this.editMode; | ||||
|         }, | ||||
|   | ||||
| @@ -1,102 +0,0 @@ | ||||
| <template> | ||||
|     <div v-if="! (!plugin.installed && plugin.local)" class="plugin-item pt-4 pb-2"> | ||||
|         <div class="info"> | ||||
|             <h5>{{ plugin.fullName }}</h5> | ||||
|             <p class="description"> | ||||
|                 {{ plugin.description }} | ||||
|             </p> | ||||
|             <span class="version">{{ $t("Version") }}: {{ plugin.version }} <a v-if="plugin.repo" :href="plugin.repo" target="_blank">Repo</a></span> | ||||
|         </div> | ||||
|         <div class="buttons"> | ||||
|             <button v-if="status === 'installing'" class="btn btn-primary" disabled>{{ $t("installing") }}</button> | ||||
|             <button v-else-if="status === 'uninstalling'" class="btn btn-danger" disabled>{{ $t("uninstalling") }}</button> | ||||
|             <button v-else-if="plugin.installed || status === 'installed'" class="btn btn-danger" @click="deleteConfirm">{{ $t("uninstall") }}</button> | ||||
|             <button v-else class="btn btn-primary" @click="install">{{ $t("install") }}</button> | ||||
|         </div> | ||||
|  | ||||
|         <Confirm ref="confirmDelete" btn-style="btn-danger" :yes-text="$t('Yes')" :no-text="$t('No')" @yes="uninstall"> | ||||
|             {{ $t("confirmUninstallPlugin") }} | ||||
|         </Confirm> | ||||
|     </div> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import Confirm from "./Confirm.vue"; | ||||
|  | ||||
| export default { | ||||
|     components: { | ||||
|         Confirm, | ||||
|     }, | ||||
|     props: { | ||||
|         plugin: { | ||||
|             type: Object, | ||||
|             required: true, | ||||
|         }, | ||||
|     }, | ||||
|     data() { | ||||
|         return { | ||||
|             status: "", | ||||
|         }; | ||||
|     }, | ||||
|     methods: { | ||||
|         /** | ||||
|          * Show confirmation for deleting a tag | ||||
|          */ | ||||
|         deleteConfirm() { | ||||
|             this.$refs.confirmDelete.show(); | ||||
|         }, | ||||
|  | ||||
|         install() { | ||||
|             this.status = "installing"; | ||||
|  | ||||
|             this.$root.getSocket().emit("installPlugin", this.plugin.repo, this.plugin.name, (res) => { | ||||
|                 if (res.ok) { | ||||
|                     this.status = ""; | ||||
|                     // eslint-disable-next-line vue/no-mutating-props | ||||
|                     this.plugin.installed = true; | ||||
|                 } else { | ||||
|                     this.$root.toastRes(res); | ||||
|                 } | ||||
|             }); | ||||
|         }, | ||||
|  | ||||
|         uninstall() { | ||||
|             this.status = "uninstalling"; | ||||
|  | ||||
|             this.$root.getSocket().emit("uninstallPlugin", this.plugin.name, (res) => { | ||||
|                 if (res.ok) { | ||||
|                     this.status = ""; | ||||
|                     // eslint-disable-next-line vue/no-mutating-props | ||||
|                     this.plugin.installed = false; | ||||
|                 } else { | ||||
|                     this.$root.toastRes(res); | ||||
|                 } | ||||
|             }); | ||||
|         } | ||||
|     } | ||||
| }; | ||||
| </script> | ||||
|  | ||||
| <style lang="scss" scoped> | ||||
| @import "../assets/vars.scss"; | ||||
|  | ||||
| .plugin-item { | ||||
|     display: flex; | ||||
|     justify-content: space-between; | ||||
|     align-content: center; | ||||
|     align-items: center; | ||||
|  | ||||
|     .info { | ||||
|         margin-right: 10px; | ||||
|     } | ||||
|  | ||||
|     .description { | ||||
|         font-size: 13px; | ||||
|         margin-bottom: 0; | ||||
|     } | ||||
|  | ||||
|     .version { | ||||
|         font-size: 13px; | ||||
|     } | ||||
| } | ||||
| </style> | ||||
| @@ -155,7 +155,7 @@ export default { | ||||
|             // We must check if there are any elements in monitorList to | ||||
|             // prevent undefined errors if it hasn't been loaded yet | ||||
|             if (this.$parent.editMode && ignoreSendUrl && Object.keys(this.$root.monitorList).length) { | ||||
|                 return this.$root.monitorList[monitor.element.id].type === "http" || this.$root.monitorList[monitor.element.id].type === "keyword"; | ||||
|                 return this.$root.monitorList[monitor.element.id].type === "http" || this.$root.monitorList[monitor.element.id].type === "keyword" || this.$root.monitorList[monitor.element.id].type === "json-query"; | ||||
|             } | ||||
|             return monitor.element.sendUrl && monitor.element.url && monitor.element.url !== "https://" && !this.editMode; | ||||
|         }, | ||||
|   | ||||
| @@ -5,7 +5,18 @@ | ||||
|     </div> | ||||
|  | ||||
|     <div class="mb-3"> | ||||
|         <label for="twilio-auth-token" class="form-label">{{ $t("Auth Token") }}</label> | ||||
|         <label for="twilio-apikey-token" class="form-label">{{ $t("Api Key (optional)") }}</label> | ||||
|         <input id="twilio-apikey-token" v-model="$parent.notification.twilioApiKey" type="text" class="form-control"> | ||||
|         <div class="form-text"> | ||||
|             <p> | ||||
|                 The API key is optional but recommended. You can provide either Account SID and AuthToken | ||||
|                 from the may TwilioConsole page or Account SID and the pair of Api Key and Api Key secret | ||||
|             </p> | ||||
|         </div> | ||||
|     </div> | ||||
|  | ||||
|     <div class="mb-3"> | ||||
|         <label for="twilio-auth-token" class="form-label">{{ $t("Auth Token / Api Key Secret") }}</label> | ||||
|         <input id="twilio-auth-token" v-model="$parent.notification.twilioAuthToken" type="text" class="form-control" required> | ||||
|     </div> | ||||
|  | ||||
|   | ||||
| @@ -12,61 +12,97 @@ | ||||
|     </div> | ||||
|  | ||||
|     <div class="mb-3"> | ||||
|         <label for="webhook-content-type" class="form-label">{{ | ||||
|             $t("Content Type") | ||||
|         <label for="webhook-request-body" class="form-label">{{ | ||||
|             $t("Request Body") | ||||
|         }}</label> | ||||
|         <select | ||||
|             id="webhook-content-type" | ||||
|             id="webhook-request-body" | ||||
|             v-model="$parent.notification.webhookContentType" | ||||
|             class="form-select" | ||||
|             required | ||||
|         > | ||||
|             <option value="json">application/json</option> | ||||
|             <option value="form-data">multipart/form-data</option> | ||||
|             <option value="json">{{ $t("webhookBodyPresetOption", ["application/json"]) }}</option> | ||||
|             <option value="form-data">{{ $t("webhookBodyPresetOption", ["multipart/form-data"]) }}</option> | ||||
|             <option value="custom">{{ $t("webhookBodyCustomOption") }}</option> | ||||
|         </select> | ||||
|  | ||||
|         <div class="form-text"> | ||||
|             <div v-if="$parent.notification.webhookContentType == 'json'"> | ||||
|                 <p>{{ $t("webhookJsonDesc", ['"application/json"']) }}</p> | ||||
|             </div> | ||||
|             <div v-if="$parent.notification.webhookContentType == 'form-data'"> | ||||
|                 <i18n-t tag="p" keypath="webhookFormDataDesc"> | ||||
|                 <template #multipart>"multipart/form-data"</template> | ||||
|                     <template #multipart>multipart/form-data"</template> | ||||
|                     <template #decodeFunction> | ||||
|                         <strong>json_decode($_POST['data'])</strong> | ||||
|                     </template> | ||||
|                 </i18n-t> | ||||
|             </div> | ||||
|             <div v-if="$parent.notification.webhookContentType == 'custom'"> | ||||
|                 <i18n-t tag="p" keypath="webhookCustomBodyDesc"> | ||||
|                     <template #msg> | ||||
|                         <code>msg</code> | ||||
|                     </template> | ||||
|                     <template #heartbeat> | ||||
|                         <code>heartbeatJSON</code> | ||||
|                     </template> | ||||
|                     <template #monitor> | ||||
|                         <code>monitorJSON</code> | ||||
|                     </template> | ||||
|                 </i18n-t> | ||||
|             </div> | ||||
|         </div> | ||||
|  | ||||
|         <textarea | ||||
|             v-if="$parent.notification.webhookContentType == 'custom'" | ||||
|             id="customBody" | ||||
|             v-model="$parent.notification.webhookCustomBody" | ||||
|             class="form-control" | ||||
|             :placeholder="customBodyPlaceholder" | ||||
|         ></textarea> | ||||
|     </div> | ||||
|  | ||||
|     <div class="mb-3"> | ||||
|         <i18n-t | ||||
|             tag="label" | ||||
|             class="form-label" | ||||
|             for="additionalHeaders" | ||||
|             keypath="webhookAdditionalHeadersTitle" | ||||
|         > | ||||
|         </i18n-t> | ||||
|         <div class="form-check form-switch"> | ||||
|             <input v-model="showAdditionalHeadersField" class="form-check-input" type="checkbox"> | ||||
|             <label class="form-check-label">{{ $t("webhookAdditionalHeadersTitle") }}</label> | ||||
|         </div> | ||||
|         <div class="form-text"> | ||||
|             <i18n-t tag="p" keypath="webhookAdditionalHeadersDesc"> </i18n-t> | ||||
|         </div> | ||||
|         <textarea | ||||
|             v-if="showAdditionalHeadersField" | ||||
|             id="additionalHeaders" | ||||
|             v-model="$parent.notification.webhookAdditionalHeaders" | ||||
|             class="form-control" | ||||
|             :placeholder="headersPlaceholder" | ||||
|         ></textarea> | ||||
|         <div class="form-text"> | ||||
|             <i18n-t tag="p" keypath="webhookAdditionalHeadersDesc"> </i18n-t> | ||||
|         </div> | ||||
|     </div> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| export default { | ||||
|     data() { | ||||
|         return { | ||||
|             showAdditionalHeadersField: this.$parent.notification.webhookAdditionalHeaders != null, | ||||
|         }; | ||||
|     }, | ||||
|     computed: { | ||||
|         headersPlaceholder() { | ||||
|             return this.$t("Example:", [ | ||||
|                 ` | ||||
| { | ||||
|     "HeaderName": "HeaderValue" | ||||
|     "Authorization": "Authorization Token" | ||||
| }`, | ||||
|             ]); | ||||
|         }, | ||||
|         customBodyPlaceholder() { | ||||
|             return `Example: | ||||
| { | ||||
|     "Title": "Uptime Kuma Alert - {{ monitorJSON['name'] }}", | ||||
|     "Body": "{{ msg }}" | ||||
| }`; | ||||
|         } | ||||
|     }, | ||||
| }; | ||||
| </script> | ||||
|   | ||||
| @@ -1,57 +0,0 @@ | ||||
| <template> | ||||
|     <div> | ||||
|         <div class="mt-3">{{ remotePluginListMsg }}</div> | ||||
|         <PluginItem v-for="plugin in remotePluginList" :key="plugin.id" :plugin="plugin" /> | ||||
|     </div> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import PluginItem from "../PluginItem.vue"; | ||||
|  | ||||
| export default { | ||||
|     components: { | ||||
|         PluginItem | ||||
|     }, | ||||
|  | ||||
|     data() { | ||||
|         return { | ||||
|             remotePluginList: [], | ||||
|             remotePluginListMsg: "", | ||||
|         }; | ||||
|     }, | ||||
|  | ||||
|     computed: { | ||||
|         pluginList() { | ||||
|             return this.$parent.$parent.$parent.pluginList; | ||||
|         }, | ||||
|         settings() { | ||||
|             return this.$parent.$parent.$parent.settings; | ||||
|         }, | ||||
|         saveSettings() { | ||||
|             return this.$parent.$parent.$parent.saveSettings; | ||||
|         }, | ||||
|         settingsLoaded() { | ||||
|             return this.$parent.$parent.$parent.settingsLoaded; | ||||
|         }, | ||||
|     }, | ||||
|  | ||||
|     async mounted() { | ||||
|         this.loadList(); | ||||
|     }, | ||||
|  | ||||
|     methods: { | ||||
|         loadList() { | ||||
|             this.remotePluginListMsg = this.$t("Loading") + "..."; | ||||
|  | ||||
|             this.$root.getSocket().emit("getPluginList", (res) => { | ||||
|                 if (res.ok) { | ||||
|                     this.remotePluginList = res.pluginList; | ||||
|                     this.remotePluginListMsg = ""; | ||||
|                 } else { | ||||
|                     this.remotePluginListMsg = this.$t("loadingError") + " " + res.msg; | ||||
|                 } | ||||
|             }); | ||||
|         } | ||||
|     }, | ||||
| }; | ||||
| </script> | ||||
| @@ -51,6 +51,9 @@ | ||||
|     "Ping": "Ping", | ||||
|     "Monitor Type": "Monitor Type", | ||||
|     "Keyword": "Keyword", | ||||
|     "Invert Keyword": "Invert Keyword", | ||||
|     "Expected Value": "Expected Value", | ||||
|     "Json Query": "Json Query", | ||||
|     "Friendly Name": "Friendly Name", | ||||
|     "URL": "URL", | ||||
|     "Hostname": "Hostname", | ||||
| @@ -195,8 +198,11 @@ | ||||
|     "Content Type": "Content Type", | ||||
|     "webhookJsonDesc": "{0} is good for any modern HTTP servers such as Express.js", | ||||
|     "webhookFormDataDesc": "{multipart} is good for PHP. The JSON will need to be parsed with {decodeFunction}", | ||||
|     "webhookCustomBodyDesc": "Define a custom HTTP Body for the request. Template variables {msg}, {heartbeat}, {monitor} are accepted.", | ||||
|     "webhookAdditionalHeadersTitle": "Additional Headers", | ||||
|     "webhookAdditionalHeadersDesc": "Sets additional headers sent with the webhook.", | ||||
|     "webhookAdditionalHeadersDesc": "Sets additional headers sent with the webhook. Each header should be defined as a JSON key/value.", | ||||
|     "webhookBodyPresetOption": "Preset - {0}", | ||||
|     "webhookBodyCustomOption": "Custom Body", | ||||
|     "Webhook URL": "Webhook URL", | ||||
|     "Application Token": "Application Token", | ||||
|     "Server URL": "Server URL", | ||||
| @@ -518,6 +524,8 @@ | ||||
|     "passwordNotMatchMsg": "The repeat password does not match.", | ||||
|     "notificationDescription": "Notifications must be assigned to a monitor to function.", | ||||
|     "keywordDescription": "Search keyword in plain HTML or JSON response. The search is case-sensitive.", | ||||
|     "invertKeywordDescription": "Look for the keyword to be absent rather than present.", | ||||
|     "jsonQueryDescription": "Do a json Query against the response and check for expected value (Return value will get converted into string for comparison). Check out <a href='https://jsonata.org/'>jsonata.org</a> for the documentation about the query language. A playground can be found <a href='https://try.jsonata.org/'>here</a>.", | ||||
|     "backupDescription": "You can backup all monitors and notifications into a JSON file.", | ||||
|     "backupDescription2": "Note: history and event data is not included.", | ||||
|     "backupDescription3": "Sensitive data such as notification tokens are included in the export file; please store export securely.", | ||||
| @@ -725,7 +733,8 @@ | ||||
|     "ntfyAuthenticationMethod": "Authentication Method", | ||||
|     "ntfyUsernameAndPassword": "Username and Password", | ||||
|     "twilioAccountSID": "Account SID", | ||||
|     "twilioAuthToken": "Auth Token", | ||||
|     "twilioApiKey": "Api Key (optional)", | ||||
|     "twilioAuthToken": "Auth Token / Api Key Secret", | ||||
|     "twilioFromNumber": "From Number", | ||||
|     "twilioToNumber": "To Number", | ||||
|     "Monitor Setting": "{0}'s Monitor Setting", | ||||
| @@ -756,6 +765,7 @@ | ||||
|     "Monitor Group": "Monitor Group", | ||||
|     "noGroupMonitorMsg": "Not Available. Create a Group Monitor First.", | ||||
|     "Close": "Close", | ||||
|     "Request Body": "Request Body", | ||||
|     "showCertificateExpiry": "Show Certificate Expiry", | ||||
|     "noOrBadCertificate": "No/Bad Certificate" | ||||
| } | ||||
|   | ||||
| @@ -30,6 +30,9 @@ export default { | ||||
|         theme() { | ||||
|             // As entry can be status page now, set forceStatusPageTheme to true to use status page theme | ||||
|             if (this.forceStatusPageTheme) { | ||||
|                 if (this.statusPageTheme === "auto") { | ||||
|                     return this.system; | ||||
|                 } | ||||
|                 return this.statusPageTheme; | ||||
|             } | ||||
|  | ||||
|   | ||||
| @@ -8,12 +8,20 @@ | ||||
|                 <Tag v-for="tag in monitor.tags" :key="tag.id" :item="tag" :size="'sm'" /> | ||||
|             </div> | ||||
|             <p class="url"> | ||||
|                 <a v-if="monitor.type === 'http' || monitor.type === 'keyword' || monitor.type === 'mp-health' " :href="monitor.url" target="_blank" rel="noopener noreferrer">{{ filterPassword(monitor.url) }}</a> | ||||
|                 <a v-if="monitor.type === 'http' || monitor.type === 'keyword' || monitor.type === 'json-query' || monitor.type === 'mp-health' " :href="monitor.url" target="_blank" rel="noopener noreferrer">{{ filterPassword(monitor.url) }}</a> | ||||
|                 <span v-if="monitor.type === 'port'">TCP Port {{ monitor.hostname }}:{{ monitor.port }}</span> | ||||
|                 <span v-if="monitor.type === 'ping'">Ping: {{ monitor.hostname }}</span> | ||||
|                 <span v-if="monitor.type === 'keyword'"> | ||||
|                     <br> | ||||
|                     <span>{{ $t("Keyword") }}:</span> <span class="keyword">{{ monitor.keyword }}</span> | ||||
|                     <span>{{ $t("Keyword") }}: </span> | ||||
|                     <span class="keyword">{{ monitor.keyword }}</span> | ||||
|                     <span v-if="monitor.invertKeyword" alt="Inverted keyword" class="keyword-inverted"> ↧</span> | ||||
|                 </span> | ||||
|                 <span v-if="monitor.type === 'json-query'"> | ||||
|                     <br> | ||||
|                     <span>{{ $t("Json Query") }}:</span> <span class="keyword">{{ monitor.jsonPath }}</span> | ||||
|                     <br> | ||||
|                     <span>{{ $t("Expected Value") }}:</span> <span class="keyword">{{ monitor.expectedValue }}</span> | ||||
|                 </span> | ||||
|                 <span v-if="monitor.type === 'dns'">[{{ monitor.dns_resolve_type }}] {{ monitor.hostname }} | ||||
|                     <br> | ||||
| @@ -432,7 +440,7 @@ export default { | ||||
|                 translationPrefix = "Avg. "; | ||||
|             } | ||||
|  | ||||
|             if (this.monitor.type === "http" || this.monitor.type === "keyword") { | ||||
|             if (this.monitor.type === "http" || this.monitor.type === "keyword" || this.monitor.type === "json-query") { | ||||
|                 return this.$t(translationPrefix + "Response"); | ||||
|             } | ||||
|  | ||||
| @@ -582,6 +590,10 @@ table { | ||||
|         color: $dark-font-color; | ||||
|     } | ||||
|  | ||||
|     .keyword-inverted { | ||||
|         color: $dark-font-color; | ||||
|     } | ||||
|  | ||||
|     .dropdown-clear-data { | ||||
|         ul { | ||||
|             background-color: $dark-bg; | ||||
|   | ||||
| @@ -27,6 +27,9 @@ | ||||
|                                         <option value="keyword"> | ||||
|                                             HTTP(s) - {{ $t("Keyword") }} | ||||
|                                         </option> | ||||
|                                         <option value="json-query"> | ||||
|                                             HTTP(s) - {{ $t("Json Query") }} | ||||
|                                         </option> | ||||
|                                         <option value="grpc-keyword"> | ||||
|                                             gRPC(s) - {{ $t("Keyword") }} | ||||
|                                         </option> | ||||
| @@ -97,7 +100,7 @@ | ||||
|                             </div> | ||||
|  | ||||
|                             <!-- URL --> | ||||
|                             <div v-if="monitor.type === 'http' || monitor.type === 'keyword' || monitor.type === 'real-browser' " class="my-3"> | ||||
|                             <div v-if="monitor.type === 'http' || monitor.type === 'keyword' || monitor.type === 'json-query' || monitor.type === 'real-browser' " class="my-3"> | ||||
|                                 <label for="url" class="form-label">{{ $t("URL") }}</label> | ||||
|                                 <input id="url" v-model="monitor.url" type="url" class="form-control" pattern="https?://.+" required> | ||||
|                             </div> | ||||
| @@ -127,6 +130,31 @@ | ||||
|                                 </div> | ||||
|                             </div> | ||||
|  | ||||
|                             <!-- Invert keyword --> | ||||
|                             <div v-if="monitor.type === 'keyword' || monitor.type === 'grpc-keyword'" class="my-3 form-check"> | ||||
|                                 <input id="invert-keyword" v-model="monitor.invertKeyword" class="form-check-input" type="checkbox"> | ||||
|                                 <label class="form-check-label" for="invert-keyword"> | ||||
|                                     {{ $t("Invert Keyword") }} | ||||
|                                 </label> | ||||
|                                 <div class="form-text"> | ||||
|                                     {{ $t("invertKeywordDescription") }} | ||||
|                                 </div> | ||||
|                             </div> | ||||
|  | ||||
|                             <!-- Json Query --> | ||||
|                             <div v-if="monitor.type === 'json-query'" class="my-3"> | ||||
|                                 <label for="jsonPath" class="form-label">{{ $t("Json Query") }}</label> | ||||
|                                 <input id="jsonPath" v-model="monitor.jsonPath" type="text" class="form-control" required> | ||||
|  | ||||
|                                 <!-- eslint-disable-next-line vue/no-v-html --> | ||||
|                                 <div class="form-text" v-html="$t('jsonQueryDescription')"> | ||||
|                                 </div> | ||||
|                                 <br> | ||||
|  | ||||
|                                 <label for="expectedValue" class="form-label">{{ $t("Expected Value") }}</label> | ||||
|                                 <input id="expectedValue" v-model="monitor.expectedValue" type="text" class="form-control" required> | ||||
|                             </div> | ||||
|  | ||||
|                             <!-- Game --> | ||||
|                             <!-- GameDig only --> | ||||
|                             <div v-if="monitor.type === 'gamedig'" class="my-3"> | ||||
| @@ -356,7 +384,7 @@ | ||||
|  | ||||
|                             <h2 v-if="monitor.type !== 'push'" class="mt-5 mb-2">{{ $t("Advanced") }}</h2> | ||||
|  | ||||
|                             <div v-if="monitor.type === 'http' || monitor.type === 'keyword' " class="my-3 form-check"> | ||||
|                             <div v-if="monitor.type === 'http' || monitor.type === 'keyword' || monitor.type === 'json-query' " class="my-3 form-check"> | ||||
|                                 <input id="expiry-notification" v-model="monitor.expiryNotification" class="form-check-input" type="checkbox"> | ||||
|                                 <label class="form-check-label" for="expiry-notification"> | ||||
|                                     {{ $t("Certificate Expiry Notification") }} | ||||
| @@ -365,7 +393,7 @@ | ||||
|                                 </div> | ||||
|                             </div> | ||||
|  | ||||
|                             <div v-if="monitor.type === 'http' || monitor.type === 'keyword' " class="my-3 form-check"> | ||||
|                             <div v-if="monitor.type === 'http' || monitor.type === 'keyword' || monitor.type === 'json-query' " class="my-3 form-check"> | ||||
|                                 <input id="ignore-tls" v-model="monitor.ignoreTls" class="form-check-input" type="checkbox" value=""> | ||||
|                                 <label class="form-check-label" for="ignore-tls"> | ||||
|                                     {{ $t("ignoreTLSError") }} | ||||
| @@ -457,7 +485,7 @@ | ||||
|                             </button> | ||||
|  | ||||
|                             <!-- Proxies --> | ||||
|                             <div v-if="monitor.type === 'http' || monitor.type === 'keyword'"> | ||||
|                             <div v-if="monitor.type === 'http' || monitor.type === 'keyword' || monitor.type === 'json-query'"> | ||||
|                                 <h2 class="mt-5 mb-2">{{ $t("Proxy") }}</h2> | ||||
|                                 <p v-if="$root.proxyList.length === 0"> | ||||
|                                     {{ $t("Not available, please setup.") }} | ||||
| @@ -485,7 +513,7 @@ | ||||
|                             </div> | ||||
|  | ||||
|                             <!-- HTTP Options --> | ||||
|                             <template v-if="monitor.type === 'http' || monitor.type === 'keyword' "> | ||||
|                             <template v-if="monitor.type === 'http' || monitor.type === 'keyword' || monitor.type === 'json-query' "> | ||||
|                                 <h2 class="mt-5 mb-2">{{ $t("HTTP Options") }}</h2> | ||||
|  | ||||
|                                 <!-- Method --> | ||||
| @@ -1107,7 +1135,7 @@ message HealthCheckResponse { | ||||
|                 this.monitor.body = JSON.stringify(JSON.parse(this.monitor.body), null, 4); | ||||
|             } | ||||
|  | ||||
|             if (this.monitor.type && this.monitor.type !== "http" && this.monitor.type !== "keyword") { | ||||
|             if (this.monitor.type && this.monitor.type !== "http" && (this.monitor.type !== "keyword" || this.monitor.type !== "json-query")) { | ||||
|                 this.monitor.httpBodyEncoding = null; | ||||
|             } | ||||
|  | ||||
|   | ||||
| @@ -116,12 +116,6 @@ export default { | ||||
|                 backup: { | ||||
|                     title: this.$t("Backup"), | ||||
|                 }, | ||||
|                 /* | ||||
|                 Hidden for now: Unfortunately, after some test, I found that Playwright requires a lot of libraries to be installed on the Linux host in order to start Chrome or Firefox. | ||||
|                 It will be hard to install, so I hide this feature for now. But it still accessible via URL: /settings/plugins. | ||||
|                 plugins: { | ||||
|                     title: this.$tc("plugin", 2), | ||||
|                 },*/ | ||||
|                 about: { | ||||
|                     title: this.$t("About"), | ||||
|                 }, | ||||
|   | ||||
| @@ -19,7 +19,6 @@ import DockerHosts from "./components/settings/Docker.vue"; | ||||
| import MaintenanceDetails from "./pages/MaintenanceDetails.vue"; | ||||
| import ManageMaintenance from "./pages/ManageMaintenance.vue"; | ||||
| import APIKeys from "./components/settings/APIKeys.vue"; | ||||
| import Plugins from "./components/settings/Plugins.vue"; | ||||
|  | ||||
| // Settings - Sub Pages | ||||
| import Appearance from "./components/settings/Appearance.vue"; | ||||
| @@ -130,10 +129,6 @@ const routes = [ | ||||
|                                 path: "backup", | ||||
|                                 component: Backup, | ||||
|                             }, | ||||
|                             { | ||||
|                                 path: "plugins", | ||||
|                                 component: Plugins, | ||||
|                             }, | ||||
|                             { | ||||
|                                 path: "about", | ||||
|                                 component: About, | ||||
|   | ||||
		Reference in New Issue
	
	Block a user