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:
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user