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) }