init: 自 zomaintain/backend/rdplib 平移独立成库; module path 从上游 github.com/nakagami/grdp 改为 git.zeroonesoft.cn/golib/rdplib

This commit is contained in:
w11
2026-09-19 18:55:43 +08:00
commit 1096fbce9d
94 changed files with 36790 additions and 0 deletions
+479
View File
@@ -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
}