diff --git a/.eslintrc.js b/.eslintrc.js index 4b60d672..ef556a34 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -84,7 +84,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, @@ -96,7 +96,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 diff --git a/.github/workflows/auto-test.yml b/.github/workflows/auto-test.yml index 22769319..853548ff 100644 --- a/.github/workflows/auto-test.yml +++ b/.github/workflows/auto-test.yml @@ -27,10 +27,10 @@ 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@9 -g @@ -55,10 +55,10 @@ 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@9 -g @@ -69,40 +69,39 @@ jobs: steps: - run: git config --global core.autocrlf false # Mainly for Windows - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Use Node.js 20 - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: 20 - run: npm install - - run: npm run lint + - run: npm run lint:prod -# TODO: Temporarily disable, as it cannot pass the test in 2.0.0 yet -# e2e-tests: -# needs: [ check-linters ] -# runs-on: ubuntu-latest -# steps: -# - run: git config --global core.autocrlf false # Mainly for Windows -# - uses: actions/checkout@v3 -# -# - name: Use Node.js 14 -# uses: actions/setup-node@v3 -# with: -# node-version: 14 -# - run: npm install -# - run: npm run build -# - run: npm run cy:test + e2e-tests: + needs: [ check-linters ] + runs-on: ubuntu-latest + steps: + - run: git config --global core.autocrlf false # Mainly for Windows + - uses: actions/checkout@v4 + + - name: Use Node.js 14 + uses: actions/setup-node@v4 + with: + node-version: 14 + - run: npm install + - run: npm run build + - run: npm run cy:test frontend-unit-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 diff --git a/.github/workflows/close-incorrect-issue.yml b/.github/workflows/close-incorrect-issue.yml index 762bc968..e26cf5e5 100644 --- a/.github/workflows/close-incorrect-issue.yml +++ b/.github/workflows/close-incorrect-issue.yml @@ -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' diff --git a/.github/workflows/json-yaml-validate.yml b/.github/workflows/json-yaml-validate.yml index 104e37a1..b6437ec4 100644 --- a/.github/workflows/json-yaml-validate.yml +++ b/.github/workflows/json-yaml-validate.yml @@ -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 diff --git a/config/vite.config.js b/config/vite.config.js index 5d9c5c1d..8f2a076f 100644 --- a/config/vite.config.js +++ b/config/vite.config.js @@ -2,7 +2,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"); @@ -21,7 +20,6 @@ export default defineConfig({ "CODESPACE_NAME": JSON.stringify(process.env.CODESPACE_NAME), }, plugins: [ - commonjs(), vue(), visualizer({ filename: "tmp/dist-stats.html" diff --git a/extra/healthcheck.js b/extra/healthcheck.js index 5e06c212..c9391c41 100644 --- a/extra/healthcheck.js +++ b/extra/healthcheck.js @@ -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"; diff --git a/extra/reset-password.js b/extra/reset-password.js index 2fbc622d..ec3ede6c 100644 --- a/extra/reset-password.js +++ b/extra/reset-password.js @@ -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, @@ -50,6 +52,9 @@ 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 { @@ -57,6 +62,7 @@ const main = async () => { } } console.log("Password reset successfully."); + } } catch (e) { console.error("Error: " + e.message); @@ -81,6 +87,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(); } diff --git a/package.json b/package.json index 456e2c5d..29289447 100644 --- a/package.json +++ b/package.json @@ -11,10 +11,12 @@ }, "scripts": { "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", @@ -44,7 +46,7 @@ "build-docker-nightly-local": "npm run build && docker build -f docker/dockerfile -t louislam/uptime-kuma:nightly2 --target nightly .", "build-docker-pr-test": "docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64 -t louislam/uptime-kuma:pr-test2 --target pr-test2 . --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.8 && 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", @@ -191,7 +193,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", diff --git a/public/icon.svg b/public/icon.svg index bad8394c..c4217915 100644 --- a/public/icon.svg +++ b/public/icon.svg @@ -1,10 +1,9 @@ - - - - - + + + - + + diff --git a/server/config.js b/server/config.js index 77f9e74b..635c37e7 100644 --- a/server/config.js +++ b/server/config.js @@ -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, }; diff --git a/server/routers/api-router.js b/server/routers/api-router.js index 7b14a6da..b63b5123 100644 --- a/server/routers/api-router.js +++ b/server/routers/api-router.js @@ -11,11 +11,10 @@ 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 { makeBadge } = require("badge-maker"); -const { badgeConstants } = require("../config"); const { Prometheus } = require("../prometheus"); const Database = require("../database"); const { UptimeCalculator } = require("../uptime-calculator"); diff --git a/server/routers/status-page-router.js b/server/routers/status-page-router.js index 86d2af9a..29acaf8e 100644 --- a/server/routers/status-page-router.js +++ b/server/routers/status-page-router.js @@ -4,7 +4,7 @@ const { UptimeKumaServer } = require("../uptime-kuma-server"); const StatusPage = require("../model/status_page"); const { allowDevAllOrigin, sendHttpError } = require("../util-server"); const { R } = require("redbean-node"); -const { badgeConstants } = require("../config"); +const { badgeConstants } = require("../../src/util"); const { makeBadge } = require("badge-maker"); const { UptimeCalculator } = require("../uptime-calculator"); diff --git a/server/server.js b/server/server.js index 098b731f..1994303f 100644 --- a/server/server.js +++ b/server/server.js @@ -46,8 +46,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", "Env: " + process.env.NODE_ENV); log.debug("server", "Inside Container: " + (process.env.UPTIME_KUMA_IS_CONTAINER === "1")); +log.info("server", "WebSocket Origin Check: " + process.env.UPTIME_KUMA_WS_ORIGIN_CHECK); const checkVersion = require("./check-version"); log.info("server", "Uptime Kuma Version: " + checkVersion.version); @@ -72,8 +77,7 @@ 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; @@ -82,7 +86,7 @@ const Monitor = require("./model/monitor"); const User = require("./model/user"); log.debug("server", "Importing Settings"); -const { getSettings, setSettings, setting, initJWTSecret, checkLogin, FBSD, doubleCheckPassword, startE2eTests, shake256, SHAKE256_LENGTH, allowDevAllOrigin, +const { getSettings, setSettings, setting, initJWTSecret, checkLogin, startUnitTest, doubleCheckPassword, startE2eTests, shake256, SHAKE256_LENGTH, allowDevAllOrigin, } = require("./util-server"); log.debug("server", "Importing Notification"); @@ -100,19 +104,13 @@ const { apiAuth } = require("./auth"); const { login } = require("./auth"); const passwordHash = require("./password-hash"); -// 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; @@ -1265,6 +1263,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: "successAuthChangePassword", diff --git a/server/socket-handlers/general-socket-handler.js b/server/socket-handlers/general-socket-handler.js index 1269bc25..68e1f814 100644 --- a/server/socket-handlers/general-socket-handler.js +++ b/server/socket-handlers/general-socket-handler.js @@ -109,4 +109,14 @@ module.exports.generalSocketHandler = (socket, server) => { msg: "Not found", }); }); + + // 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); + } + }); }; diff --git a/server/uptime-kuma-server.js b/server/uptime-kuma-server.js index f3a1c7b7..d5ed5687 100644 --- a/server/uptime-kuma-server.js +++ b/server/uptime-kuma-server.js @@ -4,7 +4,7 @@ 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 { Settings } = require("./settings"); @@ -12,6 +12,7 @@ const dayjs = require("dayjs"); const childProcessAsync = require("promisify-child-process"); const path = require("path"); const axios = require("axios"); +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. /** @@ -67,9 +68,9 @@ class UptimeKumaServer { * @param {object} args Arguments to pass to instance constructor * @returns {UptimeKumaServer} Server instance */ - static getInstance(args) { + static getInstance() { if (UptimeKumaServer.instance == null) { - UptimeKumaServer.instance = new UptimeKumaServer(args); + UptimeKumaServer.instance = new UptimeKumaServer(); } return UptimeKumaServer.instance; } @@ -77,7 +78,7 @@ class UptimeKumaServer { /** * @param {object} args Arguments to initialise server with */ - constructor(args) { + constructor() { // 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; @@ -91,7 +92,7 @@ class UptimeKumaServer { 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), @@ -119,7 +120,41 @@ class UptimeKumaServer { UptimeKumaServer.monitorTypeList["dns"] = new DnsMonitorType(); UptimeKumaServer.monitorTypeList["mqtt"] = new MqttMonitorType(); - 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); + } + }); } /** @@ -424,6 +459,25 @@ class UptimeKumaServer { getUserAgent() { return "Uptime-Kuma/" + require("../package.json").version; } + + /** + * 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 = { diff --git a/server/util-server.js b/server/util-server.js index 3246925e..5f8c063b 100644 --- a/server/util-server.js +++ b/server/util-server.js @@ -1,14 +1,13 @@ 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"); const iconv = require("iconv-lite"); const chardet = require("chardet"); const chroma = require("chroma-js"); -const { badgeConstants } = require("./config"); const mssql = require("mssql"); const { Client } = require("pg"); const postgresConParse = require("pg-connection-string").parse; diff --git a/src/components/ActionInput.vue b/src/components/ActionInput.vue index 5303e427..a61e4f96 100644 --- a/src/components/ActionInput.vue +++ b/src/components/ActionInput.vue @@ -8,7 +8,7 @@ :placeholder="placeholder" :disabled="!enabled" > - diff --git a/src/components/ActionSelect.vue b/src/components/ActionSelect.vue index b163cd3a..d47154f4 100644 --- a/src/components/ActionSelect.vue +++ b/src/components/ActionSelect.vue @@ -3,7 +3,7 @@ - diff --git a/src/components/BadgeGeneratorDialog.vue b/src/components/BadgeGeneratorDialog.vue index 5ae0a877..7faa4c53 100644 --- a/src/components/BadgeGeneratorDialog.vue +++ b/src/components/BadgeGeneratorDialog.vue @@ -135,7 +135,7 @@