Files
rdplib/plugin/rdpdr/rdpdr_test.go
T

429 lines
16 KiB
Go
Raw 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 rdpdr
import (
"bytes"
"encoding/binary"
"testing"
)
// fakeSender 捕获发往通道的消息。
type fakeSender struct{ msgs [][]byte }
func (f *fakeSender) SendToChannel(ch string, s []byte) (int, error) {
f.msgs = append(f.msgs, append([]byte(nil), s...))
return len(s), nil
}
// fakeFS 记录派发并支持手动完成。
type fakeFS struct {
h *Handler
opens []string
lists int
complet []uint32
}
func (f *fakeFS) Open(completionID, fileID uint32, path string) {
f.opens = append(f.opens, path)
}
func (f *fakeFS) Read(completionID, fileID uint32, offset uint64, length uint32) {}
func (f *fakeFS) Close(completionID, fileID uint32) {}
func (f *fakeFS) List(completionID, fileID uint32, initial bool) {
f.complet = append(f.complet, completionID)
f.lists++
}
func (f *fakeFS) Stat(completionID, fileID uint32) {}
func (f *fakeFS) Volume(completionID uint32, infoClass uint32) {}
func newTestHandler() (*Handler, *fakeSender, *fakeFS) {
h := NewHandler("webrdp", "local")
s := &fakeSender{}
h.Sender(s)
fs := &fakeFS{h: h}
h.SetFilesystem(fs)
return h, s, fs
}
func TestServerAnnounceFlow(t *testing.T) {
h, s, _ := newTestHandler()
// Server Announce:major 1 minor 5 clientID 0x1234
req := make([]byte, 12)
binary.LittleEndian.PutUint16(req[0:], RDPDR_CTYP_CORE)
binary.LittleEndian.PutUint16(req[2:], PAKID_CORE_SERVER_ANNOUNCE)
binary.LittleEndian.PutUint16(req[4:], 1)
binary.LittleEndian.PutUint16(req[6:], 5)
binary.LittleEndian.PutUint32(req[8:], 0x1234)
h.Process(req)
if len(s.msgs) != 2 {
t.Fatalf("应发 2 条消息(announce reply/name),实发 %d", len(s.msgs))
}
// 1) Client Announce Reply:v1.5 + 原 ClientId
if got := binary.LittleEndian.Uint16(s.msgs[0][2:]); got != PAKID_CORE_CLIENTID_CONFIRM {
t.Fatalf("msg0 packetID=%#x", got)
}
if binary.LittleEndian.Uint16(s.msgs[0][4:]) != 1 || binary.LittleEndian.Uint16(s.msgs[0][6:]) != 5 {
t.Fatal("announce reply 版本应为 1.5")
}
if binary.LittleEndian.Uint32(s.msgs[0][8:]) != 0x1234 {
t.Fatal("announce reply 应原样回 ClientId")
}
// 2) Client Name Request:UnicodeFlag=1
if got := binary.LittleEndian.Uint16(s.msgs[1][2:]); got != PAKID_CORE_CLIENT_NAME {
t.Fatalf("msg1 packetID=%#x", got)
}
if binary.LittleEndian.Uint32(s.msgs[1][4:]) != 1 {
t.Fatal("UnicodeFlag 应为 1")
}
// 服务端 CLIENTID_CONFIRM:仅记录状态,设备列表还不发
req2 := make([]byte, 8)
binary.LittleEndian.PutUint16(req2[0:], RDPDR_CTYP_CORE)
binary.LittleEndian.PutUint16(req2[2:], PAKID_CORE_CLIENTID_CONFIRM)
h.Process(req2)
if len(s.msgs) != 2 {
t.Fatalf("CLIENTID_CONFIRM 后不应发消息,实发 %d", len(s.msgs))
}
// 服务端 USER_LOGGEDON → 此刻才宣告设备列表(MS-RDPEFS 3.2.5.1.3)
req3 := make([]byte, 8)
binary.LittleEndian.PutUint16(req3[0:], RDPDR_CTYP_CORE)
binary.LittleEndian.PutUint16(req3[2:], PAKID_CORE_USER_LOGGEDON)
h.Process(req3)
if len(s.msgs) != 3 {
t.Fatalf("USER_LOGGEDON 后应发设备列表,实发 %d", len(s.msgs))
}
// 3) Device List:count=1, type=FILESYSTEM, DosName "webrdp"
if got := binary.LittleEndian.Uint16(s.msgs[2][2:]); got != PAKID_CORE_DEVICELIST_ANNOUNCE {
t.Fatalf("msg2 packetID=%#x", got)
}
if binary.LittleEndian.Uint32(s.msgs[2][4:]) != 1 {
t.Fatal("DeviceCount 应为 1")
}
if binary.LittleEndian.Uint32(s.msgs[2][8:]) != RDPDR_DTYP_FILESYSTEM {
t.Fatal("DeviceType 应为 FILESYSTEM(8)")
}
if got := binary.LittleEndian.Uint32(s.msgs[2][12:]); got != 1 {
t.Fatalf("DeviceId 应为 1(非 0),实 %d", got)
}
if name := string(bytes.TrimRight(s.msgs[2][16:24], "\x00")); name != "webrdp" {
t.Fatalf("DosName=%q", name)
}
// V02 协商下 DeviceData = ASCII 全名 + 1 字节 NUL(FreeRDP drive_main.c
// 同款;服务端 UNC 映射键取 DosName,DeviceData 内容须非空否则拒装)
if got := len(s.msgs[2]); got != 35 { // 28 + "webrdp" ASCII 6 + NUL 1
t.Fatalf("设备列表报文应为 35 字节,实 %d", got)
}
if got := binary.LittleEndian.Uint32(s.msgs[2][24:]); got != 7 {
t.Fatalf("DeviceDataLength 应为 7,实 %d", got)
}
if !bytes.Equal(s.msgs[2][28:35], []byte("webrdp\x00")) {
t.Fatalf("DeviceData 应为 ASCII 全名+NUL,实 % x", s.msgs[2][28:35])
}
}
func TestAnnounceVersionNegotiation(t *testing.T) {
h, s, _ := newTestHandler()
// 服务端 1.13(Win10)→ 客户端应回 1.MIN(0x000D, 0x000D)=1.13
req := make([]byte, 12)
binary.LittleEndian.PutUint16(req[0:], RDPDR_CTYP_CORE)
binary.LittleEndian.PutUint16(req[2:], PAKID_CORE_SERVER_ANNOUNCE)
binary.LittleEndian.PutUint16(req[4:], 1)
binary.LittleEndian.PutUint16(req[6:], 0x000D)
binary.LittleEndian.PutUint32(req[8:], 0x1234)
h.Process(req)
if binary.LittleEndian.Uint16(s.msgs[0][4:]) != 1 ||
binary.LittleEndian.Uint16(s.msgs[0][6:]) != 0x000D {
t.Fatalf("服务端 1.13 时应回 1.13,实 %d.%d",
binary.LittleEndian.Uint16(s.msgs[0][4:]),
binary.LittleEndian.Uint16(s.msgs[0][6:]))
}
// 服务端 1.5(老语义)→ 回 1.5
h2, s2, _ := newTestHandler()
binary.LittleEndian.PutUint16(req[6:], 5)
h2.Process(req)
if binary.LittleEndian.Uint16(s2.msgs[0][6:]) != 5 {
t.Fatal("服务端 1.5 时应回 1.5")
}
}
func TestSingleAnnounceAtLogon(t *testing.T) {
h, s, _ := newTestHandler()
// Server Announce → Server Caps → ClientID Confirm:1.0x 语义下
// 均不应发设备列表(FreeRDP:非登录阶段跳过 FS 设备,count=0 不发)
ann := make([]byte, 12)
binary.LittleEndian.PutUint16(ann[0:], RDPDR_CTYP_CORE)
binary.LittleEndian.PutUint16(ann[2:], PAKID_CORE_SERVER_ANNOUNCE)
binary.LittleEndian.PutUint16(ann[4:], 1)
binary.LittleEndian.PutUint16(ann[6:], 0x000D)
binary.LittleEndian.PutUint32(ann[8:], 0x1234)
h.Process(ann)
caps := make([]byte, 8)
binary.LittleEndian.PutUint16(caps[0:], RDPDR_CTYP_CORE)
binary.LittleEndian.PutUint16(caps[2:], PAKID_CORE_SERVER_CAPABILITY)
caps[4] = 0
h.Process(caps)
conf := make([]byte, 12)
binary.LittleEndian.PutUint16(conf[0:], RDPDR_CTYP_CORE)
binary.LittleEndian.PutUint16(conf[2:], PAKID_CORE_CLIENTID_CONFIRM)
binary.LittleEndian.PutUint16(conf[4:], 1)
binary.LittleEndian.PutUint16(conf[6:], 0x000D)
binary.LittleEndian.PutUint32(conf[8:], 0x99) // 服务端回显采纳
h.Process(conf)
// msgs: announce reply, name request, caps response——无设备列表
if n := len(s.msgs); n != 3 {
t.Fatalf("登录前不应发设备列表(3 条),实 %d", n)
}
if h.clientID != 0x99 {
t.Fatalf("应采纳 Confirm 回显的 ClientId,实 %#x", h.clientID)
}
// USER_LOGGEDON:唯一一次设备列表宣告
logon := make([]byte, 8)
binary.LittleEndian.PutUint16(logon[0:], RDPDR_CTYP_CORE)
binary.LittleEndian.PutUint16(logon[2:], PAKID_CORE_USER_LOGGEDON)
h.Process(logon)
if n := len(s.msgs); n != 4 {
t.Fatalf("USER_LOGGEDON 后应发设备列表(4 条),实 %d", n)
}
if got := binary.LittleEndian.Uint16(s.msgs[3][2:]); got != PAKID_CORE_DEVICELIST_ANNOUNCE {
t.Fatalf("msg3 应为设备列表,实 %#x", got)
}
// 重复 USER_LOGGEDON 不重发
h.Process(logon)
if n := len(s.msgs); n != 4 {
t.Fatalf("重复 USER_LOGGEDON 不应重发,实 %d", n)
}
}
func TestSanitizeShareName(t *testing.T) {
if got := sanitizeShareName(`a:b<c>d"e/f\g|h i,j`); got != "a_b_c_d_e_f_g_h_i_j" {
t.Fatalf("sanitizeShareName=%q", got)
}
if got := sanitizeShareName("rdpdrive-test"); got != "rdpdrive-test" {
t.Fatalf("合法名不应改动,实 %q", got)
}
}
func TestClientCapabilityResponse(t *testing.T) {
h, s, _ := newTestHandler()
// 先握手(服务端 1.13 → 客户端版本 1.13),再发带 GENERAL capset 的
// 服务端能力请求(ioCode1 = 全部常见 IRP 位)
ann := make([]byte, 12)
binary.LittleEndian.PutUint16(ann[0:], RDPDR_CTYP_CORE)
binary.LittleEndian.PutUint16(ann[2:], PAKID_CORE_SERVER_ANNOUNCE)
binary.LittleEndian.PutUint16(ann[4:], 1)
binary.LittleEndian.PutUint16(ann[6:], 0x000D)
binary.LittleEndian.PutUint32(ann[8:], 0x1234)
h.Process(ann)
req := make([]byte, 8+44)
binary.LittleEndian.PutUint16(req[0:], RDPDR_CTYP_CORE)
binary.LittleEndian.PutUint16(req[2:], PAKID_CORE_SERVER_CAPABILITY)
binary.LittleEndian.PutUint16(req[4:], 1) // numCapabilities
binary.LittleEndian.PutUint16(req[8:], CAP_GENERAL_TYPE)
binary.LittleEndian.PutUint16(req[10:], 44)
binary.LittleEndian.PutUint32(req[12:], 2)
binary.LittleEndian.PutUint32(req[28:], 0xFFFFFFFF) // ioCode1
h.Process(req)
if len(s.msgs) != 3 || len(s.msgs[2]) != 8+44+8 {
t.Fatalf("能力响应长度应 60,实 %v", len(s.msgs[2]))
}
m := s.msgs[2]
if binary.LittleEndian.Uint16(m[2:]) != PAKID_CORE_CLIENT_CAPABILITY {
t.Fatal("packetID 应为 CLIENT_CAPABILITY")
}
if binary.LittleEndian.Uint16(m[4:]) != 2 {
t.Fatal("numCapabilities 应为 2")
}
// GENERAL:版本字段各 2 字节(旧实现误写 4 字节,服务端解析错位)
if binary.LittleEndian.Uint16(m[8:]) != CAP_GENERAL_TYPE || binary.LittleEndian.Uint16(m[10:]) != 44 {
t.Fatal("GENERAL capset 头错误")
}
if binary.LittleEndian.Uint16(m[24:]) != 1 || binary.LittleEndian.Uint16(m[26:]) != 0x000D {
t.Fatal("GENERAL 协议版本应为 1.13(各 2 字节)")
}
// ioCode1 = 客户端掩码 ∩ 服务端掩码
if got := binary.LittleEndian.Uint32(m[28:]); got != clientIOCode1 {
t.Fatalf("ioCode1 应为求交结果 %#x,实 %#x", clientIOCode1, got)
}
// extendedPDU = REMOVE|DISPLAY_NAME|USER_LOGGEDON;extraFlags1 = ENABLE_ASYNCIO
if got := binary.LittleEndian.Uint32(m[36:]); got != 7 {
t.Fatalf("extendedPDU 应为 7,实 %#x", got)
}
if got := binary.LittleEndian.Uint32(m[40:]); got != RDPDR_ENABLE_ASYNCIO {
t.Fatalf("extraFlags1 应为 ENABLE_ASYNCIO,实 %#x", got)
}
// DRIVE:仅 8 字节头 {type=4, len=8, version=2}(FreeRDP 同款)
if binary.LittleEndian.Uint16(m[52:]) != CAP_DRIVE_TYPE || binary.LittleEndian.Uint16(m[54:]) != 8 {
t.Fatal("DRIVE capset 头错误(FreeRDP 对齐:仅 8 字节头,len=8)")
}
if binary.LittleEndian.Uint32(m[56:]) != 2 {
t.Fatal("DRIVE capset Version 应为 2")
}
}
func TestCreateDispatchAndComplete(t *testing.T) {
h, s, fs := newTestHandler()
// 构造 CREATE 请求:path "\hello.txt"(FileId 由 handler 分配,预期为 1)
path := utf16Bytes("\\hello.txt")
req := make([]byte, 56+len(path))
binary.LittleEndian.PutUint16(req[0:], RDPDR_CTYP_CORE)
binary.LittleEndian.PutUint16(req[2:], PAKID_CORE_DEVICE_IOREQUEST)
binary.LittleEndian.PutUint32(req[4:], h.deviceID)
binary.LittleEndian.PutUint32(req[12:], 77) // CompletionId
binary.LittleEndian.PutUint32(req[16:], IRP_MJ_CREATE)
binary.LittleEndian.PutUint32(req[52:], uint32(len(path)))
copy(req[56:], path)
h.Process(req)
if len(fs.opens) != 1 || fs.opens[0] != "\\hello.txt" {
t.Fatalf("Open 未正确派发: %v", fs.opens)
}
// 完成路径 A:失败 → 纯状态响应
h.CompleteJSON(77, STATUS_NO_SUCH_FILE, "create", "file")
if len(s.msgs) != 1 {
t.Fatalf("失败完成应发 1 条响应,实 %d", len(s.msgs))
}
resp := s.msgs[0]
if binary.LittleEndian.Uint16(resp[2:]) != PAKID_CORE_DEVICE_IOCOMPLETION ||
binary.LittleEndian.Uint32(resp[8:]) != 77 ||
binary.LittleEndian.Uint32(resp[12:]) != STATUS_NO_SUCH_FILE {
t.Fatal("失败 CREATE 响应头错误")
}
// 完成路径 B:成功 → 16 字节头 + FileId(4) + Information(1)
h.track(78, &pending{deviceID: h.deviceID, fileID: 1, major: IRP_MJ_CREATE})
h.CompleteJSON(78, STATUS_SUCCESS, "create", "file")
resp = s.msgs[1]
if len(resp) != 21 {
t.Fatalf("成功 CREATE 响应应 21 字节,实 %d", len(resp))
}
if binary.LittleEndian.Uint32(resp[16:]) != 1 || resp[20] != 1 {
t.Fatal("CREATE 响应 FileId/Information 错误")
}
}
func TestDirectoryListPagination(t *testing.T) {
h, s, fs := newTestHandler()
dirID := uint32(1)
req := make([]byte, 56)
binary.LittleEndian.PutUint16(req[0:], RDPDR_CTYP_CORE)
binary.LittleEndian.PutUint16(req[2:], PAKID_CORE_DEVICE_IOREQUEST)
binary.LittleEndian.PutUint32(req[4:], h.deviceID)
binary.LittleEndian.PutUint32(req[8:], dirID)
binary.LittleEndian.PutUint32(req[12:], 500)
binary.LittleEndian.PutUint32(req[16:], IRP_MJ_DIRECTORY_CONTROL)
binary.LittleEndian.PutUint32(req[20:], IRP_MN_QUERY_DIRECTORY)
binary.LittleEndian.PutUint32(req[24:], FileBothDirectoryInformation)
req[28] = 1 // InitialQuery
h.Process(req)
if len(fs.complet) != 1 {
t.Fatal("List 未派发")
}
// 桥接返回 2 条目
h.CompleteJSON(fs.complet[0], STATUS_SUCCESS, "list",
`[{"name":"a.txt","size":5,"mtime":1700000000000},{"name":"sub","dir":true,"mtime":1700000000000}]`)
resp := s.msgs[0]
if binary.LittleEndian.Uint32(resp[12:]) != STATUS_SUCCESS {
t.Fatal("首次枚举应成功")
}
dataLen := binary.LittleEndian.Uint32(resp[16:])
data := resp[20:]
if int(dataLen) != len(data)-0 || dataLen == 0 {
t.Fatalf("数据长度字段不一致: %d", dataLen)
}
// 条目链校验:第一条 NextEntryOffset 指向第二条,第二条为 0
first := binary.LittleEndian.Uint32(data[0:])
if first == 0 || int(first)+4 > len(data) {
t.Fatalf("NextEntryOffset 链错误: %d", first)
}
// a.txt 名字长度(UTF-16 字节数)在 class3: FileNameLength @60
nameLen := binary.LittleEndian.Uint32(data[60:])
if nameLen != 10 { // "a.txt" 5 chars × 2
t.Fatalf("FileNameLength=%d", nameLen)
}
second := binary.LittleEndian.Uint32(data[first:])
if second != 0 {
t.Fatalf("末条 NextEntryOffset 应为 0,实 %d", second)
}
// 续枚举(InitialQuery=0):桥接再次返回全量,游标已到末尾 → NO_MORE_FILES
req[28] = 0
h.Process(req)
if len(fs.complet) != 2 {
t.Fatal("续枚举 List 未派发")
}
h.CompleteJSON(fs.complet[1], STATUS_SUCCESS, "list",
`[{"name":"a.txt","size":5,"mtime":1700000000000},{"name":"sub","dir":true,"mtime":1700000000000}]`)
resp = s.msgs[1]
if binary.LittleEndian.Uint32(resp[12:]) != STATUS_NO_MORE_FILES {
t.Fatalf("续枚举应返回 NO_MORE_FILES,实 %#x", binary.LittleEndian.Uint32(resp[12:]))
}
}
func TestReadCompletionEncoding(t *testing.T) {
h, s, _ := newTestHandler()
h.track(9, &pending{deviceID: h.deviceID, fileID: 3, major: IRP_MJ_READ})
h.CompleteBytes(9, STATUS_SUCCESS, []byte("hello"))
resp := s.msgs[0]
if len(resp) != 16+4+5 {
t.Fatalf("READ 响应应 25 字节,实 %d", len(resp))
}
if binary.LittleEndian.Uint32(resp[16:]) != 5 {
t.Fatal("Length 前缀应为 5")
}
if !bytes.Equal(resp[20:], []byte("hello")) {
t.Fatal("数据内容不一致")
}
}
func TestWriteDeniedInReadOnlyMode(t *testing.T) {
h, s, _ := newTestHandler()
req := make([]byte, 24)
binary.LittleEndian.PutUint16(req[0:], RDPDR_CTYP_CORE)
binary.LittleEndian.PutUint16(req[2:], PAKID_CORE_DEVICE_IOREQUEST)
binary.LittleEndian.PutUint32(req[4:], h.deviceID)
binary.LittleEndian.PutUint32(req[12:], 42)
binary.LittleEndian.PutUint32(req[16:], IRP_MJ_WRITE)
h.Process(req)
if len(s.msgs) != 1 {
t.Fatal("WRITE 应直接拒绝")
}
if binary.LittleEndian.Uint32(s.msgs[0][12:]) != STATUS_ACCESS_DENIED {
t.Fatal("WRITE 应回 ACCESS_DENIED")
}
}
func TestDirEntryEncodingLayouts(t *testing.T) {
entries := []DirEntry{{Name: "x", Dir: true, Size: 0, Mtime: 1700000000000}}
// 单条即末条:NextEntryOffset=0,无对齐填充
if d := encodeDirEntries(FileDirectoryInformation, entries); len(d) != 64+2 {
t.Fatalf("class1 单条应为 66,实 %d", len(d))
}
if d := encodeDirEntries(FileBothDirectoryInformation, entries); len(d) != 93+2 {
t.Fatalf("class3 单条应为 95,实 %d", len(d))
}
if d := encodeDirEntries(FileNamesInformation, entries); len(d) != 12+2 {
t.Fatalf("names 单条应为 14,实 %d", len(d))
}
if d := encodeDirEntries(0x99, entries); d != nil {
t.Fatal("未知信息类应返回 nil")
}
// 非末条需 4 字节对齐:两条 class1("x"=2 字节名)→ 68 + 66
two := []DirEntry{{Name: "x", Mtime: 1}, {Name: "y", Mtime: 1}}
if d := encodeDirEntries(FileDirectoryInformation, two); len(d) != 68+66 {
t.Fatalf("class1 两条应 68+66,实 %d", len(d))
}
// FileBasicInformation 40 字节
if d := encodeFileInfo(FileBasicInformation, entries[0]); len(d) != 40 {
t.Fatalf("FileBasicInformation 应 40 字节,实 %d", len(d))
}
// FileFsSizeInformation 24 字节
if d := encodeVolumeInfo(FileFsSizeInformation, "local"); len(d) != 24 {
t.Fatalf("FileFsSizeInformation 应 24 字节,实 %d", len(d))
}
}