Compare commits

..

77 Commits

Author SHA1 Message Date
Louis Lam
621419e434 Update to 1.23.9 2023-12-10 20:43:29 +08:00
Louis Lam
482049c72b Merge pull request from GHSA-88j4-pcx8-q4q3
* WIP, still need to handle npm run reset-password

* Implement it for "npm run reset-password"

Bug fixes and change along with this commit
- Move `ssl`, `hostname`, `port` to ./server/config.js, so `reset-password` is able to read it
- Fix: FBSD is missing, no idea who dropped it.
- Fix: Frontend code should not require any backend code (./server/config.js), moved "badgeConstants" to the common util (./src/util.ts) and drop vite-common.js

* Minor
2023-12-10 20:40:40 +08:00
Louis Lam
2815cc73cf Merge pull request from GHSA-mj22-23ff-2hrr
* WIP

* WIP

* Handle parsing error

* Fix matching origin issue
2023-12-10 20:39:43 +08:00
Louis Lam
e1147c06aa Update denpendecies 2023-12-10 02:45:42 +08:00
Ritik Singh
abc8f2b131 Fix: Correct Maintenance Start/End Time Input to Use Explicitly Specified Timezone (#4186) 2023-12-09 18:27:07 +08:00
Frank Elsinga
777ef6bc7b chore: added a helptext for ntfy's priority field (#4175)
* added a helptext for `ntfy`'s `priority` field

* linting fixes

* removed an unnecessary `Math.max` call
2023-12-09 17:37:33 +08:00
Louis Lam
b244e8fcbb Re-export the icon on vectr.com, so it can be editable again. The current icon.svg was reduced size by a contributor previously, but the border is detached after that, which cannot edit by any svg editor anymore. 2023-12-09 04:24:19 +08:00
Frank Elsinga
031947319a Add an aria-label to the monitor search box (#4163)
* added the `Search monitored sites` label

* rebase
2023-12-05 01:15:28 +08:00
Adam Stachowicz
74a908a069 Max ESLint warnings 0 (#4158)
* Fix ESLint warnings. Update workflows. 0 ESLint warnings for auto-test

* json-yaml-validate: Fix `unable to find version `v2``
2023-12-04 18:19:18 +08:00
Frank Elsinga
9c56c9b346 Fixed the buttons of ActionsSelect and ActionsInput having a default type="submit" (#4162)
* fixed the buttons having a default type="submit"

* fixed linting issue
2023-12-04 01:15:40 +08:00
Louis Lam
37666bf35f Update to 1.23.8 2023-12-02 17:51:06 +08:00
Louis Lam
90badfabee Update dependencies 2023-12-02 15:41:59 +08:00
Frank Elsinga
e3396251a8 accessible domain selector (#4133)
* made the status domain selector more accessible

* linting fix

* implemented the suggested changes

---------

Co-authored-by: Louis Lam <louislam@users.noreply.github.com>
2023-12-01 15:05:01 +08:00
Frank Elsinga
9c9a086788 accessible ActionSelect/ ActionInput (#4132)
* made sure that the ActionSelect'or has correct accessibiltiy tags

* fixed linting error

* improved the ActionInputs accessibility
2023-12-01 14:34:37 +08:00
Louis Lam
9fb95fe95e Add support for /snap/bin/chromium (#4141) 2023-12-01 14:25:41 +08:00
Louis Lam
1e75d81bcf Update apprise from 1.4.5 to 1.6.0 (#4140) 2023-12-01 14:16:35 +08:00
Louis Lam
cb3a104dc0 Default Retries from 1 to 0 (#4139)
* Default "Retries" from 1 to 0
2023-12-01 14:09:13 +08:00
Louis Lam
57a18958d6 Update gamedig from ~4.1.0 to ^4.2.0 (#4136) 2023-12-01 11:42:31 +08:00
Louis Lam
1708b67949 Change execSync/spawnSync to async (#4123)
* WIP

* Add missing await

* Update package-lock.json
2023-11-30 16:12:04 +08:00
Louis Lam
73239d441d Update to 1.23.7 2023-11-24 18:49:27 +08:00
Louis Lam
4ceeb304f1 Add a script to prepare a changelog 2023-11-24 18:44:54 +08:00
Louis Lam
711380bbbe Merge pull request #4095 from louislam/update-3
Rewrite Tailscale ping using spawnSync
2023-11-24 17:44:24 +08:00
Louis Lam
9536c6aa6a Minor 2023-11-24 17:33:13 +08:00
Louis Lam
4255496b11 Rewrite Tailscale ping using spawnSync 2023-11-24 17:29:42 +08:00
Louis Lam
f28dccf4e1 Merge pull request from GHSA-v4v2-8h88-65qj 2023-11-24 17:18:01 +08:00
Louis Lam
b689733d59 Fix getGameList, testChrome without checkLogin 2023-11-24 16:37:52 +08:00
Louis Lam
afaa7bb2f0 Do not process debug log for production 2023-11-24 16:03:35 +08:00
Louis Lam
121d1a11af Revert "Restart running monitors if no heartbeat (#3952)" (#4088)
This reverts commit c43223a16d.
2023-11-24 02:23:38 +08:00
Louis Lam
8e61158758 Close the client postgresql connection after rejection. (#4084)
Co-authored-by: Manuel Vázquez Acosta <manuel@merchise.org>
2023-11-22 19:50:03 +08:00
Louis Lam
bf58838b89 +10 seconds for Abort signal (#4053)
* Debug only

* Remove debug
2023-11-22 16:03:03 +08:00
Nelson Chan
33ce0ef02c Fix: Improve error message on timeout (#4054)
* Fix: Improve error message on timeout

* Chore: Format
2023-11-21 23:56:17 +08:00
Louis Lam
c1aaad0d85 Update to 1.23.6 2023-11-18 11:37:14 +08:00
Louis Lam
954e05b72f Fix #4051 2023-11-18 11:33:34 +08:00
Louis Lam
6d4a45f18c Update to 1.23.5 2023-11-18 01:23:53 +08:00
Louis Lam
f0975cd929 Should be a final ulitmate fix for request timeout issue (#4045)
* Try to fix timeout again

* Ops
2023-11-18 01:17:54 +08:00
Louis Lam
40d6a21453 Fix kafka migration script again (#4043) 2023-11-17 15:21:29 +08:00
Louis Lam
b383392e8f Remains Node.js 16' SSL behavior for 1.23.X (#4044) 2023-11-17 15:21:08 +08:00
Nelson Chan
9964b6c4d8 Fix: Update monitor object on pause (#4032) 2023-11-16 20:41:35 +08:00
Louis Lam
d56bf08cd7 Update to 1.23.4 2023-11-13 15:23:32 +08:00
Louis Lam
291d5d7c55 Update dependencies 2023-11-13 15:22:51 +08:00
Louis Lam
8e3ff25f7b Followup #3864, rebase for 1.23.x (#4016)
* Fix: Use ActionSelect Docker Host & validate input

* Fix: Handle docker host deleted while editing

* UI: Use add for ActionSelect & prevent delete instead

---------

Co-authored-by: Nelson Chan <chakflying@hotmail.com>
2023-11-12 20:32:40 +08:00
Louis Lam
6e80c850f4 Should be an ulitmate fix for request timeout issue (#4011) 2023-11-12 13:50:51 +08:00
Muhammed Hussein karimi
0608881954 🐛 fix: kafka producer booleans migration null values (#3984)
Signed-off-by: Muhammed Hussein Karimi <info@karimi.dev>
2023-11-10 00:32:54 +08:00
Nelson Chan
38efd97b28 Fix: Support float ping in push route (#3987) 2023-11-09 23:39:44 +08:00
Louis Lam
ce0ba6c0ca Fix/axios abort signal for 1.23.X (#3971)
* Fix: Add axios abort signal

* Chore: Fix comment

---------

Co-authored-by: Nelson Chan <chakflying@hotmail.com>
2023-11-01 10:10:48 +08:00
Louis Lam
c43223a16d Restart running monitors if no heartbeat (#3952) 2023-11-01 09:36:12 +08:00
Muhammed Hussein karimi
9f170a68d7 🐛 fix: boolean fields in kafka producer monitor (#3949)
* 🐛 fix: boolean fields in kafka producer monitor

Signed-off-by: Muhammed Hussein Karimi <info@karimi.dev>

* 🐛 fix: boolean fields db patch table modify

Signed-off-by: Muhammed Hussein Karimi <info@karimi.dev>

* ✏️  typo: remove `_old` COLUMNs in patch-fix-kafka-producer-booleans

Signed-off-by: Muhammed Hussein Karimi <info@karimi.dev>

---------

Signed-off-by: Muhammed Hussein Karimi <info@karimi.dev>
2023-10-28 14:42:55 +08:00
Louis Lam
1a862e47ab Check if the password changed when user is not null 2023-10-23 06:21:39 +08:00
Nelson Chan
e64bf0e3fe Fix: Stop notification check on root certs (#3874)
* Fix: Stop notification check on root certs

* Chore: Use Set for optimization

* Fix: Manually calculate SHA256 to support node v14
2023-10-16 02:20:38 +08:00
Louis Lam
523d137e2b Lint 2023-10-16 00:43:07 +08:00
Louis Lam
18169c59a1 [MySQL monitor] Split password into a standalone field (#3899) 2023-10-16 00:38:56 +08:00
Louis Lam
4ccf263481 Update docker image base from Node.js 16 to Node.js 18 for Uptime Kuma v1 (#3901) 2023-10-16 00:27:47 +08:00
Louis Lam
1c13a75970 Fix #3868 postgres monitor could possibly crash Uptime Kuma (#3880)
* Bump pg

* Handle uncaughtException

* Fix parsing issue of postgres connection and fix the query example
2023-10-13 02:50:10 +08:00
Louis Lam
c3e3f27457 Update to 1.23.3 2023-10-09 20:26:18 +08:00
Louis Lam
794f1810bf Minor 2023-10-09 20:09:29 +08:00
Louis Lam
168357d93c Update dependencies 2023-10-09 20:05:50 +08:00
Louis Lam
476deb9fec Pin npm@9 2023-10-09 07:33:52 +08:00
Louis Lam
a36f2a75ca Enable auto-test for 1.23.X branch 2023-10-09 07:23:30 +08:00
Louis Lam
88afab6571 Merge pull request from GHSA-g9v2-wqcj-j99g
* Fix attempt

* Update message
2023-10-09 07:01:54 +08:00
Nelson Chan
bd9c44cccf Fix: Disable status page saving before getData 2023-10-09 06:41:07 +08:00
前端小武
1b148786a5 Fix: Update x-forwarded-host field when using reverse proxy (#3726) 2023-10-09 06:31:52 +08:00
Louis Lam
66a10b8993 Pump gamedig from 4.0.X to 4.1.X and update dependencies 2023-10-03 04:51:02 +08:00
Muhammed Hussein karimi
2ab21ccf8a 🐛 fix: kafka producer bugs (#3771)
* 🐛 fix: missing Kafka Producer SSL option in frontend object

Signed-off-by: Muhammed Hussein Karimi <info@karimi.dev>

* ♻️  refactor: better error handling of kafka producer

Signed-off-by: Muhammed Hussein Karimi <info@karimi.dev>

---------

Signed-off-by: Muhammed Hussein Karimi <info@karimi.dev>
2023-09-24 03:30:15 +08:00
Marvin A. Ruder
90d0e8ccde Enable status page certificate expiry badge for all HTTP(s) monitors (#3649) 2023-09-24 03:18:18 +08:00
Louis Lam
16a396debb Similar to #3763, but for 1.23.3 2023-09-21 20:38:54 +08:00
Louis Lam
6b3d69e1d3 Ignore /extra/healthcheck-armv7 2023-09-18 03:59:56 +08:00
Louis Lam
b3b8e9f3a0 Update to 1.23.2 2023-09-18 03:04:29 +08:00
Louis Lam
e5345848a2 Update dependencies 2023-09-18 03:01:58 +08:00
FC (Fay) Stegerman
d8a8f6c08b EditMonitor: add missing config UI for json-query (#3751) 2023-09-17 20:39:07 +08:00
Louis Lam
f98a1ce077 Ignore some build files 2023-09-17 20:32:49 +08:00
Louis Lam
86fa57449e Fix #3596 2023-09-17 02:56:19 +08:00
Louis Lam
ff51704cdf Fix #3712 2023-09-17 02:40:08 +08:00
Henrik Gerdes
33804d8823 fix: respect the user defined oauth2 auth method (#3727) 2023-09-16 05:13:20 +08:00
Nelson Chan
1e12ca4786 Fix: Disable save button on submit (#3729) 2023-09-16 05:11:18 +08:00
Nelson Chan
0af4ee6c34 Fix: Missing await for isActive (#3717) 2023-09-10 01:54:03 +08:00
ZaneL1u
1f29fabe64 fix: fix the judgment condition of Tailscale (#3701) 2023-09-07 16:15:21 +08:00
Louis Lam
c4e222d1e6 Update README.md 2023-08-29 20:49:03 +08:00
48 changed files with 2905 additions and 2020 deletions

View File

@@ -34,7 +34,12 @@ tsconfig.json
/ecosystem.config.js
/extra/healthcheck.exe
/extra/healthcheck
extra/exe-builder
/extra/exe-builder
/extra/push-examples
/extra/uptime-kuma-push
# Comment the following line if you want to rebuild the healthcheck binary
/extra/healthcheck-armv7
### .gitignore content (commented rules are duplicated)

View File

@@ -78,7 +78,7 @@ module.exports = {
"checkLoops": false,
}],
"space-before-blocks": "warn",
//'no-console': 'warn',
//"no-console": "warn",
"no-extra-boolean-cast": "off",
"no-multiple-empty-lines": [ "warn", {
"max": 1,
@@ -90,7 +90,8 @@ module.exports = {
"no-unneeded-ternary": "error",
"array-bracket-newline": [ "error", "consistent" ],
"eol-last": [ "error", "always" ],
//'prefer-template': 'error',
//"prefer-template": "error",
"template-curly-spacing": [ "warn", "never" ],
"comma-dangle": [ "warn", "only-multiline" ],
"no-empty": [ "error", {
"allowEmptyCatch": true

View File

@@ -5,11 +5,11 @@ name: Auto Test
on:
push:
branches: [ master ]
branches: [ master, 1.23.X ]
paths-ignore:
- '*.md'
pull_request:
branches: [ master, 2.0.X ]
branches: [ master, 1.23.X ]
paths-ignore:
- '*.md'
@@ -27,13 +27,13 @@ jobs:
steps:
- run: git config --global core.autocrlf false # Mainly for Windows
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Use Node.js ${{ matrix.node }}
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
- run: npm install npm@latest -g
- run: npm install npm@9 -g
- run: npm install
- run: npm run build
- run: npm test
@@ -55,13 +55,13 @@ jobs:
steps:
- run: git config --global core.autocrlf false # Mainly for Windows
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Use Node.js ${{ matrix.node }}
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
- run: npm install npm@latest -g
- run: npm install npm@9 -g
- run: npm ci --production
check-linters:
@@ -69,24 +69,24 @@ jobs:
steps:
- run: git config --global core.autocrlf false # Mainly for Windows
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Use Node.js 14
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: 14
- run: npm install
- run: npm run lint
- run: npm run lint:prod
e2e-tests:
needs: [ check-linters ]
runs-on: ubuntu-latest
steps:
- run: git config --global core.autocrlf false # Mainly for Windows
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Use Node.js 14
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: 14
- run: npm install
@@ -98,10 +98,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- run: git config --global core.autocrlf false # Mainly for Windows
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Use Node.js 14
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: 14
- run: npm install

View File

@@ -14,10 +14,10 @@ jobs:
node-version: [16]
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'

View File

@@ -6,7 +6,7 @@ on:
pull_request:
branches:
- master
- 2.0.X
- 1.23.X
workflow_dispatch:
permissions:
@@ -17,11 +17,11 @@ jobs:
json-yaml-validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: json-yaml-validate
id: json-yaml-validate
uses: GrantBirki/json-yaml-validate@v1.3.0
uses: GrantBirki/json-yaml-validate@v2.4.0
with:
comment: "true" # enable comment mode
exclude_file: ".github/config/exclude.txt" # gitignore style file for exclusions

View File

@@ -9,7 +9,7 @@ jobs:
stale:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v7
- uses: actions/stale@v8
with:
stale-issue-message: 'We are clearing up our old issues and your ticket has been open for 3 months with no activity. Remove stale label or comment or this will be closed in 2 days.'
close-issue-message: 'This issue was closed because it has been stalled for 2 days with no activity.'

View File

@@ -93,7 +93,7 @@ pm2 save && pm2 startup
### Windows Portable (x64)
https://github.com/louislam/uptime-kuma/files/11886108/uptime-kuma-win64-portable-1.0.1.zip
https://github.com/louislam/uptime-kuma/releases/download/1.23.1/uptime-kuma-windows-x64-portable-1.23.1.zip
### Advanced Installation

View File

@@ -3,7 +3,6 @@ import vue from "@vitejs/plugin-vue";
import { defineConfig } from "vite";
import visualizer from "rollup-plugin-visualizer";
import viteCompression from "vite-plugin-compression";
import commonjs from "vite-plugin-commonjs";
const postCssScss = require("postcss-scss");
const postcssRTLCSS = require("postcss-rtlcss");
@@ -22,7 +21,6 @@ export default defineConfig({
"CODESPACE_NAME": JSON.stringify(process.env.CODESPACE_NAME),
},
plugins: [
commonjs(),
vue(),
legacy({
targets: [ "since 2015" ],

View File

@@ -0,0 +1,34 @@
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
BEGIN TRANSACTION;
-- Rename COLUMNs to another one (suffixed by `_old`)
ALTER TABLE monitor
RENAME COLUMN kafka_producer_ssl TO kafka_producer_ssl_old;
ALTER TABLE monitor
RENAME COLUMN kafka_producer_allow_auto_topic_creation TO kafka_producer_allow_auto_topic_creation_old;
-- Add correct COLUMNs
ALTER TABLE monitor
ADD COLUMN kafka_producer_ssl BOOLEAN default 0 NOT NULL;
ALTER TABLE monitor
ADD COLUMN kafka_producer_allow_auto_topic_creation BOOLEAN default 0 NOT NULL;
-- These SQL is still not fully safe. See https://github.com/louislam/uptime-kuma/issues/4039.
-- Set bring old values from `_old` COLUMNs to correct ones
-- UPDATE monitor SET kafka_producer_allow_auto_topic_creation = monitor.kafka_producer_allow_auto_topic_creation_old
-- WHERE monitor.kafka_producer_allow_auto_topic_creation_old IS NOT NULL;
-- UPDATE monitor SET kafka_producer_ssl = monitor.kafka_producer_ssl_old
-- WHERE monitor.kafka_producer_ssl_old IS NOT NULL;
-- Remove old COLUMNs
ALTER TABLE monitor
DROP COLUMN kafka_producer_allow_auto_topic_creation_old;
ALTER TABLE monitor
DROP COLUMN kafka_producer_ssl_old;
COMMIT;

View 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;
-- SQLite: Change the data type of the column "config" from VARCHAR to TEXT
ALTER TABLE notification RENAME COLUMN config TO config_old;
ALTER TABLE notification ADD COLUMN config TEXT;
UPDATE notification SET config = config_old;
ALTER TABLE notification DROP COLUMN config_old;
COMMIT;

7
db/patch-timeout.sql Normal file
View 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;
UPDATE monitor SET timeout = (interval * 0.8)
WHERE timeout IS NULL OR timeout <= 0;
COMMIT;

View File

@@ -1,6 +1,6 @@
# DON'T UPDATE TO node:14-bullseye-slim, see #372.
# If the image changed, the second stage image should be changed too
FROM node:16-buster-slim
# DON'T UPDATE TO bullseye-slim, see #372.
# There is no 20-buster-slim for armv7 unfortunately, 18-buster-slim is the last one for Uptime Kuma v1.
FROM node:18-buster-slim
ARG TARGETPLATFORM
WORKDIR /app
@@ -27,7 +27,7 @@ RUN apt-get update && \
ca-certificates \
sudo \
nscd && \
pip3 --no-cache-dir install apprise==1.4.5 && \
pip3 --no-cache-dir install apprise==1.6.0 && \
rm -rf /var/lib/apt/lists/* && \
apt --yes autoremove

View File

@@ -6,7 +6,7 @@
* ⚠️ Deprecated: Changed to healthcheck.go, it will be deleted in the future.
* This script should be run after a period of time (180s), because the server may need some time to prepare.
*/
const { FBSD } = require("../server/util-server");
const FBSD = /^freebsd/.test(process.platform);
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";

View File

@@ -0,0 +1,44 @@
// Generate on GitHub
const input = `
* Add Korean translation by @Alanimdeo in https://github.com/louislam/dockge/pull/86
`;
const template = `
### 🆕 New Features
### 💇‍♀️ Improvements
### 🐞 Bug Fixes
### ⬆️ Security Fixes
### 🦎 Translation Contributions
### Others
- Other small changes, code refactoring and comment/doc updates in this repo:
`;
const lines = input.split("\n").filter((line) => line.trim() !== "");
for (const line of lines) {
// Split the last " by "
const usernamePullRequesURL = line.split(" by ").pop();
if (!usernamePullRequesURL) {
console.log("Unable to parse", line);
continue;
}
const [ username, pullRequestURL ] = usernamePullRequesURL.split(" in ");
const pullRequestID = "#" + pullRequestURL.split("/").pop();
let message = line.split(" by ").shift();
if (!message) {
console.log("Unable to parse", line);
continue;
}
message = message.split("* ").pop();
console.log("-", pullRequestID, message, `(Thanks ${username})`);
}
console.log(template);

View File

@@ -5,6 +5,8 @@ const { R } = require("redbean-node");
const readline = require("readline");
const { initJWTSecret } = require("../server/util-server");
const User = require("../server/model/user");
const { io } = require("socket.io-client");
const { localWebSocketURL } = require("../server/config");
const args = require("args-parser")(process.argv);
const rl = readline.createInterface({
input: process.stdin,
@@ -36,12 +38,16 @@ const main = async () => {
// Reset all sessions by reset jwt secret
await initJWTSecret();
// Disconnect all other socket clients of the user
await disconnectAllSocketClients(user.username, password);
break;
} else {
console.log("Passwords do not match, please try again.");
}
}
console.log("Password reset successfully.");
}
} catch (e) {
console.error("Error: " + e.message);
@@ -66,6 +72,45 @@ function question(question) {
});
}
function disconnectAllSocketClients(username, password) {
return new Promise((resolve) => {
console.log("Connecting to " + localWebSocketURL + " to disconnect all other socket clients");
// Disconnect all socket connections
const socket = io(localWebSocketURL, {
transports: [ "websocket" ],
reconnection: false,
timeout: 5000,
});
socket.on("connect", () => {
socket.emit("login", {
username,
password,
}, (res) => {
if (res.ok) {
console.log("Logged in.");
socket.emit("disconnectOtherSocketClients");
} else {
console.warn("Login failed.");
console.warn("Please restart the server to disconnect all sessions.");
}
socket.close();
});
});
socket.on("connect_error", function () {
// The localWebSocketURL is not guaranteed to be working for some complicated Uptime Kuma setup
// Ask the user to restart the server manually
console.warn("Failed to connect to " + localWebSocketURL);
console.warn("Please restart the server to disconnect all sessions manually.");
resolve();
});
socket.on("disconnect", () => {
resolve();
});
});
}
if (!process.env.TEST_BACKEND) {
main();
}

3870
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "uptime-kuma",
"version": "1.23.1",
"version": "1.23.9",
"license": "MIT",
"repository": {
"type": "git",
@@ -13,10 +13,12 @@
"install-legacy": "npm install",
"update-legacy": "npm update",
"lint:js": "eslint --ext \".js,.vue\" --ignore-path .gitignore .",
"lint:js-prod": "npm run lint:js -- --max-warnings 0",
"lint-fix:js": "eslint --ext \".js,.vue\" --fix --ignore-path .gitignore .",
"lint:style": "stylelint \"**/*.{vue,css,scss}\" --ignore-path .gitignore",
"lint-fix:style": "stylelint \"**/*.{vue,css,scss}\" --fix --ignore-path .gitignore",
"lint": "npm run lint:js && npm run lint:style",
"lint:prod": "npm run lint:js-prod && npm run lint:style",
"dev": "concurrently -k -r \"wait-on tcp:3000 && npm run start-server-dev \" \"npm run start-frontend-dev\"",
"start-frontend-dev": "cross-env NODE_ENV=development vite --host --config ./config/vite.config.js",
"start-frontend-devcontainer": "cross-env NODE_ENV=development DEVCONTAINER=1 vite --host --config ./config/vite.config.js",
@@ -40,7 +42,7 @@
"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.23.1 && npm ci --production && npm run download-dist",
"setup": "git checkout 1.23.9 && 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",
@@ -56,6 +58,9 @@
"test-install-script-ubuntu1604": "npm run compile-install-script && docker build --progress plain -f test/test_install_script/ubuntu1604.dockerfile .",
"simple-dns-server": "node extra/simple-dns-server.js",
"simple-mqtt-server": "node extra/simple-mqtt-server.js",
"simple-mongo": "docker run --rm -p 27017:27017 mongo",
"simple-postgres": "docker run --rm -p 5432:5432 -e POSTGRES_PASSWORD=postgres postgres",
"simple-mariadb": "docker run --rm -p 3306:3306 -e MYSQL_ROOT_PASSWORD=mariadb# mariadb",
"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/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",
@@ -69,7 +74,8 @@
"cypress-open": "concurrently -k -r \"node test/prepare-test-server.js && node server/server.js --port=3002 --data-dir=./data/test/\" \"cypress open --config-file ./config/cypress.config.js\"",
"build-healthcheck-armv7": "cross-env GOOS=linux GOARCH=arm GOARM=7 go build -x -o ./extra/healthcheck-armv7 ./extra/healthcheck.go",
"deploy-demo-server": "node extra/deploy-demo-server.js",
"sort-contributors": "node extra/sort-contributors.js"
"sort-contributors": "node extra/sort-contributors.js",
"start-server-node14-win": "private\\node14\\node.exe server/server.js"
},
"dependencies": {
"@grpc/grpc-js": "~1.7.3",
@@ -95,7 +101,8 @@
"express-basic-auth": "~1.2.1",
"express-static-gzip": "~2.1.7",
"form-data": "~4.0.0",
"gamedig": "~4.0.5",
"gamedig": "^4.2.0",
"html-escaper": "^3.0.3",
"http-graceful-shutdown": "~3.1.7",
"http-proxy-agent": "~5.0.0",
"https-proxy-agent": "~5.0.1",
@@ -108,10 +115,10 @@
"kafkajs": "^2.2.4",
"limiter": "~2.1.0",
"liquidjs": "^10.7.0",
"mongodb": "~4.14.0",
"mongodb": "~4.17.1",
"mqtt": "~4.3.7",
"mssql": "~8.1.4",
"mysql2": "~2.3.3",
"mysql2": "~3.6.2",
"nanoid": "~3.3.4",
"node-cloudflared-tunnel": "~1.0.9",
"node-radius-client": "~1.0.0",
@@ -120,11 +127,12 @@
"notp": "~2.0.3",
"openid-client": "^5.4.2",
"password-hash": "~1.2.2",
"pg": "~8.8.0",
"pg-connection-string": "~2.5.0",
"pg": "~8.11.3",
"pg-connection-string": "~2.6.2",
"playwright-core": "~1.35.1",
"prom-client": "~13.2.0",
"prometheus-api-metrics": "~3.2.1",
"promisify-child-process": "~4.1.2",
"protobufjs": "~7.2.4",
"qs": "~6.10.4",
"redbean-node": "~0.3.0",
@@ -160,7 +168,7 @@
"core-js": "~3.26.1",
"cronstrue": "~2.24.0",
"cross-env": "~7.0.3",
"cypress": "^12.17.0",
"cypress": "^13.2.0",
"delay": "^5.0.0",
"dns2": "~2.0.1",
"dompurify": "~2.4.3",
@@ -184,7 +192,6 @@
"typescript": "~4.4.4",
"v-pagination-3": "~0.1.7",
"vite": "~4.4.1",
"vite-plugin-commonjs": "^0.8.0",
"vite-plugin-compression": "^0.5.1",
"vue": "~3.3.4",
"vue-chartjs": "~5.2.0",
@@ -198,7 +205,7 @@
"vue-router": "~4.0.14",
"vue-toastification": "~2.0.0-rc.5",
"vuedraggable": "~4.1.0",
"wait-on": "^6.0.1",
"wait-on": "^7.2.0",
"whatwg-url": "~12.0.1"
}
}

View File

@@ -1,10 +1,9 @@
<svg width="640" height="640" viewBox="0 0 640 640" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M490.4 235.64C544.09 358.38 544.09 435.34 490.4 466.5C409.85 513.24 199.96 527.49 139.54 455.64C99.2601 407.74 99.2601 334.4 139.54 235.64C180.5 168.18 238.71 134.45 314.17 134.45C389.64 134.45 448.38 168.18 490.4 235.64Z" fill="url(#paint0_linear_381_799)"/>
<path d="M490.4 235.64C544.09 358.38 544.09 435.34 490.4 466.5C409.85 513.24 199.96 527.49 139.54 455.64C99.2601 407.74 99.2601 334.4 139.54 235.64C180.5 168.18 238.71 134.45 314.17 134.45C389.64 134.45 448.38 168.18 490.4 235.64Z" stroke="#F2F2F2" stroke-opacity="0.51" stroke-width="200"/>
<defs>
<linearGradient id="paint0_linear_381_799" x1="259.78" y1="261.15" x2="463.85" y2="456.49" gradientUnits="userSpaceOnUse">
<svg width="640" height="640" viewBox="0 0 640 640" xml:space="preserve" xmlns="http://www.w3.org/2000/svg">
<g transform="matrix(1 0 0 1 320 320)">
<linearGradient id="S3" gradientUnits="userSpaceOnUse" gradientTransform="matrix(1 0 0 1 -319.99875 -320.0001577393)" x1="259.78" y1="261.15" x2="463.85" y2="456.49">
<stop stop-color="#5CDD8B"/>
<stop offset="1" stop-color="#86E6A9"/>
</linearGradient>
</defs>
<path style="stroke: rgb(242,242,242); stroke-opacity: 0.51; stroke-width: 200; stroke-dasharray: none; stroke-linecap: butt; stroke-dashoffset: 0; stroke-linejoin: miter; stroke-miterlimit: 4; fill: url(#S3); fill-rule: nonzero; opacity: 1;" transform=" translate(0, 0)" d="M 170.40125 -84.36016 C 224.09125 38.37984 224.09125 115.33984 170.40125 146.49984 C 89.85125000000001 193.23984000000002 -120.03875 207.48984000000002 -180.45875 135.63984 C -220.73875 87.73983999999999 -220.73875 14.399839999999998 -180.45875 -84.36016000000001 C -139.49875 -151.82016 -81.28875000000001 -185.55016 -5.828750000000014 -185.55016 C 69.64124999999999 -185.55016 128.38125 -151.82016000000002 170.40124999999998 -84.36016000000001 z" stroke-linecap="round" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 893 B

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -1,29 +1,42 @@
const isFreeBSD = /^freebsd/.test(process.platform);
// Interop with browser
const args = (typeof process !== "undefined") ? require("args-parser")(process.argv) : {};
const demoMode = args["demo"] || false;
const badgeConstants = {
naColor: "#999",
defaultUpColor: "#66c20a",
defaultWarnColor: "#eed202",
defaultDownColor: "#c2290a",
defaultPendingColor: "#f8a306",
defaultMaintenanceColor: "#1747f5",
defaultPingColor: "blue", // as defined by badge-maker / shields.io
defaultStyle: "flat",
defaultPingValueSuffix: "ms",
defaultPingLabelSuffix: "h",
defaultUptimeValueSuffix: "%",
defaultUptimeLabelSuffix: "h",
defaultCertExpValueSuffix: " days",
defaultCertExpLabelSuffix: "h",
// Values Come From Default Notification Times
defaultCertExpireWarnDays: "14",
defaultCertExpireDownDays: "7"
};
// 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 (::)
// Also read HOST if not FreeBSD, as HOST is a system environment variable in FreeBSD
let hostEnv = isFreeBSD ? null : process.env.HOST;
const hostname = args.host || process.env.UPTIME_KUMA_HOST || hostEnv;
const port = [ args.port, process.env.UPTIME_KUMA_PORT, process.env.PORT, 3001 ]
.map(portValue => parseInt(portValue))
.find(portValue => !isNaN(portValue));
const sslKey = args["ssl-key"] || process.env.UPTIME_KUMA_SSL_KEY || process.env.SSL_KEY || undefined;
const sslCert = args["ssl-cert"] || process.env.UPTIME_KUMA_SSL_CERT || process.env.SSL_CERT || undefined;
const sslKeyPassphrase = args["ssl-key-passphrase"] || process.env.UPTIME_KUMA_SSL_KEY_PASSPHRASE || process.env.SSL_KEY_PASSPHRASE || undefined;
const isSSL = sslKey && sslCert;
function getLocalWebSocketURL() {
const protocol = isSSL ? "wss" : "ws";
const host = hostname || "localhost";
return `${protocol}://${host}:${port}`;
}
const localWebSocketURL = getLocalWebSocketURL();
const demoMode = args["demo"] || false;
module.exports = {
args,
hostname,
port,
sslKey,
sslCert,
sslKeyPassphrase,
isSSL,
localWebSocketURL,
demoMode,
badgeConstants,
};

View File

@@ -81,6 +81,9 @@ class Database {
"patch-monitor-oauth-cc.sql": true,
"patch-add-timeout-monitor.sql": true,
"patch-add-gamedig-given-port.sql": true,
"patch-notification-config.sql": true,
"patch-fix-kafka-producer-booleans.sql": true,
"patch-timeout.sql": true,
};
/**

View File

@@ -1,4 +1,5 @@
const jsesc = require("jsesc");
const { escape } = require("html-escaper");
/**
* Returns a string that represents the javascript that is required to insert the Google Analytics scripts
@@ -7,15 +8,18 @@ const jsesc = require("jsesc");
* @returns {string}
*/
function getGoogleAnalyticsScript(tagId) {
let escapedTagId = jsesc(tagId, { isScriptContext: true });
let escapedTagIdJS = jsesc(tagId, { isScriptContext: true });
if (escapedTagId) {
escapedTagId = escapedTagId.trim();
if (escapedTagIdJS) {
escapedTagIdJS = escapedTagIdJS.trim();
}
// Escape the tag ID for use in an HTML attribute.
let escapedTagIdHTMLAttribute = escape(tagId);
return `
<script async src="https://www.googletagmanager.com/gtag/js?id=${escapedTagId}"></script>
<script>window.dataLayer = window.dataLayer || []; function gtag(){dataLayer.push(arguments);} gtag('js', new Date());gtag('config', '${escapedTagId}'); </script>
<script async src="https://www.googletagmanager.com/gtag/js?id=${escapedTagIdHTMLAttribute}"></script>
<script>window.dataLayer = window.dataLayer || []; function gtag(){dataLayer.push(arguments);} gtag('js', new Date());gtag('config', '${escapedTagIdJS}'); </script>
`;
}

View File

@@ -6,7 +6,7 @@ const { log, UP, DOWN, PENDING, MAINTENANCE, flipStatus, TimeLogger, MAX_INTERVA
SQL_DATETIME_FORMAT
} = require("../../src/util");
const { tcping, ping, dnsResolve, checkCertificate, checkStatusCode, getTotalClientInRoom, setting, mssqlQuery, postgresQuery, mysqlQuery, mqttAsync, setSetting, httpNtlm, radius, grpcQuery,
redisPingAsync, mongodbPing, kafkaProducerAsync, getOidcTokenClientCredentials,
redisPingAsync, mongodbPing, kafkaProducerAsync, getOidcTokenClientCredentials, rootCertificatesFingerprints, axiosAbortSignal
} = require("../util-server");
const { R } = require("redbean-node");
const { BeanModel } = require("redbean-node/dist/bean-model");
@@ -22,6 +22,9 @@ const { UptimeCacheList } = require("../uptime-cache-list");
const Gamedig = require("gamedig");
const jsonata = require("jsonata");
const jwt = require("jsonwebtoken");
const crypto = require("crypto");
const rootCertificates = rootCertificatesFingerprints();
/**
* status:
@@ -53,7 +56,7 @@ class Monitor extends BeanModel {
obj.tags = await this.getTags();
}
if (certExpiry && this.type === "http") {
if (certExpiry && (this.type === "http" || this.type === "keyword" || this.type === "json-query") && this.getURLProtocol() === "https:") {
const { certExpiryDaysRemaining, validCert } = await this.getCertExpiry(this.id);
obj.certExpiryDaysRemaining = certExpiryDaysRemaining;
obj.validCert = validCert;
@@ -141,8 +144,8 @@ class Monitor extends BeanModel {
expectedValue: this.expectedValue,
kafkaProducerTopic: this.kafkaProducerTopic,
kafkaProducerBrokers: JSON.parse(this.kafkaProducerBrokers),
kafkaProducerSsl: this.kafkaProducerSsl === "1" && true || false,
kafkaProducerAllowAutoTopicCreation: this.kafkaProducerAllowAutoTopicCreation === "1" && true || false,
kafkaProducerSsl: this.getKafkaProducerSsl(),
kafkaProducerAllowAutoTopicCreation: this.getKafkaProducerAllowAutoTopicCreation(),
kafkaProducerMessage: this.kafkaProducerMessage,
screenshot,
};
@@ -285,6 +288,22 @@ class Monitor extends BeanModel {
return Boolean(this.gamedigGivenPortOnly);
}
/**
* Parse to boolean
* @returns {boolean} Kafka Producer Ssl enabled?
*/
getKafkaProducerSsl() {
return Boolean(this.kafkaProducerSsl);
}
/**
* Parse to boolean
* @returns {boolean} Kafka Producer Allow Auto Topic Creation Enabled?
*/
getKafkaProducerAllowAutoTopicCreation() {
return Boolean(this.kafkaProducerAllowAutoTopicCreation);
}
/**
* Start monitor
* @param {Server} io Socket server instance
@@ -339,6 +358,12 @@ class Monitor extends BeanModel {
bean.duration = 0;
}
// Runtime patch timeout if it is 0
// See https://github.com/louislam/uptime-kuma/pull/3961#issuecomment-1804149144
if (!this.timeout || this.timeout <= 0) {
this.timeout = this.interval * 1000 * 0.8;
}
try {
if (await Monitor.isUnderMaintenance(this.id)) {
bean.msg = "Monitor under maintenance";
@@ -409,6 +434,7 @@ class Monitor extends BeanModel {
const httpsAgentOptions = {
maxCachedSessions: 0, // Use Custom agent to disable session reuse (https://github.com/nodejs/node/issues/3940)
rejectUnauthorized: !this.getIgnoreTls(),
secureOptions: crypto.constants.SSL_OP_LEGACY_SERVER_CONNECT,
};
log.debug("monitor", `[${this.name}] Prepare Options for axios`);
@@ -447,6 +473,7 @@ class Monitor extends BeanModel {
validateStatus: (status) => {
return checkStatusCode(status, this.getAcceptedStatuscodes());
},
signal: axiosAbortSignal((this.timeout + 10) * 1000),
};
if (bodyValue) {
@@ -662,6 +689,7 @@ class Monitor extends BeanModel {
httpsAgent: CacheableDnsHttpAgent.getHttpsAgent({
maxCachedSessions: 0, // Use Custom agent to disable session reuse (https://github.com/nodejs/node/issues/3940)
rejectUnauthorized: !this.getIgnoreTls(),
secureOptions: crypto.constants.SSL_OP_LEGACY_SERVER_CONNECT,
}),
httpAgent: CacheableDnsHttpAgent.getHttpAgent({
maxCachedSessions: 0,
@@ -704,8 +732,6 @@ class Monitor extends BeanModel {
} else if (this.type === "docker") {
log.debug("monitor", `[${this.name}] Prepare Options for Axios`);
const dockerHost = await R.load("docker_host", this.docker_host);
const options = {
url: `/containers/${this.docker_container}/json`,
timeout: this.interval * 1000 * 0.8,
@@ -716,12 +742,19 @@ class Monitor extends BeanModel {
httpsAgent: CacheableDnsHttpAgent.getHttpsAgent({
maxCachedSessions: 0, // Use Custom agent to disable session reuse (https://github.com/nodejs/node/issues/3940)
rejectUnauthorized: !this.getIgnoreTls(),
secureOptions: crypto.constants.SSL_OP_LEGACY_SERVER_CONNECT,
}),
httpAgent: CacheableDnsHttpAgent.getHttpAgent({
maxCachedSessions: 0,
}),
};
const dockerHost = await R.load("docker_host", this.docker_host);
if (!dockerHost) {
throw new Error("Failed to load docker host config");
}
if (dockerHost._dockerType === "socket") {
options.socketPath = dockerHost._dockerDaemon;
} else if (dockerHost._dockerType === "tcp") {
@@ -756,7 +789,7 @@ class Monitor extends BeanModel {
} else if (this.type === "sqlserver") {
let startTime = dayjs().valueOf();
await mssqlQuery(this.databaseConnectionString, this.databaseQuery);
await mssqlQuery(this.databaseConnectionString, this.databaseQuery || "SELECT 1");
bean.msg = "";
bean.status = UP;
@@ -795,7 +828,7 @@ class Monitor extends BeanModel {
} else if (this.type === "postgres") {
let startTime = dayjs().valueOf();
await postgresQuery(this.databaseConnectionString, this.databaseQuery);
await postgresQuery(this.databaseConnectionString, this.databaseQuery || "SELECT 1");
bean.msg = "";
bean.status = UP;
@@ -803,7 +836,11 @@ class Monitor extends BeanModel {
} else if (this.type === "mysql") {
let startTime = dayjs().valueOf();
bean.msg = await mysqlQuery(this.databaseConnectionString, this.databaseQuery);
// Use `radius_password` as `password` field, since there are too many unnecessary fields
// TODO: rename `radius_password` to `password` later for general use
let mysqlPassword = this.radiusPassword;
bean.msg = await mysqlQuery(this.databaseConnectionString, this.databaseQuery || "SELECT 1", mysqlPassword);
bean.status = UP;
bean.ping = dayjs().valueOf() - startTime;
} else if (this.type === "mongodb") {
@@ -891,7 +928,11 @@ class Monitor extends BeanModel {
} catch (error) {
bean.msg = error.message;
if (error?.name === "CanceledError") {
bean.msg = `timeout by AbortSignal (${this.timeout}s)`;
} else {
bean.msg = error.message;
}
// If UP come in here, it must be upside down mode
// Just reset the retries
@@ -1075,6 +1116,19 @@ class Monitor extends BeanModel {
}
}
/**
* Example: http: or https:
* @returns {(null|string)}
*/
getURLProtocol() {
const url = this.getUrl();
if (url) {
return this.getUrl().protocol;
} else {
return null;
}
}
/**
* Store TLS info to database
* @param checkCertificateResult
@@ -1411,7 +1465,10 @@ class Monitor extends BeanModel {
let certInfo = tlsInfoObject.certInfo;
while (certInfo) {
let subjectCN = certInfo.subject["CN"];
if (certInfo.daysRemaining > targetDays) {
if (rootCertificates.has(certInfo.fingerprint256)) {
log.debug("monitor", `Known root cert: ${certInfo.certType} certificate "${subjectCN}" (${certInfo.daysRemaining} days valid) on ${targetDays} deadline.`);
break;
} else if (certInfo.daysRemaining > targetDays) {
log.debug("monitor", `No need to send cert notification for ${certInfo.certType} certificate "${subjectCN}" (${certInfo.daysRemaining} days valid) on ${targetDays} deadline.`);
} else {
log.debug("monitor", `call sendCertNotificationByTargetDays for ${targetDays} deadline on certificate ${subjectCN}.`);

View File

@@ -1,6 +1,8 @@
const { BeanModel } = require("redbean-node/dist/bean-model");
const passwordHash = require("../password-hash");
const { R } = require("redbean-node");
const jwt = require("jsonwebtoken");
const { shake256, SHAKE256_LENGTH } = require("../util-server");
class User extends BeanModel {
/**
@@ -27,6 +29,19 @@ class User extends BeanModel {
this.password = newPassword;
}
/**
* Create a new JWT for a user
* @param {User} user
* @param {string} jwtSecret
* @return {string}
*/
static createJWT(user, jwtSecret) {
return jwt.sign({
username: user.username,
h: shake256(user.password, SHAKE256_LENGTH),
}, jwtSecret);
}
}
module.exports = User;

View File

@@ -40,6 +40,7 @@ if (process.platform === "win32") {
"/usr/bin/chromium",
"/usr/bin/chromium-browser",
"/usr/bin/google-chrome",
"/snap/bin/chromium", // Ubuntu
];
} else if (process.platform === "darwin") {
// TODO: Generated by GitHub Copilot, but not sure if it's correct

View File

@@ -1,6 +1,6 @@
const { MonitorType } = require("./monitor-type");
const { UP, log } = require("../../src/util");
const exec = require("child_process").exec;
const { UP } = require("../../src/util");
const childProcessAsync = require("promisify-child-process");
/**
* A TailscalePing class extends the MonitorType.
@@ -23,7 +23,6 @@ class TailscalePing extends MonitorType {
let tailscaleOutput = await this.runTailscalePing(monitor.hostname, monitor.interval);
this.parseTailscaleOutput(tailscaleOutput, heartbeat);
} catch (err) {
log.debug("Tailscale", err);
// trigger log function somewhere to display a notification or alert to the user (but how?)
throw new Error(`Error checking Tailscale ping: ${err}`);
}
@@ -33,30 +32,23 @@ class TailscalePing extends MonitorType {
* Runs the Tailscale ping command to the given URL.
*
* @param {string} hostname - The hostname to ping.
* @param {number} interval
* @returns {Promise<string>} - A Promise that resolves to the output of the Tailscale ping command
* @throws Will throw an error if the command execution encounters any error.
*/
async runTailscalePing(hostname, interval) {
let cmd = `tailscale ping ${hostname}`;
log.debug("Tailscale", cmd);
return new Promise((resolve, reject) => {
let timeout = interval * 1000 * 0.8;
exec(cmd, { timeout: timeout }, (error, stdout, stderr) => {
// we may need to handle more cases if tailscale reports an error that isn't necessarily an error (such as not-logged in or DERP health-related issues)
if (error) {
reject(`Execution error: ${error.message}`);
return;
}
if (stderr) {
reject(`Error in output: ${stderr}`);
return;
}
resolve(stdout);
});
let timeout = interval * 1000 * 0.8;
let res = await childProcessAsync.spawn("tailscale", [ "ping", "--c", "1", hostname ], {
timeout: timeout
});
if (res.stderr && res.stderr.toString()) {
throw new Error(`Error in output: ${res.stderr.toString()}`);
}
if (res.stdout && res.stdout.toString()) {
return res.stdout.toString();
} else {
throw new Error("No output from Tailscale ping");
}
}
/**
@@ -74,7 +66,7 @@ class TailscalePing extends MonitorType {
heartbeat.status = UP;
let time = line.split(" in ")[1].split(" ")[0];
heartbeat.ping = parseInt(time);
heartbeat.msg = line;
heartbeat.msg = "OK";
break;
} else if (line.includes("timed out")) {
throw new Error(`Ping timed out: "${line}"`);

View File

@@ -1,5 +1,5 @@
const NotificationProvider = require("./notification-provider");
const childProcess = require("child_process");
const childProcessAsync = require("promisify-child-process");
class Apprise extends NotificationProvider {
@@ -11,7 +11,7 @@ class Apprise extends NotificationProvider {
args.push("-t");
args.push(notification.title);
}
const s = childProcess.spawnSync("apprise", args);
const s = await childProcessAsync.spawn("apprise", args);
const output = (s.stdout) ? s.stdout.toString() : "ERROR: maybe apprise not found";

View File

@@ -1,15 +1,21 @@
let express = require("express");
const { allowDevAllOrigin, allowAllOrigin, percentageToColor, filterAndJoin, sendHttpError } = require("../util-server");
const {
setting,
allowDevAllOrigin,
allowAllOrigin,
percentageToColor,
filterAndJoin,
sendHttpError,
} = require("../util-server");
const { R } = require("redbean-node");
const apicache = require("../modules/apicache");
const Monitor = require("../model/monitor");
const dayjs = require("dayjs");
const { UP, MAINTENANCE, DOWN, PENDING, flipStatus, log } = require("../../src/util");
const { UP, MAINTENANCE, DOWN, PENDING, flipStatus, log, badgeConstants } = require("../../src/util");
const StatusPage = require("../model/status_page");
const { UptimeKumaServer } = require("../uptime-kuma-server");
const { UptimeCacheList } = require("../uptime-cache-list");
const { makeBadge } = require("badge-maker");
const { badgeConstants } = require("../config");
const { Prometheus } = require("../prometheus");
let router = express.Router();
@@ -22,10 +28,14 @@ router.get("/api/entry-page", async (request, response) => {
allowDevAllOrigin(response);
let result = { };
let hostname = request.hostname;
if ((await setting("trustProxy")) && request.headers["x-forwarded-host"]) {
hostname = request.headers["x-forwarded-host"];
}
if (request.hostname in StatusPage.domainMappingList) {
if (hostname in StatusPage.domainMappingList) {
result.type = "statusPageMatchedDomain";
result.statusPageSlug = StatusPage.domainMappingList[request.hostname];
result.statusPageSlug = StatusPage.domainMappingList[hostname];
} else {
result.type = "entryPage";
result.entryPage = server.entryPage;
@@ -38,7 +48,7 @@ router.get("/api/push/:pushToken", async (request, response) => {
let pushToken = request.params.pushToken;
let msg = request.query.msg || "OK";
let ping = parseInt(request.query.ping) || null;
let ping = parseFloat(request.query.ping) || null;
let statusString = request.query.status || "up";
let status = (statusString === "up") ? UP : DOWN;

View File

@@ -5,7 +5,7 @@ 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 { badgeConstants } = require("../../src/util");
const { makeBadge } = require("badge-maker");
let router = express.Router();

View File

@@ -48,8 +48,13 @@ if (! process.env.NODE_ENV) {
process.env.NODE_ENV = "production";
}
if (!process.env.UPTIME_KUMA_WS_ORIGIN_CHECK) {
process.env.UPTIME_KUMA_WS_ORIGIN_CHECK = "cors-like";
}
log.info("server", "Node Env: " + process.env.NODE_ENV);
log.info("server", "Inside Container: " + (process.env.UPTIME_KUMA_IS_CONTAINER === "1"));
log.info("server", "WebSocket Origin Check: " + process.env.UPTIME_KUMA_WS_ORIGIN_CHECK);
log.info("server", "Importing Node libraries");
const fs = require("fs");
@@ -76,15 +81,18 @@ const notp = require("notp");
const base32 = require("thirty-two");
const { UptimeKumaServer } = require("./uptime-kuma-server");
const server = UptimeKumaServer.getInstance(args);
const server = UptimeKumaServer.getInstance();
const io = module.exports.io = server.io;
const app = server.app;
log.info("server", "Importing this project modules");
log.debug("server", "Importing Monitor");
const Monitor = require("./model/monitor");
const User = require("./model/user");
log.debug("server", "Importing Settings");
const { getSettings, setSettings, setting, initJWTSecret, checkLogin, startUnitTest, FBSD, doubleCheckPassword, startE2eTests } = require("./util-server");
const { getSettings, setSettings, setting, initJWTSecret, checkLogin, startUnitTest, doubleCheckPassword, startE2eTests, shake256, SHAKE256_LENGTH
} = require("./util-server");
log.debug("server", "Importing Notification");
const { Notification } = require("./notification");
@@ -107,19 +115,13 @@ const passwordHash = require("./password-hash");
const checkVersion = require("./check-version");
log.info("server", "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 (::)
// Also read HOST if not FreeBSD, as HOST is a system environment variable in FreeBSD
let hostEnv = FBSD ? null : process.env.HOST;
let hostname = args.host || process.env.UPTIME_KUMA_HOST || hostEnv;
const hostname = config.hostname;
if (hostname) {
log.info("server", "Custom hostname: " + hostname);
}
const port = [ args.port, process.env.UPTIME_KUMA_PORT, process.env.PORT, 3001 ]
.map(portValue => parseInt(portValue))
.find(portValue => !isNaN(portValue));
const port = config.port;
const disableFrameSameOrigin = !!process.env.UPTIME_KUMA_DISABLE_FRAME_SAMEORIGIN || args["disable-frame-sameorigin"] || false;
const cloudflaredToken = args["cloudflared-token"] || process.env.UPTIME_KUMA_CLOUDFLARED_TOKEN || undefined;
@@ -297,6 +299,11 @@ let needSetup = false;
]);
if (user) {
// Check if the password changed
if (decoded.h !== shake256(user.password, SHAKE256_LENGTH)) {
throw new Error("The token is invalid due to password change or old token");
}
log.debug("auth", "afterLogin");
afterLogin(socket, user);
log.debug("auth", "afterLogin ok");
@@ -316,9 +323,10 @@ let needSetup = false;
});
}
} catch (error) {
log.error("auth", `Invalid token. IP=${clientIP}`);
if (error.message) {
log.error("auth", error.message, `IP=${clientIP}`);
}
callback({
ok: false,
msg: "Invalid token.",
@@ -357,9 +365,7 @@ let needSetup = false;
callback({
ok: true,
token: jwt.sign({
username: data.username,
}, server.jwtSecret),
token: User.createJWT(user, server.jwtSecret),
});
}
@@ -387,9 +393,7 @@ let needSetup = false;
callback({
ok: true,
token: jwt.sign({
username: data.username,
}, server.jwtSecret),
token: User.createJWT(user, server.jwtSecret),
});
} else {
@@ -726,11 +730,11 @@ let needSetup = false;
bean.basic_auth_user = monitor.basic_auth_user;
bean.basic_auth_pass = monitor.basic_auth_pass;
bean.timeout = monitor.timeout;
bean.oauth_client_id = monitor.oauth_client_id,
bean.oauth_client_secret = monitor.oauth_client_secret,
bean.oauth_auth_method = this.oauth_auth_method,
bean.oauth_token_url = monitor.oauth_token_url,
bean.oauth_scopes = monitor.oauth_scopes,
bean.oauth_client_id = monitor.oauth_client_id;
bean.oauth_client_secret = monitor.oauth_client_secret;
bean.oauth_auth_method = monitor.oauth_auth_method;
bean.oauth_token_url = monitor.oauth_token_url;
bean.oauth_scopes = monitor.oauth_scopes;
bean.tlsCa = monitor.tlsCa;
bean.tlsCert = monitor.tlsCert;
bean.tlsKey = monitor.tlsKey;
@@ -784,6 +788,9 @@ let needSetup = false;
bean.kafkaProducerAllowAutoTopicCreation = monitor.kafkaProducerAllowAutoTopicCreation;
bean.kafkaProducerSaslOptions = JSON.stringify(monitor.kafkaProducerSaslOptions);
bean.kafkaProducerMessage = monitor.kafkaProducerMessage;
bean.kafkaProducerSsl = monitor.kafkaProducerSsl;
bean.kafkaProducerAllowAutoTopicCreation =
monitor.kafkaProducerAllowAutoTopicCreation;
bean.gamedigGivenPortOnly = monitor.gamedigGivenPortOnly;
bean.validate();
@@ -796,7 +803,7 @@ let needSetup = false;
await updateMonitorNotification(bean.id, monitor.notificationIDList);
if (bean.isActive()) {
if (await bean.isActive()) {
await restartMonitor(socket.userID, bean.id);
}
@@ -1144,6 +1151,8 @@ let needSetup = false;
let user = await doubleCheckPassword(socket, password.currentPassword);
await user.resetPassword(password.newPassword);
server.disconnectAllSocketClient(user.id, socket.id);
callback({
ok: true,
msg: "Password has been updated successfully.",
@@ -1215,9 +1224,9 @@ let needSetup = false;
// Update nscd status
if (previousNSCDStatus !== data.nscd) {
if (data.nscd) {
server.startNSCDServices();
await server.startNSCDServices();
} else {
server.stopNSCDServices();
await server.stopNSCDServices();
}
}
@@ -1824,6 +1833,7 @@ async function pauseMonitor(userID, monitorID) {
if (monitorID in server.monitorList) {
server.monitorList[monitorID].stop();
server.monitorList[monitorID].active = 0;
}
}
@@ -1882,8 +1892,10 @@ gracefulShutdown(server.httpServer, {
});
// Catch unexpected errors here
process.addListener("unhandledRejection", (error, promise) => {
let unexpectedErrorHandler = (error, promise) => {
console.trace(error);
UptimeKumaServer.errorLog(error, false);
console.error("If you keep encountering errors, please report to https://github.com/louislam/uptime-kuma/issues");
});
};
process.addListener("unhandledRejection", unexpectedErrorHandler);
process.addListener("uncaughtException", unexpectedErrorHandler);

View File

@@ -42,24 +42,50 @@ module.exports.generalSocketHandler = (socket, server) => {
});
socket.on("getGameList", async (callback) => {
callback({
ok: true,
gameList: getGameList(),
});
});
socket.on("testChrome", (executable, callback) => {
// Just noticed that await call could block the whole socket.io server!!! Use pure promise instead.
testChrome(executable).then((version) => {
try {
checkLogin(socket);
callback({
ok: true,
msg: "Found Chromium/Chrome. Version: " + version,
gameList: getGameList(),
});
}).catch((e) => {
} catch (e) {
callback({
ok: false,
msg: e.message,
});
});
}
});
socket.on("testChrome", (executable, callback) => {
try {
checkLogin(socket);
// Just noticed that await call could block the whole socket.io server!!! Use pure promise instead.
testChrome(executable).then((version) => {
callback({
ok: true,
msg: "Found Chromium/Chrome. Version: " + version,
});
}).catch((e) => {
callback({
ok: false,
msg: e.message,
});
});
} catch (e) {
callback({
ok: false,
msg: e.message,
});
}
});
// Disconnect all other socket clients of the user
socket.on("disconnectOtherSocketClients", async () => {
try {
checkLogin(socket);
server.disconnectAllSocketClients(socket.userID, socket.id);
} catch (e) {
log.warn("disconnectAllSocketClients", e.message);
}
});
};

View File

@@ -4,14 +4,15 @@ const fs = require("fs");
const http = require("http");
const { Server } = require("socket.io");
const { R } = require("redbean-node");
const { log } = require("../src/util");
const { log, isDev } = require("../src/util");
const Database = require("./database");
const util = require("util");
const { CacheableDnsHttpAgent } = require("./cacheable-dns-http-agent");
const { Settings } = require("./settings");
const dayjs = require("dayjs");
const childProcess = require("child_process");
const childProcessAsync = require("promisify-child-process");
const path = require("path");
const { isSSL, sslKey, sslCert, sslKeyPassphrase } = require("./config");
// DO NOT IMPORT HERE IF THE MODULES USED `UptimeKumaServer.getInstance()`, put at the bottom of this file instead.
/**
@@ -62,22 +63,17 @@ class UptimeKumaServer {
*/
jwtSecret = null;
static getInstance(args) {
static getInstance() {
if (UptimeKumaServer.instance == null) {
UptimeKumaServer.instance = new UptimeKumaServer(args);
UptimeKumaServer.instance = new UptimeKumaServer();
}
return UptimeKumaServer.instance;
}
constructor(args) {
// SSL
const sslKey = args["ssl-key"] || process.env.UPTIME_KUMA_SSL_KEY || process.env.SSL_KEY || undefined;
const sslCert = args["ssl-cert"] || process.env.UPTIME_KUMA_SSL_CERT || process.env.SSL_CERT || undefined;
const sslKeyPassphrase = args["ssl-key-passphrase"] || process.env.UPTIME_KUMA_SSL_KEY_PASSPHRASE || process.env.SSL_KEY_PASSPHRASE || undefined;
constructor() {
log.info("server", "Creating express and socket.io instance");
this.app = express();
if (sslKey && sslCert) {
if (isSSL) {
log.info("server", "Server Type: HTTPS");
this.httpServer = https.createServer({
key: fs.readFileSync(sslKey),
@@ -103,7 +99,41 @@ class UptimeKumaServer {
UptimeKumaServer.monitorTypeList["real-browser"] = new RealBrowserMonitorType();
UptimeKumaServer.monitorTypeList["tailscale-ping"] = new TailscalePing();
this.io = new Server(this.httpServer);
this.io = new Server(this.httpServer, {
allowRequest: (req, callback) => {
let isOriginValid = true;
const bypass = isDev || process.env.UPTIME_KUMA_WS_ORIGIN_CHECK === "bypass";
if (!bypass) {
let host = req.headers.host;
// If this is set, it means the request is from the browser
let origin = req.headers.origin;
// If this is from the browser, check if the origin is allowed
if (origin) {
try {
let originURL = new URL(origin);
if (host !== originURL.host) {
isOriginValid = false;
log.error("auth", `Origin (${origin}) does not match host (${host}), IP: ${req.socket.remoteAddress}`);
}
} catch (e) {
// Invalid origin url, probably not from browser
isOriginValid = false;
log.error("auth", `Invalid origin url (${origin}), IP: ${req.socket.remoteAddress}`);
}
} else {
log.info("auth", `Origin is not set, IP: ${req.socket.remoteAddress}`);
}
} else {
log.debug("auth", "Origin check is bypassed");
}
callback(null, isOriginValid);
}
});
}
/** Initialise app after the database has been set up */
@@ -344,7 +374,7 @@ class UptimeKumaServer {
let enable = await Settings.get("nscd");
if (enable || enable === null) {
this.startNSCDServices();
await this.startNSCDServices();
}
}
@@ -356,7 +386,7 @@ class UptimeKumaServer {
let enable = await Settings.get("nscd");
if (enable || enable === null) {
this.stopNSCDServices();
await this.stopNSCDServices();
}
}
@@ -364,11 +394,11 @@ class UptimeKumaServer {
* Start all system services (e.g. nscd)
* For now, only used in Docker
*/
startNSCDServices() {
async startNSCDServices() {
if (process.env.UPTIME_KUMA_IS_CONTAINER) {
try {
log.info("services", "Starting nscd");
childProcess.execSync("sudo service nscd start", { stdio: "pipe" });
await childProcessAsync.exec("sudo service nscd start");
} catch (e) {
log.info("services", "Failed to start nscd");
}
@@ -378,16 +408,35 @@ class UptimeKumaServer {
/**
* Stop all system services
*/
stopNSCDServices() {
async stopNSCDServices() {
if (process.env.UPTIME_KUMA_IS_CONTAINER) {
try {
log.info("services", "Stopping nscd");
childProcess.execSync("sudo service nscd stop");
await childProcessAsync.exec("sudo service nscd stop");
} catch (e) {
log.info("services", "Failed to stop nscd");
}
}
}
/**
* Force connected sockets of a user to refresh and disconnect.
* Used for resetting password.
* @param {string} userID
* @param {string?} currentSocketID
*/
disconnectAllSocketClients(userID, currentSocketID = undefined) {
for (const socket of this.io.sockets.sockets.values()) {
if (socket.userID === userID && socket.id !== currentSocketID) {
try {
socket.emit("refresh");
socket.disconnect();
} catch (e) {
}
}
}
}
}
module.exports = {

View File

@@ -1,7 +1,7 @@
const tcpp = require("tcp-ping");
const ping = require("@louislam/ping");
const { R } = require("redbean-node");
const { log, genSecret } = require("../src/util");
const { log, genSecret, badgeConstants } = require("../src/util");
const passwordHash = require("./password-hash");
const { Resolver } = require("dns");
const childProcess = require("child_process");
@@ -9,7 +9,6 @@ const iconv = require("iconv-lite");
const chardet = require("chardet");
const mqtt = require("mqtt");
const chroma = require("chroma-js");
const { badgeConstants } = require("./config");
const mssql = require("mssql");
const { Client } = require("pg");
const postgresConParse = require("pg-connection-string").parse;
@@ -22,6 +21,7 @@ const protojs = require("protobufjs");
const radiusClient = require("node-radius-client");
const redis = require("redis");
const oidc = require("openid-client");
const tls = require("tls");
const {
dictionaries: {
@@ -33,6 +33,7 @@ const dayjs = require("dayjs");
// SASLOptions used in JSDoc
// eslint-disable-next-line no-unused-vars
const { Kafka, SASLOptions } = require("kafkajs");
const crypto = require("crypto");
const isWindows = process.platform === /^win/.test(process.platform);
/**
@@ -286,22 +287,22 @@ exports.kafkaProducerAsync = function (brokers, topic, message, options = {}, sa
producer.connect().then(
() => {
try {
producer.send({
topic: topic,
messages: [{
value: message,
}],
});
connectedToKafka = true;
clearTimeout(timeoutID);
producer.send({
topic: topic,
messages: [{
value: message,
}],
}).then((_) => {
resolve("Message sent successfully");
} catch (e) {
}).catch((e) => {
connectedToKafka = true;
producer.disconnect();
clearTimeout(timeoutID);
reject(new Error("Error sending message: " + e.message));
}
}).finally(() => {
connectedToKafka = true;
clearTimeout(timeoutID);
});
}
).catch(
(e) => {
@@ -313,8 +314,10 @@ exports.kafkaProducerAsync = function (brokers, topic, message, options = {}, sa
);
producer.on("producer.network.request_timeout", (_) => {
clearTimeout(timeoutID);
reject(new Error("producer.network.request_timeout"));
if (!connectedToKafka) {
clearTimeout(timeoutID);
reject(new Error("producer.network.request_timeout"));
}
});
producer.on("producer.disconnect", (_) => {
@@ -392,6 +395,9 @@ exports.mssqlQuery = async function (connectionString, query) {
try {
pool = new mssql.ConnectionPool(connectionString);
await pool.connect();
if (!query) {
query = "SELECT 1";
}
await pool.request().query(query);
pool.close();
} catch (e) {
@@ -412,12 +418,22 @@ exports.postgresQuery = function (connectionString, query) {
return new Promise((resolve, reject) => {
const config = postgresConParse(connectionString);
if (config.password === "") {
// See https://github.com/brianc/node-postgres/issues/1927
return reject(new Error("Password is undefined."));
// Fix #3868, which true/false is not parsed to boolean
if (typeof config.ssl === "string") {
config.ssl = config.ssl === "true";
}
const client = new Client({ connectionString });
if (config.password === "") {
// See https://github.com/brianc/node-postgres/issues/1927
reject(new Error("Password is undefined."));
return;
}
const client = new Client(config);
client.on("error", (error) => {
log.debug("postgres", "Error caught in the error event handler.");
reject(error);
});
client.connect((err) => {
if (err) {
@@ -441,6 +457,7 @@ exports.postgresQuery = function (connectionString, query) {
});
} catch (e) {
reject(e);
client.end();
}
}
});
@@ -452,11 +469,15 @@ exports.postgresQuery = function (connectionString, query) {
* Run a query on MySQL/MariaDB
* @param {string} connectionString The database connection string
* @param {string} query The query to validate the database with
* @param {?string} password The password to use
* @returns {Promise<(string)>}
*/
exports.mysqlQuery = function (connectionString, query) {
exports.mysqlQuery = function (connectionString, query, password = undefined) {
return new Promise((resolve, reject) => {
const connection = mysql.createConnection(connectionString);
const connection = mysql.createConnection({
uri: connectionString,
password
});
connection.on("error", (err) => {
reject(err);
@@ -1053,6 +1074,47 @@ module.exports.grpcQuery = async (options) => {
});
};
/**
* Returns an array of SHA256 fingerprints for all known root certificates.
* @returns {Set} A set of SHA256 fingerprints.
*/
module.exports.rootCertificatesFingerprints = () => {
let fingerprints = tls.rootCertificates.map(cert => {
let certLines = cert.split("\n");
certLines.shift();
certLines.pop();
let certBody = certLines.join("");
let buf = Buffer.from(certBody, "base64");
const shasum = crypto.createHash("sha256");
shasum.update(buf);
return shasum.digest("hex").toUpperCase().replace(/(.{2})(?!$)/g, "$1:");
});
fingerprints.push("6D:99:FB:26:5E:B1:C5:B3:74:47:65:FC:BC:64:8F:3C:D8:E1:BF:FA:FD:C4:C2:F9:9B:9D:47:CF:7F:F1:C2:4F"); // ISRG X1 cross-signed with DST X3
fingerprints.push("8B:05:B6:8C:C6:59:E5:ED:0F:CB:38:F2:C9:42:FB:FD:20:0E:6F:2F:F9:F8:5D:63:C6:99:4E:F5:E0:B0:27:01"); // ISRG X2 cross-signed with ISRG X1
return new Set(fingerprints);
};
module.exports.SHAKE256_LENGTH = 16;
/**
*
* @param {string} data
* @param {number} len
* @return {string}
*/
module.exports.shake256 = (data, len) => {
if (!data) {
return "";
}
return crypto.createHash("shake256", { outputLength: len })
.update(data)
.digest("hex");
};
// For unit test, export functions
if (process.env.TEST_BACKEND) {
module.exports.__test = {
@@ -1062,3 +1124,29 @@ if (process.env.TEST_BACKEND) {
return module.exports.__test[functionName];
};
}
/**
* Generates an abort signal with the specified timeout.
* @param {number} timeoutMs - The timeout in milliseconds.
* @returns {AbortSignal | null} - The generated abort signal, or null if not supported.
*/
module.exports.axiosAbortSignal = (timeoutMs) => {
try {
// Just in case, as 0 timeout here will cause the request to be aborted immediately
if (!timeoutMs || timeoutMs <= 0) {
timeoutMs = 5000;
}
return AbortSignal.timeout(timeoutMs);
} catch (_) {
// v16-: AbortSignal.timeout is not supported
try {
const abortController = new AbortController();
setTimeout(() => abortController.abort(), timeoutMs);
return abortController.signal;
} catch (_) {
// v15-: AbortController is not supported
return null;
}
}
};

View File

@@ -8,9 +8,9 @@
:placeholder="placeholder"
:disabled="!enabled"
>
<a class="btn btn-outline-primary" @click="action()">
<button type="button" class="btn btn-outline-primary" :aria-label="actionAriaLabel" @click="action()">
<font-awesome-icon :icon="icon" />
</a>
</button>
</div>
</template>
@@ -66,6 +66,13 @@ export default {
action: {
type: Function,
default: () => {},
},
/**
* The aria-label of the action button
*/
actionAriaLabel: {
type: String,
required: true,
}
},
emits: [ "update:modelValue" ],

View File

@@ -1,11 +1,11 @@
<template>
<div class="input-group mb-3">
<select ref="select" v-model="model" class="form-select" :disabled="disabled">
<option v-for="option in options" :key="option" :value="option.value">{{ option.label }}</option>
<select :id="id" ref="select" v-model="model" class="form-select" :disabled="disabled" :required="required">
<option v-for="option in options" :key="option" :value="option.value" :disabled="option.disabled">{{ option.label }}</option>
</select>
<a class="btn btn-outline-primary" @click="action()">
<font-awesome-icon :icon="icon" />
</a>
<button type="button" class="btn btn-outline-primary" :class="{ disabled: actionDisabled }" :aria-label="actionAriaLabel" @click="action()">
<font-awesome-icon :icon="icon" aria-hidden="true" />
</button>
</div>
</template>
@@ -20,6 +20,13 @@ export default {
type: Array,
default: () => [],
},
/**
* The id of the form which will be targeted by a <label for=..
*/
id: {
type: String,
required: true,
},
/**
* The value of the select field.
*/
@@ -50,6 +57,29 @@ export default {
action: {
type: Function,
default: () => {},
},
/**
* The aria-label of the action button
*/
actionAriaLabel: {
type: String,
required: true,
},
/**
* Whether the action button is disabled.
* @example true
*/
actionDisabled: {
type: Boolean,
default: false
},
/**
* Whether the select field is required.
* @example true
*/
required: {
type: Boolean,
default: false,
}
},
emits: [ "update:modelValue" ],

View File

@@ -135,7 +135,7 @@
<script lang="ts">
import { Modal } from "bootstrap";
import CopyableInput from "./CopyableInput.vue";
import { default as serverConfig } from "../../server/config.js";
import { badgeConstants } from "../util.ts";
export default {
components: {
@@ -230,7 +230,7 @@ export default {
"labelColor",
],
},
badgeConstants: serverConfig.badgeConstants,
badgeConstants,
};
},

View File

@@ -70,7 +70,7 @@ export default {
Confirm,
},
props: {},
emits: [ "added" ],
emits: [ "added", "deleted" ],
data() {
return {
modal: null,
@@ -167,6 +167,7 @@ export default {
this.processing = false;
if (res.ok) {
this.$emit("deleted", this.id);
this.modal.hide();
}
});

View File

@@ -11,7 +11,7 @@
/>
</div>
<div
v-if="size !== 'small' && beatList.length > 4 && $root.styleElapsedTime !== 'none'"
v-if="!$root.isMobile && size !== 'small' && beatList.length > 4 && $root.styleElapsedTime !== 'none'"
class="d-flex justify-content-between align-items-center word" :style="timeStyle"
>
<div>{{ timeSinceFirstBeat }} ago</div>

View File

@@ -29,10 +29,10 @@ export default {
},
computed: {
startDateTime() {
return dayjs(this.maintenance.timeslotList[0].startDate).tz(this.maintenance.timezone).format(SQL_DATETIME_FORMAT_WITHOUT_SECOND);
return dayjs(this.maintenance.timeslotList[0].startDate).tz(this.maintenance.timezone, true).format(SQL_DATETIME_FORMAT_WITHOUT_SECOND);
},
endDateTime() {
return dayjs(this.maintenance.timeslotList[0].endDate).tz(this.maintenance.timezone).format(SQL_DATETIME_FORMAT_WITHOUT_SECOND);
return dayjs(this.maintenance.timeslotList[0].endDate).tz(this.maintenance.timezone, true).format(SQL_DATETIME_FORMAT_WITHOUT_SECOND);
}
},
};

View File

@@ -16,7 +16,10 @@
</a>
<form>
<input
v-model="searchText" class="form-control search-input" :placeholder="$t('Search...')"
v-model="searchText"
class="form-control search-input"
:placeholder="$t('Search...')"
:aria-label="$t('Search monitored sites')"
autocomplete="off"
/>
</form>

View File

@@ -62,7 +62,7 @@
</span>
</div>
<div class="extra-info">
<div v-if="showCertificateExpiry && monitor.element.type === 'http'">
<div v-if="showCertificateExpiry && monitor.element.certExpiryDaysRemaining">
<Tag :item="{name: $t('Cert Exp.'), value: formattedCertExpiryMessage(monitor), color: certExpiryColor(monitor)}" :size="'sm'" />
</div>
<div v-if="showTags">

View File

@@ -13,6 +13,15 @@
<div class="mb-3">
<label for="ntfy-priority" class="form-label">{{ $t("Priority") }}</label>
<input id="ntfy-priority" v-model="$parent.notification.ntfyPriority" type="number" class="form-control" required min="1" max="5" step="1">
<div class="form-text">
<p v-if="$parent.notification.ntfyPriority >= 5">
{{ $t("ntfyPriorityHelptextAllEvents") }}
</p>
<i18n-t v-else tag="p" keypath="ntfyPriorityHelptextAllExceptDown">
<code>DOWN</code>
<code>{{ $parent.notification.ntfyPriority + 1 }}</code>
</i18n-t>
</div>
</div>
<div class="mb-3">
<label for="authentication-method" class="form-label">{{ $t("ntfyAuthenticationMethod") }}</label>

View File

@@ -27,13 +27,13 @@
<div class="mt-1 mb-3 ps-2 cert-exp-days col-12 col-xl-6">
<div v-for="day in settings.tlsExpiryNotifyDays" :key="day" class="d-flex align-items-center justify-content-between cert-exp-day-row py-2">
<span>{{ day }} {{ $tc("day", day) }}</span>
<button type="button" class="btn-rm-expiry btn btn-outline-danger ms-2 py-1" @click="removeExpiryNotifDay(day)">
<font-awesome-icon class="" icon="times" />
<button type="button" class="btn-rm-expiry btn btn-outline-danger ms-2 py-1" :aria-label="$t('Remove the expiry notification')" @click="removeExpiryNotifDay(day)">
<font-awesome-icon icon="times" />
</button>
</div>
</div>
<div class="col-12 col-xl-6">
<ActionInput v-model="expiryNotifInput" :type="'number'" :placeholder="$t('day')" :icon="'plus'" :action="() => addExpiryNotifDay(expiryNotifInput)" />
<ActionInput v-model="expiryNotifInput" :type="'number'" :placeholder="$t('day')" :icon="'plus'" :action="() => addExpiryNotifDay(expiryNotifInput)" :action-aria-label="$t('Add a new expiry notification day')" />
</div>
<div>
<button class="btn btn-primary" type="button" @click="saveSettings()">

View File

@@ -183,6 +183,7 @@
"Pink": "Pink",
"Custom": "Custom",
"Search...": "Search…",
"Search monitored sites": "Search monitored sites",
"Avg. Ping": "Avg. Ping",
"Avg. Response": "Avg. Response",
"Entry Page": "Entry Page",
@@ -334,6 +335,8 @@
"Fingerprint:": "Fingerprint:",
"No status pages": "No status pages",
"Domain Name Expiry Notification": "Domain Name Expiry Notification",
"Add a new expiry notification day": "Add a new expiry notification day",
"Remove the expiry notification": "Remove the expiry notification day",
"Proxy": "Proxy",
"Date Created": "Date Created",
"Footer Text": "Footer Text",
@@ -369,6 +372,8 @@
"Setup Docker Host": "Setup Docker Host",
"Connection Type": "Connection Type",
"Docker Daemon": "Docker Daemon",
"noDockerHostMsg": "Not Available. Setup a Docker Host First.",
"DockerHostRequired": "Please set the Docker Host for this monitor.",
"deleteDockerHostMsg": "Are you sure want to delete this docker host for all monitors?",
"socket": "Socket",
"tcp": "TCP / HTTP",
@@ -654,6 +659,10 @@
"Notify Channel": "Notify Channel",
"aboutNotifyChannel": "Notify channel will trigger a desktop or mobile notification for all members of the channel, whether their availability is set to active or away.",
"Uptime Kuma URL": "Uptime Kuma URL",
"setup a new monitor group": "setup a new monitor group",
"openModalTo": "open modal to {0}",
"Add a domain": "Add a domain",
"Remove domain": "Remove domain '{0}'",
"Icon Emoji": "Icon Emoji",
"signalImportant": "IMPORTANT: You cannot mix groups and numbers in recipients!",
"aboutWebhooks": "More info about Webhooks on: {0}",
@@ -747,6 +756,8 @@
"lunaseaDeviceID": "Device ID",
"lunaseaUserID": "User ID",
"ntfyAuthenticationMethod": "Authentication Method",
"ntfyPriorityHelptextAllEvents": "All events are send with the maximum priority",
"ntfyPriorityHelptextAllExceptDown": "All events are send with this priority, except {0}-events, which have a priority of {1}",
"ntfyUsernameAndPassword": "Username and Password",
"twilioAccountSID": "Account SID",
"twilioApiKey": "Api Key (optional)",

View File

@@ -288,6 +288,10 @@ export default {
socket.on("initServerTimezone", () => {
socket.emit("initServerTimezone", dayjs.tz.guess());
});
socket.on("refresh", () => {
location.reload();
});
},
/**

View File

@@ -82,7 +82,7 @@
<option value="redis">
Redis
</option>
<option v-if="$root.info.isContainer" value="tailscale-ping">
<option v-if="!$root.info.isContainer" value="tailscale-ping">
Tailscale Ping
</option>
</optgroup>
@@ -285,22 +285,19 @@
<!-- Docker Host -->
<!-- For Docker Type -->
<div v-if="monitor.type === 'docker'" class="my-3">
<h2 class="mb-2">{{ $t("Docker Host") }}</h2>
<p v-if="$root.dockerHostList.length === 0">
{{ $t("Not available, please setup.") }}
</p>
<div v-else class="mb-3">
<div class="mb-3">
<label for="docker-host" class="form-label">{{ $t("Docker Host") }}</label>
<select id="docket-host" v-model="monitor.docker_host" class="form-select">
<option v-for="host in $root.dockerHostList" :key="host.id" :value="host.id">{{ host.name }}</option>
</select>
<a href="#" @click="$refs.dockerHostDialog.show(monitor.docker_host)">{{ $t("Edit") }}</a>
<ActionSelect
id="docker-host"
v-model="monitor.docker_host"
:action-aria-label="$t('openModalTo', $t('Setup Docker Host'))"
:options="dockerHostOptionsList"
:disabled="$root.dockerHostList == null || $root.dockerHostList.length === 0"
:icon="'plus'"
:action="() => $refs.dockerHostDialog.show()"
:required="true"
/>
</div>
<button class="btn btn-primary me-2" type="button" @click="$refs.dockerHostDialog.show()">
{{ $t("Setup Docker Host") }}
</button>
</div>
<!-- MQTT -->
@@ -370,11 +367,20 @@
<input id="connectionString" v-model="monitor.databaseConnectionString" type="text" class="form-control" required>
</div>
</template>
<template v-if="monitor.type === 'mysql'">
<div class="my-3">
<label for="mysql-password" class="form-label">{{ $t("Password") }}</label>
<!-- TODO: Rename monitor.radiusPassword to monitor.password for general use -->
<HiddenInput id="mysql-password" v-model="monitor.radiusPassword" autocomplete="false"></HiddenInput>
</div>
</template>
<!-- SQL Server / PostgreSQL / MySQL -->
<template v-if="monitor.type === 'sqlserver' || monitor.type === 'postgres' || monitor.type === 'mysql'">
<div class="my-3">
<label for="sqlQuery" class="form-label">{{ $t("Query") }}</label>
<textarea id="sqlQuery" v-model="monitor.databaseQuery" class="form-control" :placeholder="$t('Example:', [ 'select getdate()' ])" required></textarea>
<textarea id="sqlQuery" v-model="monitor.databaseQuery" class="form-control" :placeholder="$t('Example:', [ 'SELECT 1' ])"></textarea>
</div>
</template>
@@ -401,7 +407,7 @@
</div>
<!-- Timeout: HTTP / Keyword only -->
<div v-if="monitor.type === 'http' || monitor.type === 'keyword'" class="my-3">
<div v-if="monitor.type === 'http' || monitor.type === 'keyword' || monitor.type === 'json-query'" class="my-3">
<label for="timeout" class="form-label">{{ $t("Request Timeout") }} ({{ $t("timeoutAfter", [ monitor.timeout || clampTimeout(monitor.interval) ]) }})</label>
<input id="timeout" v-model="monitor.timeout" type="number" class="form-control" required min="0" step="0.1">
</div>
@@ -460,7 +466,7 @@
</div>
<!-- HTTP / Keyword only -->
<template v-if="monitor.type === 'http' || monitor.type === 'keyword' || monitor.type === 'grpc-keyword' ">
<template v-if="monitor.type === 'http' || monitor.type === 'keyword' || monitor.type === 'json-query' || monitor.type === 'grpc-keyword' ">
<div class="my-3">
<label for="maxRedirects" class="form-label">{{ $t("Max. Redirects") }}</label>
<input id="maxRedirects" v-model="monitor.maxredirects" type="number" class="form-control" required min="0" step="1">
@@ -494,9 +500,11 @@
<!-- Parent Monitor -->
<div class="my-3">
<label for="parent" class="form-label">{{ $t("Monitor Group") }}</label>
<label for="monitorGroupSelector" class="form-label">{{ $t("Monitor Group") }}</label>
<ActionSelect
id="monitorGroupSelector"
v-model="monitor.parent"
:action-aria-label="$t('openModalTo', 'setup a new monitor group')"
:options="parentMonitorOptionsList"
:disabled="sortedGroupMonitorList.length === 0 && draftGroupName == null"
:icon="'plus'"
@@ -840,9 +848,9 @@ import NotificationDialog from "../components/NotificationDialog.vue";
import DockerHostDialog from "../components/DockerHostDialog.vue";
import ProxyDialog from "../components/ProxyDialog.vue";
import TagsManager from "../components/TagsManager.vue";
import { genSecret, isDev, MAX_INTERVAL_SECOND, MIN_INTERVAL_SECOND } from "../util.ts";
import { genSecret, isDev, MAX_INTERVAL_SECOND, MIN_INTERVAL_SECOND, sleep } from "../util.ts";
import { hostNameRegexPattern } from "../util-frontend";
import { sleep } from "../util";
import HiddenInput from "../components/HiddenInput.vue";
const toast = useToast();
@@ -855,7 +863,7 @@ const monitorDefaults = {
interval: 60,
retryInterval: 60,
resendInterval: 0,
maxretries: 1,
maxretries: 0,
timeout: 48,
notificationIDList: {},
ignoreTls: false,
@@ -880,11 +888,14 @@ const monitorDefaults = {
kafkaProducerSaslOptions: {
mechanism: "None",
},
kafkaProducerSsl: false,
kafkaProducerAllowAutoTopicCreation: false,
gamedigGivenPortOnly: true,
};
export default {
components: {
HiddenInput,
ActionSelect,
ProxyDialog,
CopyableInput,
@@ -1106,6 +1117,21 @@ message HealthCheckResponse {
return list;
},
dockerHostOptionsList() {
if (this.$root.dockerHostList && this.$root.dockerHostList.length > 0) {
return this.$root.dockerHostList.map((host) => {
return {
label: host.name,
value: host.id
};
});
} else {
return [{
label: this.$t("noDockerHostMsg"),
value: null,
}];
}
}
},
watch: {
"$root.proxyList"() {
@@ -1338,6 +1364,12 @@ message HealthCheckResponse {
return false;
}
}
if (this.monitor.type === "docker") {
if (this.monitor.docker_host == null) {
toast.error(this.$t("DockerHostRequired"));
return false;
}
}
return true;
},

View File

@@ -69,13 +69,17 @@
<div class="my-3">
<label class="form-label">
{{ $t("Domain Names") }}
<font-awesome-icon icon="plus-circle" class="btn-add-domain action text-primary" @click="addDomainField" />
<button class="p-0 bg-transparent border-0" :aria-label="$t('Add a domain')" @click="addDomainField">
<font-awesome-icon icon="plus-circle" class="action text-primary" />
</button>
</label>
<ul class="list-group domain-name-list">
<li v-for="(domain, index) in config.domainNameList" :key="index" class="list-group-item">
<input v-model="config.domainNameList[index]" type="text" class="no-bg domain-input" placeholder="example.com" />
<font-awesome-icon icon="times" class="action remove ms-2 me-3 text-danger" @click="removeDomain(index)" />
<button class="p-0 bg-transparent border-0" :aria-label="$t('Remove domain', [ domain ])" @click="removeDomain(index)">
<font-awesome-icon icon="times" class="action remove ms-2 me-3 text-danger" />
</button>
</li>
</ul>
</div>
@@ -102,7 +106,7 @@
<!-- Sidebar Footer -->
<div class="sidebar-footer">
<button class="btn btn-success me-2" @click="save">
<button class="btn btn-success me-2" :disabled="loading" @click="save">
<font-awesome-icon icon="save" />
{{ $t("Save") }}
</button>
@@ -438,6 +442,7 @@ export default {
lastUpdateTime: dayjs(),
updateCountdown: null,
updateCountdownText: null,
loading: true,
};
},
computed: {
@@ -697,6 +702,8 @@ export default {
this.incident = res.data.incident;
this.maintenanceList = res.data.maintenanceList;
this.$root.publicGroupList = res.data.publicGroupList;
this.loading = false;
}).catch( function (error) {
if (error.response.status === 404) {
location.href = "/page-not-found";
@@ -806,6 +813,7 @@ export default {
/** Save the status page */
save() {
this.loading = true;
let startTime = new Date();
this.config.slug = this.config.slug.trim().toLowerCase();
@@ -823,10 +831,12 @@ export default {
}
setTimeout(() => {
this.loading = false;
location.href = "/status/" + this.config.slug;
}, time);
} else {
this.loading = false;
toast.error(res.msg);
}
});

View File

@@ -7,7 +7,7 @@
// Backend uses the compiled file util.js
// Frontend uses util.ts
Object.defineProperty(exports, "__esModule", { value: true });
exports.localToUTC = exports.utcToLocal = exports.utcToISODateTime = exports.isoToUTCDateTime = exports.parseTimeFromTimeObject = exports.parseTimeObject = exports.getMaintenanceRelativeURL = exports.getMonitorRelativeURL = exports.genSecret = exports.getCryptoRandomInt = exports.getRandomInt = exports.getRandomArbitrary = exports.TimeLogger = exports.polyfill = exports.log = exports.debug = exports.ucfirst = exports.sleep = exports.flipStatus = exports.MIN_INTERVAL_SECOND = exports.MAX_INTERVAL_SECOND = exports.SQL_DATETIME_FORMAT_WITHOUT_SECOND = exports.SQL_DATETIME_FORMAT = exports.SQL_DATE_FORMAT = exports.STATUS_PAGE_MAINTENANCE = exports.STATUS_PAGE_PARTIAL_DOWN = exports.STATUS_PAGE_ALL_UP = exports.STATUS_PAGE_ALL_DOWN = exports.MAINTENANCE = exports.PENDING = exports.UP = exports.DOWN = exports.appName = exports.isDev = void 0;
exports.localToUTC = exports.utcToLocal = exports.utcToISODateTime = exports.isoToUTCDateTime = exports.parseTimeFromTimeObject = exports.parseTimeObject = exports.getMaintenanceRelativeURL = exports.getMonitorRelativeURL = exports.genSecret = exports.getCryptoRandomInt = exports.getRandomInt = exports.getRandomArbitrary = exports.TimeLogger = exports.polyfill = exports.log = exports.debug = exports.ucfirst = exports.sleep = exports.flipStatus = exports.badgeConstants = exports.MIN_INTERVAL_SECOND = exports.MAX_INTERVAL_SECOND = exports.SQL_DATETIME_FORMAT_WITHOUT_SECOND = exports.SQL_DATETIME_FORMAT = exports.SQL_DATE_FORMAT = exports.STATUS_PAGE_MAINTENANCE = exports.STATUS_PAGE_PARTIAL_DOWN = exports.STATUS_PAGE_ALL_UP = exports.STATUS_PAGE_ALL_DOWN = exports.MAINTENANCE = exports.PENDING = exports.UP = exports.DOWN = exports.appName = exports.isDev = void 0;
const dayjs = require("dayjs");
exports.isDev = process.env.NODE_ENV === "development";
exports.appName = "Uptime Kuma";
@@ -24,6 +24,25 @@ exports.SQL_DATETIME_FORMAT = "YYYY-MM-DD HH:mm:ss";
exports.SQL_DATETIME_FORMAT_WITHOUT_SECOND = "YYYY-MM-DD HH:mm";
exports.MAX_INTERVAL_SECOND = 2073600; // 24 days
exports.MIN_INTERVAL_SECOND = 20; // 20 seconds
exports.badgeConstants = {
naColor: "#999",
defaultUpColor: "#66c20a",
defaultWarnColor: "#eed202",
defaultDownColor: "#c2290a",
defaultPendingColor: "#f8a306",
defaultMaintenanceColor: "#1747f5",
defaultPingColor: "blue",
defaultStyle: "flat",
defaultPingValueSuffix: "ms",
defaultPingLabelSuffix: "h",
defaultUptimeValueSuffix: "%",
defaultUptimeLabelSuffix: "h",
defaultCertExpValueSuffix: " days",
defaultCertExpLabelSuffix: "h",
// Values Come From Default Notification Times
defaultCertExpireWarnDays: "14",
defaultCertExpireDownDays: "7"
};
/** Flip the status of s */
function flipStatus(s) {
if (s === exports.UP) {
@@ -101,6 +120,9 @@ class Logger {
* @param level Log level. One of INFO, WARN, ERROR, DEBUG or can be customized.
*/
log(module, msg, level) {
if (level === "DEBUG" && !exports.isDev) {
return;
}
if (this.hideLog[level] && this.hideLog[level].includes(module.toLowerCase())) {
return;
}

View File

@@ -29,6 +29,26 @@ export const SQL_DATETIME_FORMAT_WITHOUT_SECOND = "YYYY-MM-DD HH:mm";
export const MAX_INTERVAL_SECOND = 2073600; // 24 days
export const MIN_INTERVAL_SECOND = 20; // 20 seconds
export const badgeConstants = {
naColor: "#999",
defaultUpColor: "#66c20a",
defaultWarnColor: "#eed202",
defaultDownColor: "#c2290a",
defaultPendingColor: "#f8a306",
defaultMaintenanceColor: "#1747f5",
defaultPingColor: "blue", // as defined by badge-maker / shields.io
defaultStyle: "flat",
defaultPingValueSuffix: "ms",
defaultPingLabelSuffix: "h",
defaultUptimeValueSuffix: "%",
defaultUptimeLabelSuffix: "h",
defaultCertExpValueSuffix: " days",
defaultCertExpLabelSuffix: "h",
// Values Come From Default Notification Times
defaultCertExpireWarnDays: "14",
defaultCertExpireDownDays: "7"
};
/** Flip the status of s */
export function flipStatus(s: number) {
if (s === UP) {
@@ -115,6 +135,10 @@ class Logger {
* @param level Log level. One of INFO, WARN, ERROR, DEBUG or can be customized.
*/
log(module: string, msg: any, level: string) {
if (level === "DEBUG" && !isDev) {
return;
}
if (this.hideLog[level] && this.hideLog[level].includes(module.toLowerCase())) {
return;
}