init: 自 zomaintain/backend/rdplib 平移独立成库; module path 从上游 github.com/nakagami/grdp 改为 git.zeroonesoft.cn/golib/rdplib
This commit is contained in:
@@ -0,0 +1,678 @@
|
||||
// Package cliprdr handler.go implements a cross-platform CLIPRDR
|
||||
// (Clipboard Virtual Channel Extension, MS-RDPECLIP) handler for
|
||||
// bidirectional text clipboard sharing between RDP client and server.
|
||||
//
|
||||
// Only text formats (CF_UNICODETEXT / CF_TEXT) are supported.
|
||||
package cliprdr
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"log/slog"
|
||||
"strings"
|
||||
"sync"
|
||||
"unicode/utf16"
|
||||
|
||||
"git.zeroonesoft.cn/golib/rdplib/core"
|
||||
)
|
||||
|
||||
// CliprdrHandler implements plugin.ChannelTransport for the "cliprdr"
|
||||
// static virtual channel. It uses callbacks for clipboard integration
|
||||
// so that any UI toolkit can wire in its own clipboard access.
|
||||
type CliprdrHandler struct {
|
||||
channelSender core.ChannelSender
|
||||
|
||||
useLongFormatNames bool
|
||||
|
||||
// serverCapsReceived is set when the server's CB_CLIP_CAPS PDU has been
|
||||
// processed. Per MS-RDPECLIP §1.3.2.1 the server sends CB_CLIP_CAPS
|
||||
// before CB_MONITOR_READY, so FORMAT_LIST should only be sent once both
|
||||
// have arrived.
|
||||
serverCapsReceived bool
|
||||
// monitorReady is set when CB_MONITOR_READY has been received.
|
||||
monitorReady bool
|
||||
|
||||
// onRemoteClipboardChanged is called with the text when the server's
|
||||
// clipboard content arrives.
|
||||
onRemoteClipboardChanged func(text string)
|
||||
|
||||
// getLocalClipboardText is called to retrieve the current local
|
||||
// clipboard text when the server requests it.
|
||||
getLocalClipboardText func() string
|
||||
|
||||
// onRemoteClipboardImage is called with PNG-encoded bytes when the
|
||||
// server's clipboard image (PNG / CF_DIB) arrives.
|
||||
onRemoteClipboardImage func(png []byte)
|
||||
|
||||
// getLocalClipboardImage is called to retrieve the current local
|
||||
// clipboard image (PNG-encoded) when the server requests the "PNG"
|
||||
// registered format. Returns nil when no image is available.
|
||||
getLocalClipboardImage func() []byte
|
||||
|
||||
// onRemoteClipboardHTML is called with the HTML content when the server's
|
||||
// clipboard "HTML Format" data arrives (fragment already extracted from
|
||||
// the CF_HTML envelope).
|
||||
onRemoteClipboardHTML func(html string)
|
||||
|
||||
// getLocalClipboardHTML is called to retrieve the current local clipboard
|
||||
// HTML (raw fragment/document, without the CF_HTML envelope) when the
|
||||
// server requests the "HTML Format" registered format.
|
||||
// Returns "" when no HTML is available.
|
||||
getLocalClipboardHTML func() string
|
||||
|
||||
// remoteHTMLFormatId 记录服务器 Format List 中 "HTML Format" 的格式 ID,
|
||||
// 用于识别 Format Data Response(服务端注册格式的 ID 由服务端分配)。
|
||||
remoteHTMLFormatId uint32
|
||||
|
||||
// serverDropEffectId 记录服务器 Format List 中 "Preferred DropEffect"
|
||||
// 的服务器本地 ID(注册格式 ID 各端自定,按名称对齐)。
|
||||
serverDropEffectId uint32
|
||||
|
||||
// lastRequestedFormat 记录我方最后一次 Format Data Request 的格式,
|
||||
// 以正确解读 Format Data Response(PNG 原样、DIB 转码、文本按 UTF-16)。
|
||||
lastRequestedFormat uint32
|
||||
|
||||
// suppressNextLocalChange prevents an echo loop:
|
||||
// server→client clipboard update triggers a local clipboard change
|
||||
// event which would otherwise be sent back to the server.
|
||||
suppressNextLocalChange bool
|
||||
|
||||
// --- File clipboard (CF_HDROP + FileContentsRequest/Response) -----------
|
||||
//
|
||||
// Browser clipboards cannot hold files, so files are "staged" explicitly
|
||||
// by the UI (file picker / drag-drop): staged files are advertised as
|
||||
// CF_HDROP in Format List; the server fetches their bytes through
|
||||
// FileContentsRequest. Files copied on the server arrive as a CF_HDROP
|
||||
// path list, and the UI pulls bytes per file via RequestRemoteFile.
|
||||
|
||||
mu sync.Mutex
|
||||
// serverFileClip is set when the server advertised CB_STREAM_FILECLIP_ENABLED.
|
||||
// CF_HDROP is only advertised/requested when both sides support file streaming.
|
||||
serverFileClip bool
|
||||
// localFiles are files staged by the local UI, advertised as CF_HDROP.
|
||||
localFiles []LocalFile
|
||||
// remoteFiles are the paths from the server's last file list payload.
|
||||
remoteFiles []string
|
||||
// remoteFileSizes are the sizes from the server's FileGroupDescriptorW
|
||||
// (-1 when unknown; only valid alongside remoteFiles from descriptors).
|
||||
remoteFileSizes []int64
|
||||
// streams tracks in-flight outgoing FileContentsRequest transfers
|
||||
// (remote → local download) keyed by our StreamId.
|
||||
streams map[uint32]*fileStream
|
||||
// nextStreamID is the StreamId counter for outgoing FileContentsRequests.
|
||||
nextStreamID uint32
|
||||
|
||||
onRemoteFiles func(names []string)
|
||||
onFileData func(index int, name string, data []byte)
|
||||
onFileProgress func(index int, received, total int64)
|
||||
}
|
||||
|
||||
// NewHandler creates a CliprdrHandler.
|
||||
//
|
||||
// - onRemote is called when the server clipboard text is received.
|
||||
// - getLocal is called to retrieve the current local clipboard text.
|
||||
//
|
||||
// Either callback may be nil. Image callbacks can be attached later via
|
||||
// SetImageCallbacks.
|
||||
func NewHandler(onRemote func(text string), getLocal func() string) *CliprdrHandler {
|
||||
return &CliprdrHandler{
|
||||
onRemoteClipboardChanged: onRemote,
|
||||
getLocalClipboardText: getLocal,
|
||||
streams: map[uint32]*fileStream{},
|
||||
}
|
||||
}
|
||||
|
||||
// SetImageCallbacks attaches image clipboard callbacks (both optional).
|
||||
//
|
||||
// - onRemoteImage receives PNG-encoded bytes of the server clipboard image.
|
||||
// - getLocalImage returns PNG-encoded bytes of the local clipboard image
|
||||
// (nil when unavailable), used to answer server requests for "PNG".
|
||||
func (h *CliprdrHandler) SetImageCallbacks(onRemoteImage func(png []byte), getLocalImage func() []byte) {
|
||||
h.onRemoteClipboardImage = onRemoteImage
|
||||
h.getLocalClipboardImage = getLocalImage
|
||||
}
|
||||
|
||||
// SetHTMLCallbacks attaches HTML clipboard callbacks (both optional).
|
||||
//
|
||||
// - onRemoteHTML receives the HTML content when the server's clipboard
|
||||
// "HTML Format" data arrives (fragment extracted from the CF_HTML envelope).
|
||||
// - getLocalHTML returns the local clipboard HTML (raw HTML, no CF_HTML
|
||||
// envelope; empty when unavailable), used to answer server requests for
|
||||
// the "HTML Format" registered format.
|
||||
func (h *CliprdrHandler) SetHTMLCallbacks(onRemoteHTML func(html string), getLocalHTML func() string) {
|
||||
h.onRemoteClipboardHTML = onRemoteHTML
|
||||
h.getLocalClipboardHTML = getLocalHTML
|
||||
}
|
||||
|
||||
// --- plugin.ChannelTransport interface ------------------------------------
|
||||
|
||||
func (h *CliprdrHandler) GetType() (string, uint32) {
|
||||
return ChannelName, ChannelOption
|
||||
}
|
||||
|
||||
func (h *CliprdrHandler) Sender(f core.ChannelSender) {
|
||||
h.channelSender = f
|
||||
}
|
||||
|
||||
// Process handles a reassembled CLIPRDR PDU from the server.
|
||||
func (h *CliprdrHandler) Process(s []byte) {
|
||||
if len(s) < 8 {
|
||||
return
|
||||
}
|
||||
r := bytes.NewReader(s)
|
||||
msgType, _ := core.ReadUint16LE(r)
|
||||
msgFlags, _ := core.ReadUint16LE(r)
|
||||
dataLen, _ := core.ReadUInt32LE(r)
|
||||
|
||||
body := make([]byte, dataLen)
|
||||
if dataLen > 0 {
|
||||
n, _ := r.Read(body)
|
||||
body = body[:n]
|
||||
}
|
||||
|
||||
slog.Debug("cliprdr recv", "msgType", msgType, "msgFlags", msgFlags, "dataLen", dataLen)
|
||||
|
||||
switch msgType {
|
||||
case CB_CLIP_CAPS:
|
||||
h.processClipCaps(body)
|
||||
case CB_MONITOR_READY:
|
||||
h.processMonitorReady()
|
||||
case CB_FORMAT_LIST:
|
||||
h.processFormatList(body, msgFlags)
|
||||
case CB_FORMAT_LIST_RESPONSE:
|
||||
h.processFormatListResponse(msgFlags)
|
||||
case CB_FORMAT_DATA_REQUEST:
|
||||
h.processFormatDataRequest(body)
|
||||
case CB_FORMAT_DATA_RESPONSE:
|
||||
h.processFormatDataResponse(body, msgFlags)
|
||||
case CB_FILECONTENTS_REQUEST:
|
||||
h.processFileContentsRequest(body)
|
||||
case CB_FILECONTENTS_RESPONSE:
|
||||
h.processFileContentsResponse(body, msgFlags)
|
||||
case CB_LOCK_CLIPDATA, CB_UNLOCK_CLIPDATA:
|
||||
// ignored
|
||||
default:
|
||||
slog.Debug("cliprdr: unhandled msgType", "msgType", msgType)
|
||||
}
|
||||
}
|
||||
|
||||
// --- Clipboard Capabilities (MS-RDPECLIP 2.2.2.1) -------------------------
|
||||
|
||||
func (h *CliprdrHandler) processClipCaps(body []byte) {
|
||||
if len(body) < 4 {
|
||||
return
|
||||
}
|
||||
cCapSets := binary.LittleEndian.Uint16(body[0:2])
|
||||
// pad1 at [2:4]
|
||||
offset := 4
|
||||
for i := 0; i < int(cCapSets); i++ {
|
||||
if offset+4 > len(body) {
|
||||
break
|
||||
}
|
||||
capType := binary.LittleEndian.Uint16(body[offset:])
|
||||
capLen := binary.LittleEndian.Uint16(body[offset+2:])
|
||||
if capType == CB_CAPSTYPE_GENERAL && capLen >= 12 {
|
||||
generalFlags := binary.LittleEndian.Uint32(body[offset+8:])
|
||||
h.useLongFormatNames = generalFlags&CB_USE_LONG_FORMAT_NAMES != 0
|
||||
h.mu.Lock()
|
||||
h.serverFileClip = generalFlags&CB_STREAM_FILECLIP_ENABLED != 0
|
||||
fileClip := h.serverFileClip
|
||||
h.mu.Unlock()
|
||||
slog.Debug("cliprdr: server caps", "generalFlags", generalFlags,
|
||||
"longNames", h.useLongFormatNames, "fileClip", fileClip)
|
||||
}
|
||||
offset += int(capLen)
|
||||
}
|
||||
h.serverCapsReceived = true
|
||||
// If CB_MONITOR_READY already arrived before CB_CLIP_CAPS (non-standard
|
||||
// ordering), send the FORMAT_LIST now that we have correct capabilities.
|
||||
if h.monitorReady {
|
||||
h.sendFormatList()
|
||||
}
|
||||
}
|
||||
|
||||
func (h *CliprdrHandler) sendClipCaps() {
|
||||
b := &bytes.Buffer{}
|
||||
// General capability set: type(2) + length(2) + version(4) + flags(4)
|
||||
binary.Write(b, binary.LittleEndian, uint16(CB_CAPSTYPE_GENERAL))
|
||||
binary.Write(b, binary.LittleEndian, uint16(12))
|
||||
binary.Write(b, binary.LittleEndian, uint32(CB_CAPS_VERSION_2))
|
||||
// CB_STREAM_FILECLIP_ENABLED + CB_FILECLIP_NO_FILE_PATHS:声明支持
|
||||
// FileGroupDescriptorW/FileContents 流式文件传输(mstsc 同款),
|
||||
// 服务器未开时其会忽略这些位。
|
||||
binary.Write(b, binary.LittleEndian,
|
||||
uint32(CB_USE_LONG_FORMAT_NAMES|CB_STREAM_FILECLIP_ENABLED|CB_FILECLIP_NO_FILE_PATHS))
|
||||
|
||||
// cCapabilitySets(2) + pad1(2) + capabilitySet
|
||||
body := &bytes.Buffer{}
|
||||
binary.Write(body, binary.LittleEndian, uint16(1))
|
||||
binary.Write(body, binary.LittleEndian, uint16(0))
|
||||
body.Write(b.Bytes())
|
||||
|
||||
h.sendPDU(CB_CLIP_CAPS, 0, body.Bytes())
|
||||
}
|
||||
|
||||
// --- Monitor Ready (MS-RDPECLIP 2.2.2.2) ----------------------------------
|
||||
|
||||
func (h *CliprdrHandler) processMonitorReady() {
|
||||
slog.Debug("cliprdr: server Monitor Ready")
|
||||
h.monitorReady = true
|
||||
h.sendClipCaps()
|
||||
// Per MS-RDPECLIP §1.3.2.1 the server sends CB_CLIP_CAPS before
|
||||
// CB_MONITOR_READY. Only send FORMAT_LIST after server caps are known
|
||||
// so useLongFormatNames is set correctly. If CB_CLIP_CAPS hasn't been
|
||||
// received yet (non-standard ordering), defer until processClipCaps fires.
|
||||
if h.serverCapsReceived {
|
||||
h.sendFormatList()
|
||||
}
|
||||
}
|
||||
|
||||
// --- Format List (MS-RDPECLIP 2.2.3.1) ------------------------------------
|
||||
|
||||
func (h *CliprdrHandler) sendFormatList() {
|
||||
h.mu.Lock()
|
||||
staged := len(h.localFiles) > 0
|
||||
fileClip := h.serverFileClip
|
||||
h.mu.Unlock()
|
||||
// 仅当双方都声明 CB_STREAM_FILECLIP_ENABLED 且本地已暂存文件时
|
||||
// 才广告 CF_HDROP,避免服务器把普通粘贴当文件处理。
|
||||
advertiseFiles := staged && fileClip
|
||||
|
||||
b := &bytes.Buffer{}
|
||||
if h.useLongFormatNames {
|
||||
// Long Format Name: formatId(4) + wszFormatName(null-terminated UTF-16LE)
|
||||
writeFormat := func(id uint32, name string) {
|
||||
binary.Write(b, binary.LittleEndian, id)
|
||||
b.Write(encodeUTF16LE(name))
|
||||
b.Write([]byte{0, 0}) // null terminator
|
||||
}
|
||||
writeFormat(CF_UNICODETEXT, "") // 空名 = 标准格式
|
||||
writeFormat(CF_PNG, FormatNamePNG) // 注册格式 "PNG"
|
||||
writeFormat(CF_HTML_FORMAT_ID, FormatNameHTML) // 注册格式 "HTML Format"
|
||||
if advertiseFiles {
|
||||
// 文件传输三件套(mstsc 同款,名称必须一致):
|
||||
// 描述符列表 + 流式内容 + 复制语义
|
||||
writeFormat(CF_FILE_GROUP_DESCRIPTORW, FormatNameFileGroupDescriptorW)
|
||||
writeFormat(CF_FILE_CONTENTS, FormatNameFileContents)
|
||||
writeFormat(CF_DROP_EFFECT, FormatNameDropEffect)
|
||||
}
|
||||
} else {
|
||||
// Short Format Name: formatId(4) + formatName[32]
|
||||
writeShort := func(id uint32, name string) {
|
||||
binary.Write(b, binary.LittleEndian, id)
|
||||
nameBuf := make([]byte, 32)
|
||||
copy(nameBuf, name)
|
||||
b.Write(nameBuf)
|
||||
}
|
||||
writeShort(CF_UNICODETEXT, "")
|
||||
writeShort(CF_PNG, FormatNamePNG)
|
||||
writeShort(CF_HTML_FORMAT_ID, FormatNameHTML)
|
||||
if advertiseFiles {
|
||||
writeShort(CF_FILE_GROUP_DESCRIPTORW, FormatNameFileGroupDescriptorW)
|
||||
writeShort(CF_FILE_CONTENTS, FormatNameFileContents)
|
||||
writeShort(CF_DROP_EFFECT, FormatNameDropEffect)
|
||||
}
|
||||
}
|
||||
h.sendPDU(CB_FORMAT_LIST, 0, b.Bytes())
|
||||
}
|
||||
|
||||
func (h *CliprdrHandler) processFormatList(body []byte, msgFlags uint16) {
|
||||
formats := h.parseFormatList(body, msgFlags)
|
||||
slog.Debug("cliprdr: server Format List", "formats", formats)
|
||||
|
||||
// 记录服务器侧 "Preferred DropEffect" 的本地 ID(注册格式 ID 各端自定)
|
||||
for _, f := range formats {
|
||||
if strings.EqualFold(f.FormatName, FormatNameDropEffect) {
|
||||
h.mu.Lock()
|
||||
h.serverDropEffectId = f.FormatId
|
||||
h.mu.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
// Always respond OK
|
||||
h.sendPDU(CB_FORMAT_LIST_RESPONSE, CB_RESPONSE_OK, nil)
|
||||
|
||||
// 请求优先级:文件描述符(远端复制了文件)> CF_HDROP > PNG > CF_DIB >
|
||||
// HTML > 文本。资源管理器复制文件时通常还附带文件名文本,文件列表
|
||||
// 必须最先识别。
|
||||
for _, f := range formats {
|
||||
if strings.EqualFold(f.FormatName, FormatNameFileGroupDescriptorW) {
|
||||
h.mu.Lock()
|
||||
fileClip := h.serverFileClip
|
||||
h.mu.Unlock()
|
||||
if fileClip {
|
||||
slog.Debug("cliprdr: server offers files (FileGroupDescriptorW)")
|
||||
h.lastRequestedFormat = CF_FILE_GROUP_DESCRIPTORW
|
||||
h.sendFormatDataRequest(f.FormatId)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, f := range formats {
|
||||
if f.FormatId == uint32(CF_HDROP) || strings.EqualFold(f.FormatName, FormatNameHDrop) {
|
||||
h.mu.Lock()
|
||||
fileClip := h.serverFileClip
|
||||
h.mu.Unlock()
|
||||
if fileClip {
|
||||
slog.Debug("cliprdr: server offers files (CF_HDROP)")
|
||||
h.lastRequestedFormat = CF_HDROP
|
||||
h.sendFormatDataRequest(CF_HDROP)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, f := range formats {
|
||||
if f.FormatId == CF_PNG || strings.EqualFold(f.FormatName, FormatNamePNG) ||
|
||||
strings.EqualFold(f.FormatName, "image/png") {
|
||||
h.lastRequestedFormat = f.FormatId
|
||||
h.sendFormatDataRequest(f.FormatId)
|
||||
return
|
||||
}
|
||||
}
|
||||
for _, f := range formats {
|
||||
if f.FormatId == CF_DIB {
|
||||
h.lastRequestedFormat = CF_DIB
|
||||
h.sendFormatDataRequest(CF_DIB)
|
||||
return
|
||||
}
|
||||
}
|
||||
// HTML Format:远端复制的富文本(浏览器/Office),保格式优于纯文本
|
||||
for _, f := range formats {
|
||||
if f.FormatId == CF_HTML_FORMAT_ID || strings.EqualFold(f.FormatName, FormatNameHTML) {
|
||||
h.remoteHTMLFormatId = f.FormatId
|
||||
h.lastRequestedFormat = f.FormatId
|
||||
h.sendFormatDataRequest(f.FormatId)
|
||||
return
|
||||
}
|
||||
}
|
||||
for _, f := range formats {
|
||||
if f.FormatId == CF_UNICODETEXT {
|
||||
h.lastRequestedFormat = CF_UNICODETEXT
|
||||
h.sendFormatDataRequest(CF_UNICODETEXT)
|
||||
return
|
||||
}
|
||||
}
|
||||
for _, f := range formats {
|
||||
if f.FormatId == CF_TEXT {
|
||||
h.lastRequestedFormat = CF_TEXT
|
||||
h.sendFormatDataRequest(CF_TEXT)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (h *CliprdrHandler) parseFormatList(body []byte, msgFlags uint16) []CliprdrFormat {
|
||||
var formats []CliprdrFormat
|
||||
if h.useLongFormatNames && (msgFlags&CB_ASCII_NAMES == 0) {
|
||||
// Long Format Names (MS-RDPECLIP 2.2.3.1.1.1)
|
||||
offset := 0
|
||||
for offset+4 <= len(body) {
|
||||
fmtId := binary.LittleEndian.Uint32(body[offset:])
|
||||
offset += 4
|
||||
// Read null-terminated UTF-16LE string
|
||||
nameEnd := offset
|
||||
for nameEnd+1 < len(body) {
|
||||
if body[nameEnd] == 0 && body[nameEnd+1] == 0 {
|
||||
break
|
||||
}
|
||||
nameEnd += 2
|
||||
}
|
||||
name := decodeUTF16LE(body[offset:nameEnd])
|
||||
offset = nameEnd + 2
|
||||
formats = append(formats, CliprdrFormat{fmtId, name})
|
||||
}
|
||||
} else {
|
||||
// Short Format Names (MS-RDPECLIP 2.2.3.1.1.2)
|
||||
offset := 0
|
||||
for offset+36 <= len(body) {
|
||||
fmtId := binary.LittleEndian.Uint32(body[offset:])
|
||||
nameBytes := body[offset+4 : offset+36]
|
||||
var name string
|
||||
if msgFlags&CB_ASCII_NAMES != 0 {
|
||||
name = strings.TrimRight(string(nameBytes), "\x00")
|
||||
} else {
|
||||
name = decodeUTF16LE(nameBytes)
|
||||
name = strings.TrimRight(name, "\x00")
|
||||
}
|
||||
formats = append(formats, CliprdrFormat{fmtId, name})
|
||||
offset += 36
|
||||
}
|
||||
}
|
||||
return formats
|
||||
}
|
||||
|
||||
func (h *CliprdrHandler) processFormatListResponse(msgFlags uint16) {
|
||||
if msgFlags&CB_RESPONSE_OK != 0 {
|
||||
slog.Debug("cliprdr: Format List Response OK")
|
||||
} else {
|
||||
slog.Warn("cliprdr: Format List Response FAIL")
|
||||
}
|
||||
}
|
||||
|
||||
// --- Format Data Request / Response (MS-RDPECLIP 2.2.5) --------------------
|
||||
|
||||
func (h *CliprdrHandler) sendFormatDataRequest(formatId uint32) {
|
||||
b := make([]byte, 4)
|
||||
binary.LittleEndian.PutUint32(b, formatId)
|
||||
h.sendPDU(CB_FORMAT_DATA_REQUEST, 0, b)
|
||||
slog.Debug("cliprdr: sent Format Data Request", "formatId", formatId)
|
||||
}
|
||||
|
||||
func (h *CliprdrHandler) processFormatDataRequest(body []byte) {
|
||||
if len(body) < 4 {
|
||||
h.sendPDU(CB_FORMAT_DATA_RESPONSE, CB_RESPONSE_FAIL, nil)
|
||||
return
|
||||
}
|
||||
requestedFormat := binary.LittleEndian.Uint32(body[0:4])
|
||||
slog.Debug("cliprdr: server requests format", "formatId", requestedFormat)
|
||||
|
||||
switch requestedFormat {
|
||||
case CF_PNG:
|
||||
var png []byte
|
||||
if h.getLocalClipboardImage != nil {
|
||||
png = h.getLocalClipboardImage()
|
||||
}
|
||||
if len(png) == 0 {
|
||||
h.sendPDU(CB_FORMAT_DATA_RESPONSE, CB_RESPONSE_FAIL, nil)
|
||||
return
|
||||
}
|
||||
h.sendPDU(CB_FORMAT_DATA_RESPONSE, CB_RESPONSE_OK, png)
|
||||
case CF_HTML_FORMAT_ID:
|
||||
// 本地 HTML(无 CF_HTML 信封)→ 包装成 "HTML Format" 字节流
|
||||
html := ""
|
||||
if h.getLocalClipboardHTML != nil {
|
||||
html = h.getLocalClipboardHTML()
|
||||
}
|
||||
if html == "" {
|
||||
h.sendPDU(CB_FORMAT_DATA_RESPONSE, CB_RESPONSE_FAIL, nil)
|
||||
return
|
||||
}
|
||||
h.sendPDU(CB_FORMAT_DATA_RESPONSE, CB_RESPONSE_OK, EncodeCFHTML(html))
|
||||
case CF_FILE_GROUP_DESCRIPTORW:
|
||||
// 远端粘贴文件:回 FILE_GROUP_DESCRIPTORW(名称+大小+属性),
|
||||
// 字节内容由服务器随后以 CB_FILECONTENTS_REQUEST 按 lIndex 拉取
|
||||
h.mu.Lock()
|
||||
files := h.localFiles
|
||||
h.mu.Unlock()
|
||||
if len(files) == 0 {
|
||||
h.sendPDU(CB_FORMAT_DATA_RESPONSE, CB_RESPONSE_FAIL, nil)
|
||||
return
|
||||
}
|
||||
h.sendPDU(CB_FORMAT_DATA_RESPONSE, CB_RESPONSE_OK, BuildFileGroupDescriptorW(files))
|
||||
case CF_FILE_CONTENTS:
|
||||
// FileContents 的具体字节一律走 CB_FILECONTENTS_REQUEST(含流式
|
||||
// 偏移/长度),对整格式 Format Data Request 只能回答不可流式的空体
|
||||
h.sendPDU(CB_FORMAT_DATA_RESPONSE, CB_RESPONSE_FAIL, nil)
|
||||
case CF_DROP_EFFECT:
|
||||
// 有暂存文件时回答 DROPEFFECT_COPY(否则 FAIL)。
|
||||
// 该查询决定资源管理器右键菜单"粘贴"是否可用。
|
||||
h.mu.Lock()
|
||||
hasFiles := len(h.localFiles) > 0
|
||||
h.mu.Unlock()
|
||||
if !hasFiles {
|
||||
h.sendPDU(CB_FORMAT_DATA_RESPONSE, CB_RESPONSE_FAIL, nil)
|
||||
return
|
||||
}
|
||||
h.sendPDU(CB_FORMAT_DATA_RESPONSE, CB_RESPONSE_OK, buildDropEffectReply())
|
||||
case CF_UNICODETEXT:
|
||||
text := ""
|
||||
if h.getLocalClipboardText != nil {
|
||||
text = h.getLocalClipboardText()
|
||||
}
|
||||
h.sendPDU(CB_FORMAT_DATA_RESPONSE, CB_RESPONSE_OK, encodeUTF16LE(text+"\x00"))
|
||||
case CF_TEXT:
|
||||
text := ""
|
||||
if h.getLocalClipboardText != nil {
|
||||
text = h.getLocalClipboardText()
|
||||
}
|
||||
h.sendPDU(CB_FORMAT_DATA_RESPONSE, CB_RESPONSE_OK, []byte(text+"\x00"))
|
||||
default:
|
||||
// 注册格式 ID 各端自定:服务器会用它本地的 "Preferred DropEffect"
|
||||
// ID 查询(不一定先通告)。该查询决定资源管理器"粘贴"是否可用。
|
||||
h.mu.Lock()
|
||||
serverDropEffect := h.serverDropEffectId
|
||||
hasFiles := len(h.localFiles) > 0
|
||||
h.mu.Unlock()
|
||||
|
||||
// DropEffect 应答:精确命中服务器 ID,或未知注册格式 ID(≥0xC000)
|
||||
// 且有暂存文件时按 DropEffect 猜测应答
|
||||
if (serverDropEffect != 0 && requestedFormat == serverDropEffect) ||
|
||||
(requestedFormat >= 0xC000 && hasFiles) {
|
||||
if !hasFiles {
|
||||
h.sendPDU(CB_FORMAT_DATA_RESPONSE, CB_RESPONSE_FAIL, nil)
|
||||
return
|
||||
}
|
||||
slog.Debug("cliprdr: replying DropEffect", "formatId", requestedFormat)
|
||||
h.sendPDU(CB_FORMAT_DATA_RESPONSE, CB_RESPONSE_OK, buildDropEffectReply())
|
||||
return
|
||||
}
|
||||
h.sendPDU(CB_FORMAT_DATA_RESPONSE, CB_RESPONSE_FAIL, nil)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *CliprdrHandler) processFormatDataResponse(body []byte, msgFlags uint16) {
|
||||
if msgFlags&CB_RESPONSE_OK == 0 {
|
||||
slog.Warn("cliprdr: Format Data Response FAIL")
|
||||
return
|
||||
}
|
||||
|
||||
// 按我方请求的格式解读响应
|
||||
switch {
|
||||
case h.lastRequestedFormat == CF_FILE_GROUP_DESCRIPTORW:
|
||||
names, sizes, err := ParseFileGroupDescriptorW(body)
|
||||
if err != nil {
|
||||
slog.Warn("cliprdr: FileGroupDescriptorW parse failed", "err", err, "len", len(body))
|
||||
return
|
||||
}
|
||||
h.mu.Lock()
|
||||
h.remoteFiles = names
|
||||
h.remoteFileSizes = sizes
|
||||
h.mu.Unlock()
|
||||
if h.onRemoteFiles != nil {
|
||||
h.onRemoteFiles(names)
|
||||
}
|
||||
case h.lastRequestedFormat == CF_HDROP:
|
||||
names, err := ParseDropfiles(body)
|
||||
if err != nil {
|
||||
slog.Warn("cliprdr: CF_HDROP parse failed", "err", err, "len", len(body))
|
||||
return
|
||||
}
|
||||
h.mu.Lock()
|
||||
h.remoteFiles = names
|
||||
h.mu.Unlock()
|
||||
if h.onRemoteFiles != nil {
|
||||
h.onRemoteFiles(names)
|
||||
}
|
||||
case h.lastRequestedFormat != 0 && h.lastRequestedFormat == h.remoteHTMLFormatId:
|
||||
// "HTML Format"(服务端注册格式的 ID 与请求时记录的一致)
|
||||
html := DecodeCFHTML(body)
|
||||
if html == "" {
|
||||
return
|
||||
}
|
||||
if h.onRemoteClipboardHTML != nil {
|
||||
h.suppressNextLocalChange = true
|
||||
h.onRemoteClipboardHTML(html)
|
||||
}
|
||||
case h.lastRequestedFormat == CF_PNG:
|
||||
if len(body) == 0 {
|
||||
return
|
||||
}
|
||||
if h.onRemoteClipboardImage != nil {
|
||||
h.suppressNextLocalChange = true
|
||||
png := make([]byte, len(body))
|
||||
copy(png, body)
|
||||
h.onRemoteClipboardImage(png)
|
||||
}
|
||||
case h.lastRequestedFormat == CF_DIB || h.lastRequestedFormat == CF_DIBV5:
|
||||
png, err := DIBToPNG(body)
|
||||
if err != nil {
|
||||
slog.Warn("cliprdr: DIB→PNG conversion failed", "err", err)
|
||||
return
|
||||
}
|
||||
if h.onRemoteClipboardImage != nil {
|
||||
h.suppressNextLocalChange = true
|
||||
h.onRemoteClipboardImage(png)
|
||||
}
|
||||
default:
|
||||
// CF_UNICODETEXT / CF_TEXT → UTF-16LE 文本
|
||||
text := decodeUTF16LE(body)
|
||||
text = strings.TrimRight(text, "\x00")
|
||||
if text != "" && h.onRemoteClipboardChanged != nil {
|
||||
slog.Debug("cliprdr: received text", "len", len(text))
|
||||
h.suppressNextLocalChange = true
|
||||
h.onRemoteClipboardChanged(text)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Public API for local clipboard changes --------------------------------
|
||||
|
||||
// OnLocalClipboardChanged notifies the server that the local clipboard
|
||||
// content has changed. Call this from the UI when the system clipboard
|
||||
// changes (e.g. via polling or a platform clipboard-change signal).
|
||||
func (h *CliprdrHandler) OnLocalClipboardChanged() {
|
||||
if h.suppressNextLocalChange {
|
||||
h.suppressNextLocalChange = false
|
||||
return
|
||||
}
|
||||
if h.channelSender != nil {
|
||||
h.sendFormatList()
|
||||
slog.Debug("cliprdr: local clipboard changed, sent Format List")
|
||||
}
|
||||
}
|
||||
|
||||
// --- Send helpers ----------------------------------------------------------
|
||||
|
||||
func (h *CliprdrHandler) sendPDU(msgType, msgFlags uint16, body []byte) {
|
||||
if h.channelSender == nil {
|
||||
return
|
||||
}
|
||||
sendClipPDU(h.channelSender, msgType, msgFlags, body)
|
||||
}
|
||||
|
||||
// --- UTF-16LE helpers ------------------------------------------------------
|
||||
|
||||
func decodeUTF16LE(b []byte) string {
|
||||
if len(b) < 2 {
|
||||
return ""
|
||||
}
|
||||
// Trim to even length
|
||||
if len(b)%2 != 0 {
|
||||
b = b[:len(b)-1]
|
||||
}
|
||||
u16 := make([]uint16, len(b)/2)
|
||||
for i := range u16 {
|
||||
u16[i] = binary.LittleEndian.Uint16(b[i*2:])
|
||||
}
|
||||
return string(utf16.Decode(u16))
|
||||
}
|
||||
|
||||
func encodeUTF16LE(s string) []byte {
|
||||
runes := []rune(s)
|
||||
u16 := utf16.Encode(runes)
|
||||
b := make([]byte, len(u16)*2)
|
||||
for i, v := range u16 {
|
||||
binary.LittleEndian.PutUint16(b[i*2:], v)
|
||||
}
|
||||
return b
|
||||
}
|
||||
Reference in New Issue
Block a user