init: 自 zomaintain/backend/rdplib 平移独立成库; module path 从上游 github.com/nakagami/grdp 改为 git.zeroonesoft.cn/golib/rdplib
This commit is contained in:
@@ -0,0 +1,479 @@
|
||||
package cliprdr
|
||||
|
||||
// file_clip.go implements the MS-RDPECLIP file transfer sequences
|
||||
// (§3.1.5.4.4 Copy File Sequence / §3.1.5.4.5 File Transfer Data Sequence):
|
||||
//
|
||||
// - Server → client (files copied in the remote session): the server's
|
||||
// Format List contains CF_HDROP; we fetch the path list via a Format
|
||||
// Data Request, then each file's bytes via FileContentsRequest
|
||||
// (FILECONTENTS_SIZE, then FILECONTENTS_RANGE chunks).
|
||||
// - Client → server (files staged by the local UI): staged files are
|
||||
// advertised as CF_HDROP in our Format List; the server fetches the
|
||||
// DROPFILES path list via Format Data Request, then file bytes via
|
||||
// FileContentsRequest, which we answer from the staged buffers.
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"log/slog"
|
||||
"unicode/utf16"
|
||||
)
|
||||
|
||||
// CF_HDROP 是标准剪贴板格式 15(文件列表)。Windows 在 short/long 两种
|
||||
// 格式名模式下都按 ID 识别标准格式,名称可留空。
|
||||
const CF_HDROP = 15
|
||||
|
||||
// FormatNameHDrop 是部分实现(FreeRDP 等)在 short name 里使用的名称。
|
||||
const FormatNameHDrop = "Hdrop"
|
||||
|
||||
// CF_DROP_EFFECT 是客户端自定的 "Preferred DropEffect" 注册格式 ID
|
||||
// (>0xC000 即可,线上按名称匹配)。Windows 资源管理器判定文件粘贴是否
|
||||
// 可用时查询该格式;缺失或 FAIL 会导致右键菜单"粘贴"置灰。
|
||||
const CF_DROP_EFFECT = 0xC0C0
|
||||
|
||||
// FormatNameDropEffect 是 Windows 的 DropEffect 注册格式名。
|
||||
const FormatNameDropEffect = "Preferred DropEffect"
|
||||
|
||||
// DROPEFFECT_COPY 是 DropEffect 值:我们的文件提供方式是复制。
|
||||
const DROPEFFECT_COPY = 1
|
||||
|
||||
// MS-RDPECLIP 文件传输采用 FileGroupDescriptorW + FileContents 注册格式对
|
||||
// (配合 CB_FILECLIP_NO_FILE_PATHS 能力位,mstsc 同款)。ID 客户端自定,
|
||||
// 服务器按我们的 Format List 中通告的 ID 回查。
|
||||
const (
|
||||
CF_FILE_GROUP_DESCRIPTORW = 0xC0C6
|
||||
CF_FILE_CONTENTS = 0xC0C7
|
||||
)
|
||||
|
||||
// FormatNameFileGroupDescriptorW / FormatNameFileContents 是线上的注册格式名。
|
||||
const (
|
||||
FormatNameFileGroupDescriptorW = "FileGroupDescriptorW"
|
||||
FormatNameFileContents = "FileContents"
|
||||
)
|
||||
|
||||
// fileDescriptorSize 是 FILE_DESCRIPTOR 的线上字节数
|
||||
// (flags4 + clsid16 + sizl8 + pointl8 + attr4 + 3×time8 + sizeHi4 + sizeLo4 + name520)。
|
||||
const fileDescriptorSize = 592
|
||||
|
||||
// fileDescriptorNameBytes 是 cFileName 字段长度(260 WCHAR = 520 字节)。
|
||||
const fileDescriptorNameBytes = 520
|
||||
|
||||
// dropfilesHeaderLen 是 DROPFILES 结构长度(pFiles 4 + pt 8 + fNC 4 + fWide 4)。
|
||||
const dropfilesHeaderLen = 20
|
||||
|
||||
// fileRangeChunk 是我方主动拉取远端文件时的每段 RANGE 大小。服务器可能
|
||||
// 裁剪返回长度,接收端按"追加到累计长度"推进,不假设服务器返回整段。
|
||||
const fileRangeChunk = 256 * 1024
|
||||
|
||||
// maxDescriptorItems 防御性上限:一份描述符列表最多 65536 项(约 38MB
|
||||
// 载荷),超出视为协议错位。远端复制上万文件的目录仍被接受。
|
||||
const maxDescriptorItems = 65536
|
||||
|
||||
// maxFileTotalBytes 拒绝异常大的 SIZE 声明(>4GB 意味着协议解析已错位)。
|
||||
const maxFileTotalBytes = int64(4) << 30
|
||||
|
||||
// LocalFile is a file staged by the local UI for client → server transfer.
|
||||
type LocalFile struct {
|
||||
Name string
|
||||
Data []byte
|
||||
}
|
||||
|
||||
// fileStream tracks one in-flight remote → local transfer (our StreamId).
|
||||
type fileStream struct {
|
||||
index int
|
||||
name string
|
||||
total int64
|
||||
got int64
|
||||
buf []byte
|
||||
sizing bool // true until the FILECONTENTS_SIZE response arrives
|
||||
}
|
||||
|
||||
// --- Public API -------------------------------------------------------------
|
||||
|
||||
// SetFileCallbacks attaches file clipboard callbacks (all optional):
|
||||
//
|
||||
// - onRemoteFiles is called with the file names when the server's
|
||||
// clipboard holds files (CF_HDROP path list received).
|
||||
// - onFileData is called with the assembled bytes of one file after
|
||||
// RequestRemoteFile finishes; data is nil when the transfer failed.
|
||||
// - onFileProgress is called after every received chunk.
|
||||
func (h *CliprdrHandler) SetFileCallbacks(
|
||||
onRemoteFiles func(names []string),
|
||||
onFileData func(index int, name string, data []byte),
|
||||
onFileProgress func(index int, received, total int64),
|
||||
) {
|
||||
h.onRemoteFiles = onRemoteFiles
|
||||
h.onFileData = onFileData
|
||||
h.onFileProgress = onFileProgress
|
||||
}
|
||||
|
||||
// SetLocalFiles stages files as the local (client) clipboard file content and
|
||||
// advertises them to the server as CF_HDROP.
|
||||
func (h *CliprdrHandler) SetLocalFiles(files []LocalFile) {
|
||||
h.mu.Lock()
|
||||
h.localFiles = files
|
||||
h.mu.Unlock()
|
||||
// 暂存文件是真实的本地剪贴板变更(UI 主动动作),直接重发 Format List,
|
||||
// 不走 suppressNextLocalChange(它属于远端回显抑制语义)。
|
||||
if h.channelSender != nil {
|
||||
h.sendFormatList()
|
||||
slog.Debug("cliprdr: local files staged, sent Format List", "files", len(files))
|
||||
}
|
||||
}
|
||||
|
||||
// buildDropEffectReply 返回 "Preferred DropEffect" 的应答载荷
|
||||
// (4 字节 DWORD DROPEFFECT_COPY)。
|
||||
func buildDropEffectReply() []byte {
|
||||
b := make([]byte, 4)
|
||||
binary.LittleEndian.PutUint32(b, DROPEFFECT_COPY)
|
||||
return b
|
||||
}
|
||||
|
||||
// BuildFileGroupDescriptorW 将暂存文件编码为 FILE_GROUP_DESCRIPTORW 载荷
|
||||
// (MS-RDPECLIP 2.2.5.2.3.1):cItems + FILE_DESCRIPTOR[cItems]。
|
||||
func BuildFileGroupDescriptorW(files []LocalFile) []byte {
|
||||
b := &bytes.Buffer{}
|
||||
u32 := func(v uint32) { binary.Write(b, binary.LittleEndian, v) }
|
||||
u32(uint32(len(files)))
|
||||
for _, f := range files {
|
||||
u32(FD_ATTRIBUTES | FD_FILESIZE | FD_PROGRESSUI)
|
||||
b.Write(make([]byte, 16)) // clsid
|
||||
b.Write(make([]byte, 8)) // sizl
|
||||
b.Write(make([]byte, 8)) // pointl
|
||||
u32(FILE_ATTRIBUTE_ARCHIVE)
|
||||
b.Write(make([]byte, 24)) // creation/access/write time(未设时间标志,值无效)
|
||||
u32(uint32(uint64(len(f.Data)) >> 32))
|
||||
u32(uint32(len(f.Data)))
|
||||
name := make([]byte, fileDescriptorNameBytes)
|
||||
u16 := utf16.Encode([]rune(f.Name))
|
||||
for i := 0; i < len(u16) && 2*i+1 < fileDescriptorNameBytes-1; i++ {
|
||||
binary.LittleEndian.PutUint16(name[2*i:], u16[i])
|
||||
}
|
||||
b.Write(name)
|
||||
}
|
||||
return b.Bytes()
|
||||
}
|
||||
|
||||
// ParseFileGroupDescriptorW 解码服务器发来的 FILE_GROUP_DESCRIPTORW,
|
||||
// 返回文件名与文件大小。
|
||||
func ParseFileGroupDescriptorW(body []byte) ([]string, []int64, error) {
|
||||
if len(body) < 4 {
|
||||
return nil, nil, errors.New("cliprdr: FileGroupDescriptorW too short")
|
||||
}
|
||||
cItems := binary.LittleEndian.Uint32(body[0:])
|
||||
// 上限只做溢出/内存炸弹防护(uint32 项数 × 592B 的乘积合法性),
|
||||
// 实际约束是载荷长度必须恰好容纳 cItems 个描述符——远端复制含
|
||||
// 数千文件的目录是正常操作,不能按固定小数目拒绝。
|
||||
if cItems > maxDescriptorItems || len(body) < 4+int(cItems)*fileDescriptorSize {
|
||||
return nil, nil, errors.New("cliprdr: bad FileGroupDescriptorW cItems/length")
|
||||
}
|
||||
names := make([]string, 0, cItems)
|
||||
sizes := make([]int64, 0, cItems)
|
||||
for i := 0; i < int(cItems); i++ {
|
||||
d := body[4+i*fileDescriptorSize : (i+1)*fileDescriptorSize]
|
||||
flags := binary.LittleEndian.Uint32(d[0:])
|
||||
size := uint64(binary.LittleEndian.Uint32(d[68:])) |
|
||||
uint64(binary.LittleEndian.Uint32(d[64:]))<<32
|
||||
rawName := d[72:]
|
||||
// 260 WCHAR UTF-16LE,NUL 结尾
|
||||
end := 0
|
||||
for end+1 < len(rawName) {
|
||||
if rawName[end] == 0 && rawName[end+1] == 0 {
|
||||
break
|
||||
}
|
||||
end += 2
|
||||
}
|
||||
name := decodeUTF16LE(rawName[:end])
|
||||
if flags&FD_FILESIZE == 0 {
|
||||
size = ^uint64(0) // 大小未知,由 FileContentsRequest(SIZE) 探测
|
||||
}
|
||||
names = append(names, name)
|
||||
sizes = append(sizes, int64(size))
|
||||
}
|
||||
return names, sizes, nil
|
||||
}
|
||||
|
||||
// ClearLocalFiles removes staged files and re-advertises the Format List.
|
||||
func (h *CliprdrHandler) ClearLocalFiles() {
|
||||
h.mu.Lock()
|
||||
had := len(h.localFiles) > 0
|
||||
h.localFiles = nil
|
||||
h.mu.Unlock()
|
||||
if had && h.channelSender != nil {
|
||||
h.sendFormatList()
|
||||
}
|
||||
}
|
||||
|
||||
// RemoteFileNames returns the paths from the server's last CF_HDROP payload.
|
||||
func (h *CliprdrHandler) RemoteFileNames() []string {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
out := make([]string, len(h.remoteFiles))
|
||||
copy(out, h.remoteFiles)
|
||||
return out
|
||||
}
|
||||
|
||||
// RequestRemoteFile starts downloading file `index` (order within the last
|
||||
// file list) from the server. Progress goes to onFileProgress; the
|
||||
// assembled bytes go to onFileData(index, name, data) — data is nil on failure.
|
||||
func (h *CliprdrHandler) RequestRemoteFile(index int) error {
|
||||
h.mu.Lock()
|
||||
if index < 0 || index >= len(h.remoteFiles) {
|
||||
h.mu.Unlock()
|
||||
return errors.New("cliprdr: file index out of range")
|
||||
}
|
||||
name := h.remoteFiles[index]
|
||||
size := int64(-1)
|
||||
if index < len(h.remoteFileSizes) {
|
||||
size = h.remoteFileSizes[index]
|
||||
}
|
||||
h.nextStreamID++
|
||||
sid := h.nextStreamID
|
||||
h.mu.Unlock()
|
||||
|
||||
if size >= 0 {
|
||||
// FileGroupDescriptorW 已声明大小:直接从 0 开始拉 RANGE
|
||||
h.mu.Lock()
|
||||
h.streams[sid] = &fileStream{index: index, name: name, total: size}
|
||||
h.mu.Unlock()
|
||||
if size == 0 { // 零字节文件:无 RANGE 可发,直接完成
|
||||
h.processFileContentsResponse(u32le(sid), CB_RESPONSE_OK)
|
||||
return nil
|
||||
}
|
||||
h.sendFileContentsRequest(sid, uint32(index), FILECONTENTS_RANGE, 0, fileRangeChunk)
|
||||
slog.Debug("cliprdr: requesting remote file", "index", index, "name", name,
|
||||
"streamId", sid, "size", size)
|
||||
return nil
|
||||
}
|
||||
|
||||
h.mu.Lock()
|
||||
h.streams[sid] = &fileStream{index: index, name: name, sizing: true}
|
||||
h.mu.Unlock()
|
||||
h.sendFileContentsRequest(sid, uint32(index), FILECONTENTS_SIZE, 0, 8)
|
||||
slog.Debug("cliprdr: requesting remote file", "index", index, "name", name, "streamId", sid)
|
||||
return nil
|
||||
}
|
||||
|
||||
// --- Wire helpers -----------------------------------------------------------
|
||||
|
||||
// sendFileContentsRequest sends CB_FILECONTENTS_REQUEST (MS-RDPECLIP 2.2.5.2.3).
|
||||
func (h *CliprdrHandler) sendFileContentsRequest(streamId, lindex, dwFlags, posLow, cbRequested uint32) {
|
||||
b := make([]byte, 24)
|
||||
binary.LittleEndian.PutUint32(b[0:], streamId)
|
||||
binary.LittleEndian.PutUint32(b[4:], lindex)
|
||||
binary.LittleEndian.PutUint32(b[8:], dwFlags)
|
||||
binary.LittleEndian.PutUint32(b[12:], posLow)
|
||||
binary.LittleEndian.PutUint32(b[16:], 0) // NPositionHigh:4GB 内恒 0
|
||||
binary.LittleEndian.PutUint32(b[20:], cbRequested)
|
||||
h.sendPDU(CB_FILECONTENTS_REQUEST, 0, b)
|
||||
}
|
||||
|
||||
// processFileContentsRequest answers the server's request for staged file
|
||||
// bytes (client → server direction). Failure replies carry only StreamId.
|
||||
func (h *CliprdrHandler) processFileContentsRequest(body []byte) {
|
||||
if len(body) < 24 {
|
||||
slog.Warn("cliprdr: short FileContentsRequest", "len", len(body))
|
||||
return
|
||||
}
|
||||
streamId := binary.LittleEndian.Uint32(body[0:])
|
||||
lindex := binary.LittleEndian.Uint32(body[4:])
|
||||
dwFlags := binary.LittleEndian.Uint32(body[8:])
|
||||
pos := uint64(binary.LittleEndian.Uint32(body[12:])) | uint64(binary.LittleEndian.Uint32(body[16:]))<<32
|
||||
cbRequested := binary.LittleEndian.Uint32(body[20:])
|
||||
// body[24:28] 是 ClipDataId,仅在 CB_CAN_LOCK_CLIPDATA 协商后出现,此处忽略。
|
||||
|
||||
h.mu.Lock()
|
||||
files := h.localFiles
|
||||
h.mu.Unlock()
|
||||
|
||||
var data []byte
|
||||
if lindex < uint32(len(files)) {
|
||||
data = files[lindex].Data
|
||||
}
|
||||
if data == nil {
|
||||
slog.Warn("cliprdr: FileContentsRequest for unknown file", "lindex", lindex)
|
||||
h.sendPDU(CB_FILECONTENTS_RESPONSE, CB_RESPONSE_FAIL, u32le(streamId))
|
||||
return
|
||||
}
|
||||
|
||||
var payload []byte
|
||||
switch {
|
||||
case dwFlags&FILECONTENTS_SIZE != 0:
|
||||
payload = make([]byte, 8)
|
||||
binary.LittleEndian.PutUint64(payload, uint64(len(data)))
|
||||
case dwFlags&FILECONTENTS_RANGE != 0:
|
||||
start := pos
|
||||
end := pos + uint64(cbRequested)
|
||||
if end > uint64(len(data)) || end < start {
|
||||
end = uint64(len(data))
|
||||
}
|
||||
if start < uint64(len(data)) {
|
||||
payload = data[start:end]
|
||||
}
|
||||
default:
|
||||
slog.Warn("cliprdr: FileContentsRequest without SIZE/RANGE", "dwFlags", dwFlags)
|
||||
h.sendPDU(CB_FILECONTENTS_RESPONSE, CB_RESPONSE_FAIL, u32le(streamId))
|
||||
return
|
||||
}
|
||||
|
||||
out := make([]byte, 4+len(payload))
|
||||
binary.LittleEndian.PutUint32(out, streamId)
|
||||
copy(out[4:], payload)
|
||||
h.sendPDU(CB_FILECONTENTS_RESPONSE, CB_RESPONSE_OK, out)
|
||||
slog.Debug("cliprdr: served file contents", "lindex", lindex, "flags", dwFlags,
|
||||
"pos", pos, "len", len(payload))
|
||||
}
|
||||
|
||||
// processFileContentsResponse consumes the server's reply for one of our
|
||||
// outstanding RequestRemoteFile transfers, chaining RANGE requests until the
|
||||
// declared size has been received.
|
||||
func (h *CliprdrHandler) processFileContentsResponse(body []byte, msgFlags uint16) {
|
||||
if len(body) < 4 {
|
||||
return
|
||||
}
|
||||
streamId := binary.LittleEndian.Uint32(body[0:])
|
||||
data := body[4:]
|
||||
|
||||
h.mu.Lock()
|
||||
st, ok := h.streams[streamId]
|
||||
if !ok {
|
||||
h.mu.Unlock()
|
||||
slog.Debug("cliprdr: FileContentsResponse for unknown stream", "streamId", streamId)
|
||||
return
|
||||
}
|
||||
fail := msgFlags&CB_RESPONSE_OK == 0
|
||||
if fail {
|
||||
delete(h.streams, streamId)
|
||||
} else if st.sizing {
|
||||
// SIZE 响应固定 8 字节 uint64(部分实现只回 4 字节,向下兼容)
|
||||
if len(data) < 4 {
|
||||
delete(h.streams, streamId)
|
||||
fail = true
|
||||
} else {
|
||||
var size uint64
|
||||
if len(data) >= 8 {
|
||||
size = binary.LittleEndian.Uint64(data)
|
||||
} else {
|
||||
size = uint64(binary.LittleEndian.Uint32(data))
|
||||
}
|
||||
if size > uint64(maxFileTotalBytes) {
|
||||
slog.Warn("cliprdr: remote file size absurd", "size", size)
|
||||
delete(h.streams, streamId)
|
||||
fail = true
|
||||
} else {
|
||||
st.sizing = false
|
||||
st.total = int64(size)
|
||||
st.buf = make([]byte, 0, size)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// RANGE 响应:按实际到达长度追加(服务器允许裁剪返回段)
|
||||
st.buf = append(st.buf, data...)
|
||||
st.got = int64(len(st.buf))
|
||||
}
|
||||
index, name, got, total := st.index, st.name, st.got, st.total
|
||||
requestNext := false
|
||||
var nextPos uint32
|
||||
var finished []byte
|
||||
if !fail && !st.sizing {
|
||||
if st.got < st.total {
|
||||
requestNext = true
|
||||
nextPos = uint32(st.got) // 4GB 内;超出已由 maxFileTotalBytes 拒绝
|
||||
} else {
|
||||
// 传输完成(含零字节文件:SIZE 后即完成)——锁内取走缓冲防竞态
|
||||
finished = make([]byte, total)
|
||||
copy(finished, st.buf)
|
||||
delete(h.streams, streamId)
|
||||
}
|
||||
}
|
||||
h.mu.Unlock()
|
||||
|
||||
if fail {
|
||||
slog.Warn("cliprdr: file transfer failed", "name", name, "streamId", streamId)
|
||||
if h.onFileData != nil {
|
||||
h.onFileData(index, name, nil)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if h.onFileProgress != nil {
|
||||
h.onFileProgress(index, got, total)
|
||||
}
|
||||
|
||||
if requestNext {
|
||||
h.sendFileContentsRequest(streamId, uint32(index), FILECONTENTS_RANGE, nextPos, fileRangeChunk)
|
||||
return
|
||||
}
|
||||
|
||||
if finished != nil && h.onFileData != nil {
|
||||
slog.Debug("cliprdr: file transfer complete", "name", name, "bytes", total)
|
||||
h.onFileData(index, name, finished)
|
||||
}
|
||||
}
|
||||
|
||||
// --- DROPFILES (CF_HDROP format data, MS-RDPECLIP 2.2.5.2.4) ----------------
|
||||
|
||||
// ParseDropfiles decodes a CF_HDROP payload (DROPFILES header + null-
|
||||
// terminated path list, UTF-16LE when fWide==1, ASCII otherwise) into paths.
|
||||
func ParseDropfiles(body []byte) ([]string, error) {
|
||||
if len(body) < dropfilesHeaderLen {
|
||||
return nil, errors.New("cliprdr: DROPFILES body too short")
|
||||
}
|
||||
pFiles := binary.LittleEndian.Uint32(body[0:])
|
||||
fWide := binary.LittleEndian.Uint32(body[16:])
|
||||
if pFiles < dropfilesHeaderLen || int(pFiles) > len(body) {
|
||||
return nil, errors.New("cliprdr: bad DROPFILES pFiles offset")
|
||||
}
|
||||
list := body[pFiles:]
|
||||
var names []string
|
||||
if fWide == 1 {
|
||||
// 双 NUL 结尾的 UTF-16LE 路径列表
|
||||
start := 0
|
||||
for i := 0; i+1 < len(list); i += 2 {
|
||||
if list[i] == 0 && list[i+1] == 0 {
|
||||
if i == start { // 连续两个 NUL = 列表结束
|
||||
break
|
||||
}
|
||||
names = append(names, decodeUTF16LE(list[start:i]))
|
||||
start = i + 2
|
||||
}
|
||||
}
|
||||
} else {
|
||||
start := 0
|
||||
for i := 0; i < len(list); i++ {
|
||||
if list[i] == 0 {
|
||||
if i == start {
|
||||
break
|
||||
}
|
||||
names = append(names, string(list[start:i]))
|
||||
start = i + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
return names, nil
|
||||
}
|
||||
|
||||
// BuildDropfiles encodes names into a CF_HDROP payload (UTF-16LE, fWide=1).
|
||||
func BuildDropfiles(names []string) []byte {
|
||||
b := &bytes.Buffer{}
|
||||
u32 := func(v uint32) { binary.Write(b, binary.LittleEndian, v) }
|
||||
u32(dropfilesHeaderLen) // pFiles
|
||||
u32(0) // pt.x
|
||||
u32(0) // pt.y
|
||||
u32(0) // fNC
|
||||
u32(1) // fWide = Unicode
|
||||
for _, n := range names {
|
||||
b.Write(encodeUTF16LE(n))
|
||||
b.Write([]byte{0, 0}) // 路径 NUL 结尾
|
||||
}
|
||||
b.Write([]byte{0, 0}) // 列表终止双 NUL
|
||||
return b.Bytes()
|
||||
}
|
||||
|
||||
// u32le returns v as a 4-byte little-endian slice.
|
||||
func u32le(v uint32) []byte {
|
||||
b := make([]byte, 4)
|
||||
binary.LittleEndian.PutUint32(b, v)
|
||||
return b
|
||||
}
|
||||
Reference in New Issue
Block a user