From 482049c72b3a650c7bc5c26c2f4d57a21c0e0aa0 Mon Sep 17 00:00:00 2001 From: Louis Lam Date: Sun, 10 Dec 2023 20:40:40 +0800 Subject: [PATCH] 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 --- config/vite.config.js | 2 - extra/healthcheck.js | 2 +- extra/reset-password.js | 45 +++++++++++++++ package.json | 1 - server/config.js | 55 ++++++++++++------- server/routers/api-router.js | 3 +- server/routers/status-page-router.js | 2 +- server/server.js | 16 ++---- .../socket-handlers/general-socket-handler.js | 10 ++++ server/uptime-kuma-server.js | 33 ++++++++--- server/util-server.js | 3 +- src/components/BadgeGeneratorDialog.vue | 4 +- src/mixins/socket.js | 4 ++ src/pages/EditMonitor.vue | 3 +- src/util.js | 21 ++++++- src/util.ts | 20 +++++++ 16 files changed, 170 insertions(+), 54 deletions(-) diff --git a/config/vite.config.js b/config/vite.config.js index 11c61006..8aaa7d02 100644 --- a/config/vite.config.js +++ b/config/vite.config.js @@ -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" ], 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 16898331..7f23a83f 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, @@ -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(); } diff --git a/package.json b/package.json index 54b0f54b..95d6f878 100644 --- a/package.json +++ b/package.json @@ -192,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", 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 acf8d56b..c9b2ec88 100644 --- a/server/routers/api-router.js +++ b/server/routers/api-router.js @@ -11,12 +11,11 @@ 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(); diff --git a/server/routers/status-page-router.js b/server/routers/status-page-router.js index b60f286e..c7a580d8 100644 --- a/server/routers/status-page-router.js +++ b/server/routers/status-page-router.js @@ -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(); diff --git a/server/server.js b/server/server.js index 11174f76..af37c949 100644 --- a/server/server.js +++ b/server/server.js @@ -81,7 +81,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; @@ -91,7 +91,7 @@ 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, shake256, SHAKE256_LENGTH +const { getSettings, setSettings, setting, initJWTSecret, checkLogin, startUnitTest, doubleCheckPassword, startE2eTests, shake256, SHAKE256_LENGTH } = require("./util-server"); log.debug("server", "Importing Notification"); @@ -115,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; @@ -1157,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.", diff --git a/server/socket-handlers/general-socket-handler.js b/server/socket-handlers/general-socket-handler.js index 3fc6f1d5..b3163c1c 100644 --- a/server/socket-handlers/general-socket-handler.js +++ b/server/socket-handlers/general-socket-handler.js @@ -78,4 +78,14 @@ module.exports.generalSocketHandler = (socket, server) => { }); } }); + + // 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 07dfc30f..556f19d5 100644 --- a/server/uptime-kuma-server.js +++ b/server/uptime-kuma-server.js @@ -12,6 +12,7 @@ const { Settings } = require("./settings"); const dayjs = require("dayjs"); 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), @@ -422,6 +418,25 @@ class UptimeKumaServer { } } } + + /** + * 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 ec0cb768..70588880 100644 --- a/server/util-server.js +++ b/server/util-server.js @@ -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; diff --git a/src/components/BadgeGeneratorDialog.vue b/src/components/BadgeGeneratorDialog.vue index aa6fa6e8..252a18c5 100644 --- a/src/components/BadgeGeneratorDialog.vue +++ b/src/components/BadgeGeneratorDialog.vue @@ -135,7 +135,7 @@