From 07c52ccebbb0f573538dd83ad39cd27935751596 Mon Sep 17 00:00:00 2001 From: Louis Lam Date: Sun, 10 Dec 2023 21:16:23 +0800 Subject: [PATCH] Update from upstream (#271) * Fix * Fix * Fix logout * Fix logout * Fix * WIP --- backend/dockge-server.ts | 64 +++++++++++++++++++ .../socket-handlers/main-socket-handler.ts | 14 ++++ backend/util-common.ts | 5 ++ extra/reset-password.ts | 54 ++++++++++++++-- frontend/src/mixins/socket.ts | 4 ++ 5 files changed, 137 insertions(+), 4 deletions(-) diff --git a/backend/dockge-server.ts b/backend/dockge-server.ts index da0d3fb..a55ecc3 100644 --- a/backend/dockge-server.ts +++ b/backend/dockge-server.ts @@ -194,6 +194,39 @@ export class DockgeServer { // Create Socket.io this.io = new socketIO.Server(this.httpServer, { cors, + allowRequest: (req, callback) => { + let isOriginValid = true; + const bypass = isDev; + + 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); + } }); this.io.on("connection", async (socket: Socket) => { @@ -578,4 +611,35 @@ export class DockgeServer { finalFunction() { log.info("server", "Graceful shutdown successful!"); } + + /** + * Force connected sockets of a user to refresh and disconnect. + * Used for resetting password. + * @param {string} userID + * @param {string?} currentSocketID + */ + disconnectAllSocketClients(userID: number, currentSocketID? : string) { + for (const rawSocket of this.io.sockets.sockets.values()) { + let socket = rawSocket as DockgeSocket; + if (socket.userID === userID && socket.id !== currentSocketID) { + try { + socket.emit("refresh"); + socket.disconnect(); + } catch (e) { + + } + } + } + } + + isSSL() { + return this.config.sslKey && this.config.sslCert; + } + + getLocalWebSocketURL() { + const protocol = this.isSSL() ? "wss" : "ws"; + const host = this.config.hostname || "localhost"; + return `${protocol}://${host}:${this.config.port}`; + } + } diff --git a/backend/socket-handlers/main-socket-handler.ts b/backend/socket-handlers/main-socket-handler.ts index bfdb45d..e6734d5 100644 --- a/backend/socket-handlers/main-socket-handler.ts +++ b/backend/socket-handlers/main-socket-handler.ts @@ -211,6 +211,8 @@ export class MainSocketHandler extends SocketHandler { let user = await doubleCheckPassword(socket, password.currentPassword); await user.resetPassword(password.newPassword); + server.disconnectAllSocketClients(user.id, socket.id); + callback({ ok: true, msg: "Password has been updated successfully.", @@ -280,6 +282,18 @@ export class MainSocketHandler extends SocketHandler { } } }); + + // Disconnect all other socket clients of the user + socket.on("disconnectOtherSocketClients", async () => { + try { + checkLogin(socket); + server.disconnectAllSocketClients(socket.userID, socket.id); + } catch (e) { + if (e instanceof Error) { + log.warn("disconnectOtherSocketClients", e.message); + } + } + }); } async login(username : string, password : string) : Promise { diff --git a/backend/util-common.ts b/backend/util-common.ts index 2c2f893..c7eabec 100644 --- a/backend/util-common.ts +++ b/backend/util-common.ts @@ -21,6 +21,11 @@ export interface LooseObject { [key: string]: any } +export interface BaseRes { + ok: boolean; + msg?: string; +} + let randomBytes : (numBytes: number) => Uint8Array; initRandomBytes(); diff --git a/extra/reset-password.ts b/extra/reset-password.ts index 8c2a2df..4ba51ee 100644 --- a/extra/reset-password.ts +++ b/extra/reset-password.ts @@ -4,6 +4,8 @@ import readline from "readline"; import { User } from "../backend/models/user"; import { DockgeServer } from "../backend/dockge-server"; import { log } from "../backend/log"; +import { io } from "socket.io-client"; +import { BaseRes } from "../backend/util-common"; console.log("== Dockge Reset Password Tool =="); @@ -12,11 +14,10 @@ const rl = readline.createInterface({ output: process.stdout }); +const server = new DockgeServer(); + export const main = async () => { - const server = new DockgeServer(); - // Check if - console.log("Connecting the database"); try { await Database.init(server); @@ -47,12 +48,16 @@ export const main = async () => { // Reset all sessions by reset jwt secret await server.initJWTSecret(); + console.log("Password reset successfully."); + + // 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) { if (e instanceof Error) { @@ -79,6 +84,47 @@ function question(question : string) : Promise { }); } +function disconnectAllSocketClients(username : string, password : string) : Promise { + return new Promise((resolve) => { + const url = server.getLocalWebSocketURL(); + + console.log("Connecting to " + url + " to disconnect all other socket clients"); + + // Disconnect all socket connections + const socket = io(url, { + transports: [ "websocket" ], + reconnection: false, + timeout: 5000, + }); + socket.on("connect", () => { + socket.emit("login", { + username, + password, + }, (res : BaseRes) => { + 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 " + url); + 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/frontend/src/mixins/socket.ts b/frontend/src/mixins/socket.ts index 30c74e7..2ba8464 100644 --- a/frontend/src/mixins/socket.ts +++ b/frontend/src/mixins/socket.ts @@ -202,6 +202,10 @@ export default defineComponent({ } } }); + + socket.on("refresh", () => { + location.reload(); + }); }, /**