Add clipboard monitoring functionality to the agent. Implement clipboard event logging, including text and image capture, with support for retention policies. Update API to allow file deletion and enhance web interface for file management. Add tests for clipboard operations.

This commit is contained in:
2026-08-28 13:08:14 +03:00
parent fe560c6a65
commit 077d9ab850
13 changed files with 695 additions and 5 deletions
+231
View File
@@ -0,0 +1,231 @@
//go:build windows
package clipmon
import (
"bytes"
"encoding/binary"
"image"
"image/color"
"image/png"
"strings"
"syscall"
"time"
"unsafe"
)
const (
cfBitmap = 2
cfDIB = 8
cfUnicodeText = 13
)
var (
user32 = syscall.NewLazyDLL("user32.dll")
kernel32 = syscall.NewLazyDLL("kernel32.dll")
procOpenClipboard = user32.NewProc("OpenClipboard")
procCloseClipboard = user32.NewProc("CloseClipboard")
procGetClipboardData = user32.NewProc("GetClipboardData")
procIsClipboardFormatAvailable = user32.NewProc("IsClipboardFormatAvailable")
procGetClipboardSequenceNumber = user32.NewProc("GetClipboardSequenceNumber")
procGlobalLock = kernel32.NewProc("GlobalLock")
procGlobalUnlock = kernel32.NewProc("GlobalUnlock")
procGlobalSize = kernel32.NewProc("GlobalSize")
)
type bitmapInfoHeader struct {
Size uint32
Width int32
Height int32
Planes uint16
BitCount uint16
Compression uint32
SizeImage uint32
XPelsPerMeter int32
YPelsPerMeter int32
ClrUsed uint32
ClrImportant uint32
}
func startPlatform(w *Writer, events chan Event, stop <-chan struct{}, done chan struct{}) error {
go func() {
defer close(done)
pollClipboard(w, stop)
}()
return nil
}
func pollClipboard(w *Writer, stop <-chan struct{}) {
lastSeq := sequenceNumber()
ticker := time.NewTicker(300 * time.Millisecond)
defer ticker.Stop()
for {
select {
case <-stop:
return
case <-ticker.C:
seq := sequenceNumber()
if seq == lastSeq {
continue
}
lastSeq = seq
captureClipboard(w)
}
}
}
func sequenceNumber() uint32 {
n, _, _ := procGetClipboardSequenceNumber.Call()
return uint32(n)
}
func captureClipboard(w *Writer) {
ok, _, _ := procOpenClipboard.Call(0)
if ok == 0 {
return
}
defer procCloseClipboard.Call()
if text, ok := readClipboardText(); ok && strings.TrimSpace(text) != "" {
emit(Event{Kind: "text", Text: TrimText(text)})
}
if pngData, ok := readClipboardImage(); ok {
rel, err := w.SavePNG(pngData)
if err != nil {
return
}
emit(Event{Kind: "image", ImagePath: rel})
}
}
func readClipboardText() (string, bool) {
ok, _, _ := procIsClipboardFormatAvailable.Call(cfUnicodeText)
if ok == 0 {
return "", false
}
handle, _, _ := procGetClipboardData.Call(cfUnicodeText)
if handle == 0 {
return "", false
}
ptr, _, _ := procGlobalLock.Call(handle)
if ptr == 0 {
return "", false
}
defer procGlobalUnlock.Call(handle)
size, _, _ := procGlobalSize.Call(handle)
if size == 0 {
return "", false
}
data := unsafe.Slice((*uint16)(unsafe.Pointer(ptr)), size/2)
for i, unit := range data {
if unit == 0 {
return syscall.UTF16ToString(data[:i]), true
}
}
return syscall.UTF16ToString(data), true
}
func readClipboardImage() ([]byte, bool) {
if dib, ok := readClipboardDIB(); ok {
return dib, true
}
return readClipboardBitmap()
}
func readClipboardDIB() ([]byte, bool) {
ok, _, _ := procIsClipboardFormatAvailable.Call(cfDIB)
if ok == 0 {
return nil, false
}
handle, _, _ := procGetClipboardData.Call(cfDIB)
if handle == 0 {
return nil, false
}
ptr, _, _ := procGlobalLock.Call(handle)
if ptr == 0 {
return nil, false
}
defer procGlobalUnlock.Call(handle)
size, _, _ := procGlobalSize.Call(handle)
if size == 0 {
return nil, false
}
raw := unsafe.Slice((*byte)(unsafe.Pointer(ptr)), size)
return dibToPNG(append([]byte(nil), raw...))
}
func readClipboardBitmap() ([]byte, bool) {
ok, _, _ := procIsClipboardFormatAvailable.Call(cfBitmap)
if ok == 0 {
return nil, false
}
// ponytail: CF_BITMAP needs a DC to read pixels; most apps also publish CF_DIB
return nil, false
}
func dibToPNG(raw []byte) ([]byte, bool) {
if len(raw) < int(unsafe.Sizeof(bitmapInfoHeader{})) {
return nil, false
}
var header bitmapInfoHeader
if err := binary.Read(bytes.NewReader(raw), binary.LittleEndian, &header); err != nil {
return nil, false
}
if header.Size < uint32(unsafe.Sizeof(bitmapInfoHeader{})) || header.BitCount != 24 && header.BitCount != 32 {
return nil, false
}
width := int(header.Width)
height := int(header.Height)
if width <= 0 || height <= 0 || width > 8192 || height > 8192 {
return nil, false
}
topDown := false
if height < 0 {
topDown = true
height = -height
}
headerSize := int(header.Size)
colorTable := int(header.ClrUsed) * 4
if header.BitCount <= 8 && header.ClrUsed == 0 {
colorTable = (1 << header.BitCount) * 4
}
offset := headerSize + colorTable
if offset > len(raw) {
return nil, false
}
pixels := raw[offset:]
stride := ((width*int(header.BitCount) + 31) / 32) * 4
if len(pixels) < stride*height {
return nil, false
}
img := image.NewRGBA(image.Rect(0, 0, width, height))
bytesPerPixel := int(header.BitCount) / 8
for y := 0; y < height; y++ {
srcY := y
if !topDown {
srcY = height - 1 - y
}
row := pixels[srcY*stride:]
for x := 0; x < width; x++ {
i := x * bytesPerPixel
if i+2 >= len(row) {
continue
}
b, g, r := row[i], row[i+1], row[i+2]
a := uint8(255)
if bytesPerPixel == 4 && i+3 < len(row) {
a = row[i+3]
}
img.SetRGBA(x, y, color.RGBA{R: r, G: g, B: b, A: a})
}
}
var out bytes.Buffer
if err := png.Encode(&out, img); err != nil {
return nil, false
}
return out.Bytes(), true
}