Files

480 lines
16 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}