Files

528 lines
19 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
import (
"bytes"
"encoding/binary"
"fmt"
"testing"
)
// capturedPDU 是 fakeSender 捕获的一条完整 CLIPRDR PDU。
type capturedPDU struct {
msgType uint16
flags uint16
body []byte
}
// fakeSender 捕获所有发出通道的数据,供断言。
type fakeSender struct{ pdus []capturedPDU }
func (f *fakeSender) SendToChannel(_ string, data []byte) (int, error) {
cp := make([]byte, len(data))
copy(cp, data)
if len(cp) < 8 {
return len(cp), nil
}
f.pdus = append(f.pdus, capturedPDU{
msgType: binary.LittleEndian.Uint16(cp[0:]),
flags: binary.LittleEndian.Uint16(cp[2:]),
body: cp[8:],
})
return len(cp), nil
}
func (f *fakeSender) last() capturedPDU { return f.pdus[len(f.pdus)-1] }
// newFileTestHandler 构造带 fakeSender 且 serverFileClip=true 的 handler
// (模拟服务器 caps 已声明 CB_STREAM_FILECLIP_ENABLED)。
func newFileTestHandler(t *testing.T) (*CliprdrHandler, *fakeSender) {
t.Helper()
h := NewHandler(nil, nil)
fs := &fakeSender{}
h.Sender(fs)
caps := &bytes.Buffer{}
binary.Write(caps, binary.LittleEndian, uint16(1)) // cCapSets
binary.Write(caps, binary.LittleEndian, uint16(0)) // pad
binary.Write(caps, binary.LittleEndian, uint16(CB_CAPSTYPE_GENERAL))
binary.Write(caps, binary.LittleEndian, uint16(12))
binary.Write(caps, binary.LittleEndian, uint32(CB_CAPS_VERSION_2))
binary.Write(caps, binary.LittleEndian, uint32(CB_USE_LONG_FORMAT_NAMES|CB_STREAM_FILECLIP_ENABLED))
h.processClipCaps(caps.Bytes())
if !h.serverFileClip {
t.Fatal("serverFileClip should be set after caps with CB_STREAM_FILECLIP_ENABLED")
}
return h, fs
}
func TestBuildParseDropfilesRoundTrip(t *testing.T) {
names := []string{`C:\Users\测试\报告 最终版.docx`, "plain.txt", "数据 - 副本 (2).xlsx"}
b := BuildDropfiles(names)
if binary.LittleEndian.Uint32(b[0:]) != dropfilesHeaderLen {
t.Fatalf("pFiles = %d, want %d", binary.LittleEndian.Uint32(b[0:]), dropfilesHeaderLen)
}
if binary.LittleEndian.Uint32(b[16:]) != 1 {
t.Fatal("fWide should be 1 (Unicode)")
}
got, err := ParseDropfiles(b)
if err != nil {
t.Fatal(err)
}
if len(got) != len(names) {
t.Fatalf("got %d names, want %d: %q", len(got), len(names), got)
}
for i := range names {
if got[i] != names[i] {
t.Fatalf("name[%d] = %q, want %q", i, got[i], names[i])
}
}
}
func TestParseDropfilesASCII(t *testing.T) {
// fWide=0 的 ASCII 变体(FreeRDP 老服务器可能使用)
list := []byte("a.txt\x00b.bin\x00\x00")
body := make([]byte, dropfilesHeaderLen+len(list))
binary.LittleEndian.PutUint32(body[0:], dropfilesHeaderLen)
binary.LittleEndian.PutUint32(body[16:], 0)
copy(body[dropfilesHeaderLen:], list)
got, err := ParseDropfiles(body)
if err != nil {
t.Fatal(err)
}
if len(got) != 2 || got[0] != "a.txt" || got[1] != "b.bin" {
t.Fatalf("got %q", got)
}
}
func TestParseDropfilesRejectsBad(t *testing.T) {
if _, err := ParseDropfiles(make([]byte, 8)); err == nil {
t.Fatal("short body should fail")
}
bad := make([]byte, dropfilesHeaderLen)
binary.LittleEndian.PutUint32(bad[0:], 1<<20) // pFiles 越界
if _, err := ParseDropfiles(bad); err == nil {
t.Fatal("out-of-range pFiles should fail")
}
}
func serverFormatListWithHDrop(t *testing.T, h *CliprdrHandler) {
t.Helper()
// long-name Format List:CF_HDROP(id 15) + CF_UNICODETEXT
b := &bytes.Buffer{}
binary.Write(b, binary.LittleEndian, uint32(CF_HDROP))
b.Write(encodeUTF16LE("")) // 标准格式空名
b.Write([]byte{0, 0})
binary.Write(b, binary.LittleEndian, uint32(CF_UNICODETEXT))
b.Write(encodeUTF16LE(""))
b.Write([]byte{0, 0})
h.processFormatList(b.Bytes(), 0)
}
func TestServerFormatListTriggersHDropRequest(t *testing.T) {
h, fs := newFileTestHandler(t)
serverFormatListWithHDrop(t, h)
// 第一条:Format List Response OK;第二条:Format Data Request(CF_HDROP)
if len(fs.pdus) < 2 {
t.Fatalf("expected FORMAT_LIST_RESPONSE + FORMAT_DATA_REQUEST, got %d", len(fs.pdus))
}
req := fs.pdus[1]
if req.msgType != CB_FORMAT_DATA_REQUEST {
t.Fatalf("msgType=%#x, want FORMAT_DATA_REQUEST", req.msgType)
}
if id := binary.LittleEndian.Uint32(req.body); id != CF_HDROP {
t.Fatalf("requested format=%d, want %d", id, CF_HDROP)
}
if h.lastRequestedFormat != CF_HDROP {
t.Fatalf("lastRequestedFormat=%d", h.lastRequestedFormat)
}
}
func TestRemoteFilesReceivedViaFormatDataResponse(t *testing.T) {
h, _ := newFileTestHandler(t)
var gotNames []string
h.SetFileCallbacks(func(names []string) { gotNames = names }, nil, nil)
serverFormatListWithHDrop(t, h)
payload := BuildDropfiles([]string{`D:\share\a.pdf`, `D:\share\b.pdf`})
h.processFormatDataResponse(payload, CB_RESPONSE_OK)
if len(gotNames) != 2 || gotNames[0] != `D:\share\a.pdf` {
t.Fatalf("onRemoteFiles got %q", gotNames)
}
if names := h.RemoteFileNames(); len(names) != 2 {
t.Fatalf("RemoteFileNames = %q", names)
}
}
func TestFormatDataRequestServesStagedFiles(t *testing.T) {
h, fs := newFileTestHandler(t)
h.SetLocalFiles([]LocalFile{{Name: "x.bin", Data: []byte{1, 2, 3}}})
req := make([]byte, 4)
binary.LittleEndian.PutUint32(req, CF_FILE_GROUP_DESCRIPTORW)
h.processFormatDataRequest(req)
resp := fs.last()
if resp.msgType != CB_FORMAT_DATA_RESPONSE || resp.flags != CB_RESPONSE_OK {
t.Fatalf("msgType=%#x flags=%#x", resp.msgType, resp.flags)
}
names, sizes, err := ParseFileGroupDescriptorW(resp.body)
if err != nil || len(names) != 1 || names[0] != "x.bin" || sizes[0] != 3 {
t.Fatalf("FileGroupDescriptorW parse=%q %v err=%v", names, sizes, err)
}
}
func TestFormatDataRequestNoFilesFails(t *testing.T) {
h, fs := newFileTestHandler(t)
req := make([]byte, 4)
binary.LittleEndian.PutUint32(req, CF_FILE_GROUP_DESCRIPTORW)
h.processFormatDataRequest(req)
if fs.last().flags != CB_RESPONSE_FAIL {
t.Fatal("expected FAIL when no staged files")
}
}
// TestBuildParseFileGroupDescriptorWRoundTrip:编码→解码保持名称与大小。
func TestBuildParseFileGroupDescriptorWRoundTrip(t *testing.T) {
files := []LocalFile{
{Name: "报 告 最终版.docx", Data: make([]byte, 5)},
{Name: "小.txt", Data: []byte("hi")},
}
payload := BuildFileGroupDescriptorW(files)
if len(payload) != 4+2*fileDescriptorSize {
t.Fatalf("payload=%d bytes, want %d", len(payload), 4+2*fileDescriptorSize)
}
names, sizes, err := ParseFileGroupDescriptorW(payload)
if err != nil {
t.Fatal(err)
}
if len(names) != 2 {
t.Fatalf("names=%q", names)
}
if names[0] != "报 告 最终版.docx" || names[1] != "小.txt" {
t.Fatalf("names=%q", names)
}
if sizes[0] != 5 || sizes[1] != 2 {
t.Fatalf("sizes=%v", sizes)
}
}
// TestServerFileDescriptorListFlow:服务器通告 "FileGroupDescriptorW" 时
// 应以其 ID 请求格式数据,并用描述符填充远端文件列表(含大小)。
func TestServerFileDescriptorListFlow(t *testing.T) {
h, fs := newFileTestHandler(t)
var gotNames []string
h.SetFileCallbacks(func(names []string) { gotNames = names }, nil, nil)
// 服务器 Format List:FileGroupDescriptorW(id 0xC181) + FileContents + DropEffect
b := &bytes.Buffer{}
writeNamed := func(id uint32, name string) {
binary.Write(b, binary.LittleEndian, id)
b.Write(encodeUTF16LE(name))
b.Write([]byte{0, 0})
}
writeNamed(0xC181, FormatNameFileGroupDescriptorW)
writeNamed(0xC182, FormatNameFileContents)
writeNamed(0xC17E, FormatNameDropEffect)
h.processFormatList(b.Bytes(), 0)
req := fs.last()
if req.msgType != CB_FORMAT_DATA_REQUEST ||
binary.LittleEndian.Uint32(req.body) != 0xC181 {
t.Fatalf("expected FormatDataRequest(0xC181), got %#x id=%d",
req.msgType, binary.LittleEndian.Uint32(req.body))
}
payload := BuildFileGroupDescriptorW([]LocalFile{{Name: `C:\doc\a.pdf`, Data: make([]byte, 9)}})
h.processFormatDataResponse(payload, CB_RESPONSE_OK)
if len(gotNames) != 1 || gotNames[0] != `C:\doc\a.pdf` {
t.Fatalf("onRemoteFiles got %q", gotNames)
}
// 已知大小:RequestRemoteFile 应直接发 RANGE(0) 而非 SIZE
if err := h.RequestRemoteFile(0); err != nil {
t.Fatal(err)
}
fr := fs.last()
if binary.LittleEndian.Uint32(fr.body[8:]) != FILECONTENTS_RANGE ||
binary.LittleEndian.Uint32(fr.body[12:]) != 0 {
t.Fatalf("want direct RANGE(0), flags=%d pos=%d",
binary.LittleEndian.Uint32(fr.body[8:]), binary.LittleEndian.Uint32(fr.body[12:]))
}
}
// TestDropEffectRequest:资源管理器右键菜单查询 "Preferred DropEffect",
// 有暂存文件时必须回 OK + DROPEFFECT_COPY,否则"粘贴"被置灰。
func TestDropEffectRequest(t *testing.T) {
h, fs := newFileTestHandler(t)
req := make([]byte, 4)
binary.LittleEndian.PutUint32(req, CF_DROP_EFFECT)
h.processFormatDataRequest(req)
if fs.last().flags != CB_RESPONSE_FAIL {
t.Fatal("no staged files: expected FAIL for DropEffect")
}
h.SetLocalFiles([]LocalFile{{Name: "a.txt", Data: []byte("hi")}})
h.processFormatDataRequest(req)
resp := fs.last()
if resp.msgType != CB_FORMAT_DATA_RESPONSE || resp.flags != CB_RESPONSE_OK {
t.Fatalf("msgType=%#x flags=%#x", resp.msgType, resp.flags)
}
if v := binary.LittleEndian.Uint32(resp.body); v != DROPEFFECT_COPY {
t.Fatalf("DropEffect=%d, want %d", v, DROPEFFECT_COPY)
}
}
func TestFormatListAdvertisesDropEffectWithFiles(t *testing.T) {
h, fs := newFileTestHandler(t)
h.SetLocalFiles([]LocalFile{{Name: "a.txt", Data: []byte("hi")}})
body := fs.last().body
if !bytes.Contains(body, encodeUTF16LE(FormatNameDropEffect)) {
t.Fatal("staged Format List should advertise Preferred DropEffect")
}
}
func TestFileContentsRequestServesSizeAndRange(t *testing.T) {
h, fs := newFileTestHandler(t)
data := bytes.Repeat([]byte{0xA5}, 300*1024) // 跨两个 RANGE 段
h.SetLocalFiles([]LocalFile{{Name: "big.bin", Data: data}})
// SIZE 请求
szReq := make([]byte, 24)
binary.LittleEndian.PutUint32(szReq[0:], 7) // streamId
binary.LittleEndian.PutUint32(szReq[4:], 0) // lindex
binary.LittleEndian.PutUint32(szReq[8:], FILECONTENTS_SIZE)
binary.LittleEndian.PutUint32(szReq[20:], 8)
h.processFileContentsRequest(szReq)
resp := fs.last()
if resp.msgType != CB_FILECONTENTS_RESPONSE || resp.flags != CB_RESPONSE_OK {
t.Fatalf("SIZE: msgType=%#x flags=%#x", resp.msgType, resp.flags)
}
if sid := binary.LittleEndian.Uint32(resp.body); sid != 7 {
t.Fatalf("SIZE: streamId=%d", sid)
}
if size := binary.LittleEndian.Uint64(resp.body[4:]); size != uint64(len(data)) {
t.Fatalf("SIZE: got %d want %d", size, len(data))
}
// RANGE 请求(尾段裁剪到文件末尾)
rng := make([]byte, 24)
binary.LittleEndian.PutUint32(rng[0:], 7)
binary.LittleEndian.PutUint32(rng[8:], FILECONTENTS_RANGE)
binary.LittleEndian.PutUint32(rng[12:], 290*1024) // pos
binary.LittleEndian.PutUint32(rng[20:], 1<<20) // 请求远超末尾
h.processFileContentsRequest(rng)
resp = fs.last()
got := resp.body[4:]
if len(got) != 10*1024 {
t.Fatalf("RANGE: got %d bytes, want clamped %d", len(got), 10*1024)
}
if got[0] != 0xA5 {
t.Fatal("RANGE: wrong payload")
}
// 越界 lindex → FAIL 且带 streamId
bad := make([]byte, 24)
binary.LittleEndian.PutUint32(bad[0:], 9)
binary.LittleEndian.PutUint32(bad[4:], 5)
binary.LittleEndian.PutUint32(bad[8:], FILECONTENTS_SIZE)
h.processFileContentsRequest(bad)
resp = fs.last()
if resp.flags != CB_RESPONSE_FAIL || len(resp.body) != 4 ||
binary.LittleEndian.Uint32(resp.body) != 9 {
t.Fatalf("unknown lindex: flags=%#x body=% X", resp.flags, resp.body)
}
}
// TestRemoteFileTransferChain 走完 SIZE→RANGE→完成的完整拉取链,
// 并验证服务器裁剪返回段时按实际长度推进。
func TestRemoteFileTransferChain(t *testing.T) {
h, fs := newFileTestHandler(t)
var resultName string
var resultData []byte
var progGot, progTotal int64
h.SetFileCallbacks(func(names []string) {},
func(index int, name string, data []byte) { resultName, resultData = name, data },
func(index int, received, total int64) { progGot, progTotal = received, total })
serverFormatListWithHDrop(t, h)
payload := BuildDropfiles([]string{`E:\doc\说明 书.pdf`})
h.processFormatDataResponse(payload, CB_RESPONSE_OK)
file := bytes.Repeat([]byte{0x5A}, fileRangeChunk+1000)
if err := h.RequestRemoteFile(0); err != nil {
t.Fatal(err)
}
// 1) SIZE 请求
req := fs.last()
if req.msgType != CB_FILECONTENTS_REQUEST ||
binary.LittleEndian.Uint32(req.body[8:]) != FILECONTENTS_SIZE {
t.Fatalf("step1: msgType=%#x flags=%#x", req.msgType, binary.LittleEndian.Uint32(req.body[8:]))
}
sid := binary.LittleEndian.Uint32(req.body[0:])
// SIZE 响应 → 应发出 RANGE(0, fileRangeChunk)
h.processFileContentsResponse(append(u32le(sid), u64le(uint64(len(file)))...), CB_RESPONSE_OK)
req = fs.last()
if binary.LittleEndian.Uint32(req.body[8:]) != FILECONTENTS_RANGE ||
binary.LittleEndian.Uint32(req.body[12:]) != 0 ||
binary.LittleEndian.Uint32(req.body[20:]) != fileRangeChunk {
t.Fatalf("step2: want RANGE(0,%d), got pos=%d cb=%d", fileRangeChunk,
binary.LittleEndian.Uint32(req.body[12:]), binary.LittleEndian.Uint32(req.body[20:]))
}
// RANGE 响应(服务器只回一半)→ 应从实际到达位置续传
h.processFileContentsResponse(append(u32le(sid), file[:fileRangeChunk/2]...), CB_RESPONSE_OK)
req = fs.last()
if p := binary.LittleEndian.Uint32(req.body[12:]); p != fileRangeChunk/2 {
t.Fatalf("step3: want resume pos=%d, got %d", fileRangeChunk/2, p)
}
if progGot != fileRangeChunk/2 || progTotal != int64(len(file)) {
t.Fatalf("progress got=%d total=%d", progGot, progTotal)
}
// 剩余数据 → 完成回调
h.processFileContentsResponse(append(u32le(sid), file[fileRangeChunk/2:]...), CB_RESPONSE_OK)
if resultName != `E:\doc\说明 书.pdf` {
t.Fatalf("result name=%q", resultName)
}
if !bytes.Equal(resultData, file) {
t.Fatalf("result %d bytes, want %d", len(resultData), len(file))
}
}
func TestRemoteFileTransferFailure(t *testing.T) {
h, fs := newFileTestHandler(t)
var failData []byte
var failName string
h.SetFileCallbacks(func(names []string) {},
func(index int, name string, data []byte) { failName, failData = name, data },
nil)
serverFormatListWithHDrop(t, h)
h.processFormatDataResponse(BuildDropfiles([]string{"gone.txt"}), CB_RESPONSE_OK)
if err := h.RequestRemoteFile(0); err != nil {
t.Fatal(err)
}
sid := binary.LittleEndian.Uint32(fs.last().body[0:])
h.processFileContentsResponse(u32le(sid), CB_RESPONSE_FAIL)
if failName != "gone.txt" || failData != nil {
t.Fatalf("failure delivery: name=%q data=%v", failName, failData)
}
}
// TestServerLocalDropEffectId:Windows 用它本地的 "Preferred DropEffect"
// 注册格式 ID(实测 0xC17E)查询而非我方广告 ID,需按 DropEffect 应答。
func TestServerLocalDropEffectId(t *testing.T) {
h, fs := newFileTestHandler(t)
h.SetLocalFiles([]LocalFile{{Name: "a.txt", Data: []byte("hi")}})
req := make([]byte, 4)
binary.LittleEndian.PutUint32(req, 0xC17E)
h.processFormatDataRequest(req)
resp := fs.last()
if resp.flags != CB_RESPONSE_OK || len(resp.body) != 4 ||
binary.LittleEndian.Uint32(resp.body) != DROPEFFECT_COPY {
t.Fatalf("heuristic DropEffect reply failed: flags=%#x body=% X", resp.flags, resp.body)
}
// 服务器通告过名称后,精确匹配其本地 ID
h2, fs2 := newFileTestHandler(t)
b := &bytes.Buffer{}
binary.Write(b, binary.LittleEndian, uint32(0xC17E))
b.Write(encodeUTF16LE(FormatNameDropEffect))
b.Write([]byte{0, 0})
h2.processFormatList(b.Bytes(), 0)
h2.SetLocalFiles([]LocalFile{{Name: "a.txt", Data: []byte("hi")}})
h2.processFormatDataRequest(req)
if fs2.last().flags != CB_RESPONSE_OK {
t.Fatal("learned serverDropEffectId should be answered with OK")
}
// 无文件时 FAIL
h3, fs3 := newFileTestHandler(t)
b3 := &bytes.Buffer{}
binary.Write(b3, binary.LittleEndian, uint32(0xC17E))
b3.Write(encodeUTF16LE(FormatNameDropEffect))
b3.Write([]byte{0, 0})
h3.processFormatList(b3.Bytes(), 0)
h3.processFormatDataRequest(req)
if fs3.last().flags != CB_RESPONSE_FAIL {
t.Fatal("no staged files: server-local DropEffect should FAIL")
}
}
func TestRequestRemoteFileOutOfRange(t *testing.T) {
h, _ := newFileTestHandler(t)
if err := h.RequestRemoteFile(3); err == nil {
t.Fatal("expected error for out-of-range index")
}
}
func TestFormatListAdvertisesFileFormatsWhenStaged(t *testing.T) {
h, fs := newFileTestHandler(t) // long-name 模式
h.sendFormatList()
body := fs.last().body
if bytes.Contains(body, encodeUTF16LE(FormatNameFileGroupDescriptorW)) {
t.Fatal("file formats must not be advertised before staging files")
}
h.SetLocalFiles([]LocalFile{{Name: "a.txt", Data: []byte("hi")}})
body = fs.last().body
if !bytes.Contains(body, encodeUTF16LE(FormatNameFileGroupDescriptorW)) ||
!bytes.Contains(body, encodeUTF16LE(FormatNameFileContents)) ||
!bytes.Contains(body, encodeUTF16LE(FormatNameDropEffect)) {
t.Fatal("staged Format List should advertise FileGroupDescriptorW/FileContents/DropEffect")
}
// short-name 模式(名称为原始 ASCII 字节)
h2, fs2 := newFileTestHandler(t)
h2.useLongFormatNames = false
h2.SetLocalFiles([]LocalFile{{Name: "a.txt", Data: []byte("hi")}})
body = fs2.last().body
if !bytes.Contains(body, []byte("FileGroupDescriptorW")) {
t.Fatalf("short-name Format List should carry FileGroupDescriptorW: % X", body[:min(80, len(body))])
}
}
func u64le(v uint64) []byte {
b := make([]byte, 8)
binary.LittleEndian.PutUint64(b, v)
return b
}
// TestParseFileGroupDescriptorWLargeDirectory:远端复制含数千文件的目录
// 是正常操作(曾因固定 1024 项上限整单被拒);超过防御性上限才拒绝。
func TestParseFileGroupDescriptorWLargeDirectory(t *testing.T) {
build := func(n uint32) []byte {
body := make([]byte, 4+int(n)*fileDescriptorSize)
binary.LittleEndian.PutUint32(body[0:], n)
for i := uint32(0); i < n; i++ {
d := body[4+int(i)*fileDescriptorSize:]
binary.LittleEndian.PutUint32(d[0:], FD_ATTRIBUTES|FD_FILESIZE|FD_PROGRESSUI)
binary.LittleEndian.PutUint32(d[68:], 7) // 大小低 32 位
copy(d[72:], asciiUTF16(fmt.Sprintf("f%d.txt", i)))
}
return body
}
// 5000 项:必须接受
names, _, err := ParseFileGroupDescriptorW(build(5000))
if err != nil || len(names) != 5000 {
t.Fatalf("5000 items: err=%v names=%d", err, len(names))
}
if names[4999] != "f4999.txt" {
t.Fatalf("last name=%q", names[4999])
}
// 超过 maxDescriptorItems:拒绝
if _, _, err := ParseFileGroupDescriptorW(build(maxDescriptorItems + 1)); err == nil {
t.Fatal("over-limit cItems should be rejected")
}
// cItems 声明与载荷不符:拒绝
short := build(3)[:4+2*fileDescriptorSize]
binary.LittleEndian.PutUint32(short[0:], 3)
if _, _, err := ParseFileGroupDescriptorW(short); err == nil {
t.Fatal("truncated payload should be rejected")
}
}
// asciiUTF16 把纯 ASCII 名编码为 UTF-16LE(NUL 结尾),供构造描述符用。
func asciiUTF16(s string) []byte {
b := make([]byte, 0, (len(s)+1)*2)
for _, c := range []byte(s) {
b = append(b, c, 0)
}
return append(b, 0, 0)
}