init: 自 zomaintain/backend/rdplib 平移独立成库; module path 从上游 github.com/nakagami/grdp 改为 git.zeroonesoft.cn/golib/rdplib
This commit is contained in:
@@ -0,0 +1,256 @@
|
||||
// drive.go — MS-RDPEFS 设备 I/O 响应的编码器:目录枚举条目
|
||||
// ([MS-FSCC] FILE_*_DIRECTORY_INFORMATION)、文件/卷信息结构与 UTF-16
|
||||
// 辅助。时间统一从 JS 侧的毫秒时间戳换算 Windows FILETIME。
|
||||
package rdpdr
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"encoding/json"
|
||||
"unicode/utf16"
|
||||
)
|
||||
|
||||
// DirEntry 是桥接侧返回的单条目录/文件元数据。
|
||||
type DirEntry struct {
|
||||
Name string `json:"name"`
|
||||
Dir bool `json:"dir"`
|
||||
Size int64 `json:"size"`
|
||||
Mtime int64 `json:"mtime"` // 毫秒时间戳(最后写入)
|
||||
Created int64 `json:"created"` // 毫秒时间戳(缺省取 Mtime)
|
||||
Accessed int64 `json:"accessed"` // 毫秒时间戳(缺省取 Mtime)
|
||||
}
|
||||
|
||||
// FSCC 信息类(本实现支持的子集)。
|
||||
const (
|
||||
FileDirectoryInformation = 0x00000001
|
||||
FileFullDirectoryInformation = 0x00000002
|
||||
FileBothDirectoryInformation = 0x00000003
|
||||
FileNamesInformation = 0x0000000C
|
||||
|
||||
FileFsVolumeInformation = 0x00000001
|
||||
FileFsSizeInformation = 0x00000003
|
||||
FileFsDeviceInformation = 0x00000004
|
||||
FileFsAttributeInformation = 0x00000005
|
||||
FileFsFullSizeInformation = 0x00000007
|
||||
|
||||
FileBasicInformation = 0x00000004
|
||||
FileStandardInformation = 0x00000005
|
||||
FileNetworkOpenInformation = 0x00000022
|
||||
)
|
||||
|
||||
// filetime 把毫秒 Unix 时间戳换算为 Windows FILETIME(100ns,1601 纪元)。
|
||||
func filetime(ms int64) uint64 {
|
||||
const epochDelta = 116444736000000000 // 1601→1970 的 100ns 数
|
||||
if ms < 0 {
|
||||
ms = 0
|
||||
}
|
||||
return uint64(ms)*10000 + epochDelta
|
||||
}
|
||||
|
||||
func utf16Bytes(s string) []byte {
|
||||
u := utf16.Encode([]rune(s))
|
||||
b := make([]byte, 2*len(u))
|
||||
for i, v := range u {
|
||||
binary.LittleEndian.PutUint16(b[2*i:], v)
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func utf16ToString(b []byte) string {
|
||||
if len(b)%2 != 0 {
|
||||
b = b[:len(b)-1]
|
||||
}
|
||||
u := make([]uint16, len(b)/2)
|
||||
for i := range u {
|
||||
u[i] = binary.LittleEndian.Uint16(b[2*i:])
|
||||
}
|
||||
runes := utf16.Decode(u)
|
||||
// 去掉结尾 NUL
|
||||
for n := len(runes) - 1; n >= 0; n-- {
|
||||
if runes[n] == 0 {
|
||||
runes = runes[:n]
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
return string(runes)
|
||||
}
|
||||
|
||||
// decodeEntries 解析桥接 JSON 数组。
|
||||
func decodeEntries(jsonStr string) ([]DirEntry, error) {
|
||||
var entries []DirEntry
|
||||
if err := json.Unmarshal([]byte(jsonStr), &entries); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for i := range entries {
|
||||
fillTimes(&entries[i])
|
||||
}
|
||||
return entries, nil
|
||||
}
|
||||
|
||||
// decodeEntry 解析桥接 JSON 单对象。
|
||||
func decodeEntry(jsonStr string) (DirEntry, error) {
|
||||
var e DirEntry
|
||||
if err := json.Unmarshal([]byte(jsonStr), &e); err != nil {
|
||||
return e, err
|
||||
}
|
||||
fillTimes(&e)
|
||||
return e, nil
|
||||
}
|
||||
|
||||
func fillTimes(e *DirEntry) {
|
||||
if e.Created == 0 {
|
||||
e.Created = e.Mtime
|
||||
}
|
||||
if e.Accessed == 0 {
|
||||
e.Accessed = e.Mtime
|
||||
}
|
||||
}
|
||||
|
||||
func (e *DirEntry) attributes() uint32 {
|
||||
if e.Dir {
|
||||
return FILE_ATTRIBUTE_DIRECTORY
|
||||
}
|
||||
if e.Size == 0 {
|
||||
return FILE_ATTRIBUTE_NORMAL
|
||||
}
|
||||
return FILE_ATTRIBUTE_NORMAL | FILE_ATTRIBUTE_ARCHIVE
|
||||
}
|
||||
|
||||
// encodeDirEntries 把一批条目编码为 FSCC 目录信息结构链。
|
||||
// 支持 FileDirectoryInformation(1)/FileBothDirectoryInformation(3)/
|
||||
// FileNamesInformation(0xC);其它返回 nil(上层回 NOT_IMPLEMENTED)。
|
||||
func encodeDirEntries(infoClass uint32, entries []DirEntry) []byte {
|
||||
if len(entries) == 0 {
|
||||
return []byte{}
|
||||
}
|
||||
var out []byte
|
||||
for i := range entries {
|
||||
e := &entries[i]
|
||||
name := utf16Bytes(e.Name)
|
||||
var raw []byte
|
||||
switch infoClass {
|
||||
case FileDirectoryInformation:
|
||||
// NextEntryOffset(4) FileIndex(4) Creation(8) Access(8) Write(8)
|
||||
// Change(8) EndOfFile(8) Allocation(8) Attributes(4) NameLen(4) = 64
|
||||
raw = make([]byte, 64+len(name))
|
||||
putTimes(raw, 8, e)
|
||||
binary.LittleEndian.PutUint64(raw[40:], uint64(e.Size))
|
||||
binary.LittleEndian.PutUint64(raw[48:], uint64(e.Size))
|
||||
binary.LittleEndian.PutUint32(raw[56:], e.attributes())
|
||||
binary.LittleEndian.PutUint32(raw[60:], uint32(len(name)))
|
||||
copy(raw[64:], name)
|
||||
case FileBothDirectoryInformation:
|
||||
// 64 字节同上 + EaSize(4) ShortNameLen(1) ShortName(24) = 93
|
||||
raw = make([]byte, 93+len(name))
|
||||
putTimes(raw, 8, e)
|
||||
binary.LittleEndian.PutUint64(raw[40:], uint64(e.Size))
|
||||
binary.LittleEndian.PutUint64(raw[48:], uint64(e.Size))
|
||||
binary.LittleEndian.PutUint32(raw[56:], e.attributes())
|
||||
binary.LittleEndian.PutUint32(raw[60:], uint32(len(name)))
|
||||
raw[68] = 0 // ShortNameLength
|
||||
// ShortName[24] 全 0(无短名)
|
||||
copy(raw[93:], name)
|
||||
case FileNamesInformation:
|
||||
// NextEntryOffset(4) FileIndex(4) FileNameLength(4) = 12
|
||||
raw = make([]byte, 12+len(name))
|
||||
binary.LittleEndian.PutUint32(raw[8:], uint32(len(name)))
|
||||
copy(raw[12:], name)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
if i == len(entries)-1 {
|
||||
// 末条:NextEntryOffset=0,无需对齐
|
||||
binary.LittleEndian.PutUint32(raw[0:], 0)
|
||||
out = append(out, raw...)
|
||||
continue
|
||||
}
|
||||
// 非末条:长度 4 字节对齐,补零
|
||||
next := (len(raw) + 3) &^ 3
|
||||
buf := make([]byte, next)
|
||||
copy(buf, raw)
|
||||
binary.LittleEndian.PutUint32(buf[0:], uint32(next))
|
||||
out = append(out, buf...)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// putTimes 在 off 处写 Creation/Access/Write/Change 四个 FILETIME(32 字节)。
|
||||
func putTimes(b []byte, off int, e *DirEntry) {
|
||||
binary.LittleEndian.PutUint64(b[off:], filetime(e.Created))
|
||||
binary.LittleEndian.PutUint64(b[off+8:], filetime(e.Accessed))
|
||||
binary.LittleEndian.PutUint64(b[off+16:], filetime(e.Mtime))
|
||||
binary.LittleEndian.PutUint64(b[off+24:], filetime(e.Mtime))
|
||||
}
|
||||
|
||||
// encodeFileInfo 编码 QUERY_INFORMATION 响应体(不含 Length 前缀)。
|
||||
func encodeFileInfo(infoClass uint32, e DirEntry) []byte {
|
||||
switch infoClass {
|
||||
case FileBasicInformation:
|
||||
// Creation(8) Access(8) Write(8) Change(8) Attributes(4) Reserved(4) = 40
|
||||
b := make([]byte, 40)
|
||||
putTimes(b, 0, &e)
|
||||
binary.LittleEndian.PutUint32(b[32:], e.attributes())
|
||||
return b
|
||||
case FileStandardInformation:
|
||||
// AllocationSize(8) EndOfFile(8) NumberOfLinks(4) DeletePending(1) Directory(1) = 22
|
||||
b := make([]byte, 22)
|
||||
binary.LittleEndian.PutUint64(b[0:], uint64(e.Size))
|
||||
binary.LittleEndian.PutUint64(b[8:], uint64(e.Size))
|
||||
binary.LittleEndian.PutUint32(b[16:], 1)
|
||||
b[20] = 0
|
||||
if e.Dir {
|
||||
b[21] = 1
|
||||
}
|
||||
return b
|
||||
case FileNetworkOpenInformation:
|
||||
// Creation(8) Access(8) Write(8) Change(8) Allocation(8) EndOfFile(8) Attributes(4) = 56
|
||||
b := make([]byte, 56)
|
||||
putTimes(b, 0, &e)
|
||||
binary.LittleEndian.PutUint64(b[32:], uint64(e.Size))
|
||||
binary.LittleEndian.PutUint64(b[40:], uint64(e.Size))
|
||||
binary.LittleEndian.PutUint32(b[48:], e.attributes())
|
||||
return b
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// encodeVolumeInfo 编码 QUERY_VOLUME_INFORMATION 响应体(不含 Length 前缀)。
|
||||
func encodeVolumeInfo(infoClass uint32, label string) []byte {
|
||||
switch infoClass {
|
||||
case FileFsVolumeInformation:
|
||||
// VolumeCreationTime(8) SerialNumber(4) LabelLength(4) SupportsObjects(1) Label
|
||||
vol := utf16Bytes(label)
|
||||
b := make([]byte, 17+len(vol))
|
||||
binary.LittleEndian.PutUint32(b[8:], 0x1ABCF2D8) // 任意固定序列号
|
||||
binary.LittleEndian.PutUint32(b[12:], uint32(len(vol)))
|
||||
copy(b[17:], vol)
|
||||
return b
|
||||
case FileFsSizeInformation, FileFsFullSizeInformation:
|
||||
// TotalAllocationUnits(8) Available(8) SectorsPerUnit(4) BytesPerSector(4) = 24
|
||||
// (Win10 服务器挂载设备时常探测 FullSize——缺失会致设备"不支持")
|
||||
b := make([]byte, 24)
|
||||
binary.LittleEndian.PutUint64(b[0:], 0x00100000)
|
||||
binary.LittleEndian.PutUint64(b[8:], 0x00080000)
|
||||
binary.LittleEndian.PutUint32(b[16:], 8)
|
||||
binary.LittleEndian.PutUint32(b[20:], 512)
|
||||
return b
|
||||
case FileFsDeviceInformation:
|
||||
// DeviceType(4)=FILE_DEVICE_DISK Characteristics(4) = 8
|
||||
b := make([]byte, 8)
|
||||
binary.LittleEndian.PutUint32(b[0:], 7)
|
||||
return b
|
||||
case FileFsAttributeInformation:
|
||||
// Attributes(4) MaxComponentLen(4) NameLength(4) Name("FAT32",规避
|
||||
// 服务端按 NTFS 语义发起的 ACL/重解析点等操作——mstsc/FreeRDP 同款选择)
|
||||
fs := utf16Bytes("FAT32")
|
||||
b := make([]byte, 12+len(fs))
|
||||
binary.LittleEndian.PutUint32(b[0:], 0x00000007) // CASE_SENSITIVE_SEARCH|CASE_PRESERVED_NAMES|UNICODE_ON_DISK
|
||||
binary.LittleEndian.PutUint32(b[4:], 255)
|
||||
binary.LittleEndian.PutUint32(b[8:], uint32(len(fs)))
|
||||
copy(b[12:], fs)
|
||||
return b
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,695 @@
|
||||
// Package rdpdr implements the client side of the RDP File System Virtual
|
||||
// Channel Extension (MS-RDPEFS, channel name "rdpdr") for drive redirection:
|
||||
// the server sees a file-system device backed by an asynchronous bridge
|
||||
// (browser-picked folder), and enumerates/reads it through device I/O
|
||||
// requests.
|
||||
//
|
||||
// The bridge is asynchronous by necessity (browser filesystem operations are
|
||||
// promise-based): each Device I/O Request is dispatched to the Filesystem
|
||||
// implementation together with its completionId, and the result — or failure —
|
||||
// is fed back via CompleteStatus/CompleteBytes/CompleteJSON, which emit the
|
||||
// matching Device I/O Response with the proper wire encoding.
|
||||
package rdpdr
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"log/slog"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"git.zeroonesoft.cn/golib/rdplib/core"
|
||||
)
|
||||
|
||||
// ── 常量(MS-RDPEFS 2.2,与 FreeRDP channels/rdpdr.h 对齐)────────────────
|
||||
|
||||
const (
|
||||
RDPDR_CTYP_CORE = 0x4472
|
||||
RDPDR_CTYP_PRN = 0x5052
|
||||
)
|
||||
|
||||
const (
|
||||
PAKID_CORE_SERVER_ANNOUNCE = 0x496E
|
||||
PAKID_CORE_CLIENTID_CONFIRM = 0x4343 // 双向:客户端 Announce Reply / 服务端 Confirm
|
||||
PAKID_CORE_CLIENT_NAME = 0x434E
|
||||
PAKID_CORE_DEVICELIST_ANNOUNCE = 0x4441
|
||||
PAKID_CORE_DEVICE_REPLY = 0x6472
|
||||
PAKID_CORE_DEVICE_IOREQUEST = 0x4952
|
||||
PAKID_CORE_DEVICE_IOCOMPLETION = 0x4943
|
||||
PAKID_CORE_SERVER_CAPABILITY = 0x5350
|
||||
PAKID_CORE_CLIENT_CAPABILITY = 0x4350
|
||||
PAKID_CORE_DEVICELIST_REMOVE = 0x444D
|
||||
PAKID_CORE_USER_LOGGEDON = 0x554C
|
||||
)
|
||||
|
||||
const (
|
||||
CAP_GENERAL_TYPE = 0x0001
|
||||
CAP_DRIVE_TYPE = 0x0004
|
||||
)
|
||||
|
||||
// 协议版本与能力位(与 FreeRDP channels/rdpdr.h / rdpdr_capabilities.c 对齐)。
|
||||
const (
|
||||
RDPDR_VERSION_MINOR_RDP51 = 0x0005
|
||||
RDPDR_VERSION_MINOR_RDP10X = 0x000D // 客户端响应版本上限(FreeRDP rdpdr_main.c MIN 上限)
|
||||
|
||||
// GENERAL capset 的 ExtendedPDU 能力位(MS-RDPEFS 2.2.2.1)
|
||||
RDPDR_DEVICE_REMOVE_PDUS = 0x00000001
|
||||
RDPDR_CLIENT_DISPLAY_NAME_PDU = 0x00000002
|
||||
RDPDR_USER_LOGGEDON_PDU = 0x00000004
|
||||
// GENERAL capset 的 extraFlags1
|
||||
RDPDR_ENABLE_ASYNCIO = 0x00000001
|
||||
)
|
||||
|
||||
// clientIOCode1 是能力响应里宣告的 IRP major 码位掩码——取 FreeRDP 同款
|
||||
// 全集(含本实现未细分的 CLEANUP/FLUSH/SHUTDOWN/LOCK/SECURITY 等,这些
|
||||
// 会以 STATUS_NOT_IMPLEMENTED 兜底完成),与服务端 ioCode1 求交后回给服务端。
|
||||
const clientIOCode1 = 1<<IRP_MJ_CREATE | 1<<IRP_MJ_CLEANUP | 1<<IRP_MJ_CLOSE |
|
||||
1<<IRP_MJ_READ | 1<<IRP_MJ_WRITE | 1<<IRP_MJ_QUERY_INFORMATION |
|
||||
1<<IRP_MJ_SET_INFORMATION | 1<<IRP_MJ_FLUSH_BUFFERS |
|
||||
1<<IRP_MJ_QUERY_VOLUME_INFORMATION | 1<<IRP_MJ_SET_VOLUME_INFORMATION |
|
||||
1<<IRP_MJ_DIRECTORY_CONTROL | 1<<IRP_MJ_DEVICE_CONTROL |
|
||||
1<<IRP_MJ_SHUTDOWN | 1<<IRP_MJ_LOCK_CONTROL |
|
||||
1<<IRP_MJ_QUERY_SECURITY | 1<<IRP_MJ_SET_SECURITY
|
||||
|
||||
// IRP major/minor function codes.
|
||||
const (
|
||||
IRP_MJ_CREATE = 0x00000000
|
||||
IRP_MJ_CLEANUP = 0x00000001
|
||||
IRP_MJ_CLOSE = 0x00000002
|
||||
IRP_MJ_READ = 0x00000003
|
||||
IRP_MJ_WRITE = 0x00000004
|
||||
IRP_MJ_QUERY_INFORMATION = 0x00000005
|
||||
IRP_MJ_SET_INFORMATION = 0x00000006
|
||||
IRP_MJ_FLUSH_BUFFERS = 0x00000007
|
||||
IRP_MJ_QUERY_VOLUME_INFORMATION = 0x0000000A
|
||||
IRP_MJ_SET_VOLUME_INFORMATION = 0x0000000B
|
||||
IRP_MJ_DIRECTORY_CONTROL = 0x0000000C
|
||||
IRP_MJ_DEVICE_CONTROL = 0x0000000E
|
||||
IRP_MJ_SHUTDOWN = 0x00000010
|
||||
IRP_MJ_LOCK_CONTROL = 0x00000011
|
||||
IRP_MJ_QUERY_SECURITY = 0x00000012
|
||||
IRP_MJ_SET_SECURITY = 0x00000013
|
||||
|
||||
IRP_MN_QUERY_DIRECTORY = 0x00000001
|
||||
IRP_MN_NOTIFY_CHANGE_DIRECTORY = 0x00000002
|
||||
)
|
||||
|
||||
// NTSTATUS codes used in completions.
|
||||
const (
|
||||
STATUS_SUCCESS = 0x00000000
|
||||
STATUS_INVALID_PARAMETER = 0xC000000D
|
||||
STATUS_NOT_IMPLEMENTED = 0xC0000002
|
||||
STATUS_ACCESS_DENIED = 0xC0000022
|
||||
STATUS_NO_SUCH_FILE = 0xC000000F
|
||||
STATUS_NO_MORE_FILES = 0x80000006
|
||||
STATUS_DEVICE_NOT_READY = 0xC00000A5
|
||||
STATUS_FILE_IS_A_DIRECTORY = 0xC00000BA
|
||||
STATUS_NOT_A_DIRECTORY = 0xC0000103
|
||||
STATUS_OBJECT_NAME_NOT_FOUND = 0xC0000034
|
||||
)
|
||||
|
||||
// FILE_ATTRIBUTE_* flags(M1 上报子集:目录/常规/只读)。
|
||||
const (
|
||||
FILE_ATTRIBUTE_READONLY = 0x01
|
||||
FILE_ATTRIBUTE_DIRECTORY = 0x10
|
||||
FILE_ATTRIBUTE_ARCHIVE = 0x20
|
||||
FILE_ATTRIBUTE_NORMAL = 0x80
|
||||
)
|
||||
|
||||
// RDPDR_DTYP_FILESYSTEM 是驱动器重定向宣告的设备类型。
|
||||
const RDPDR_DTYP_FILESYSTEM = 0x00000008
|
||||
|
||||
// pending 记录一次已下发桥接、尚未完成的 IO:完成时按 MajorFunction 与
|
||||
// 信息类选择正确的响应编码。
|
||||
type pending struct {
|
||||
deviceID uint32
|
||||
fileID uint32
|
||||
major uint32
|
||||
info uint32 // FsInformationClass(QUERY_*/DIRECTORY 用)
|
||||
}
|
||||
|
||||
// Filesystem 是浏览器侧异步文件系统的桥接接口。每个方法都带发起时的
|
||||
// completionId,执行完毕后必须经 Handler 的 Complete* 回调送回结果。
|
||||
type Filesystem interface {
|
||||
// Open 打开 path('/' 分隔、UTF-8;"" 或 "/" 为设备根目录),句柄由
|
||||
// 桥接侧以 fileID 标识(该号在派发前已分配)。
|
||||
Open(completionID, fileID uint32, path string)
|
||||
// Read 从已打开文件读最多 length 字节。
|
||||
Read(completionID, fileID uint32, offset uint64, length uint32)
|
||||
// Close 关闭句柄(只回状态)。
|
||||
Close(completionID, fileID uint32)
|
||||
// List 枚举目录全量条目(initial 仅为语义提示,续枚举由 Handler 分页)。
|
||||
List(completionID, fileID uint32, initial bool)
|
||||
// Stat 查询已打开文件元数据(QUERY_INFORMATION)。
|
||||
Stat(completionID, fileID uint32)
|
||||
// Volume 查询卷信息(编码按信息类在 Complete 里区分)。
|
||||
Volume(completionID uint32, infoClass uint32)
|
||||
}
|
||||
|
||||
// Handler 实现 plugin.ChannelTransport("rdpdr" 静态虚拟通道)。
|
||||
type Handler struct {
|
||||
channelSender core.ChannelSender
|
||||
|
||||
deviceID uint32
|
||||
devName string // 共享名:DosName 取前 8 字符,DeviceData 为全名
|
||||
label string // 卷标
|
||||
|
||||
versionMajor uint16
|
||||
versionMinor uint16
|
||||
clientID uint32
|
||||
serverIOCode1 uint32 // 服务端能力集宣告的 ioCode1(能力响应时求交)
|
||||
|
||||
// 设备列表宣告时序:仅 USER_LOGGEDON 后宣告一次。这是 FreeRDP 在
|
||||
// 1.0x 版本语义下的实际行为(其 rdpdr_send_device_list_announce_request
|
||||
// 在非登录阶段会跳过文件系统设备,count=0 连报文都不发)。实测仅登录
|
||||
// 后宣告时设备可正常安装(DEVICE_REPLY result=0),但服务端不建立
|
||||
// \\tsclient\<名> 的 RDPNP 共享映射——访问报"无法访问"且零通道 IO,
|
||||
// 见 doc/history/stage6-plan.md(RDPDR-1/2 章节)。
|
||||
clientIDConfirmed bool
|
||||
deviceListSent bool
|
||||
|
||||
fs Filesystem
|
||||
|
||||
nextFileID uint32
|
||||
pendingMu sync.Mutex
|
||||
pending map[uint32]*pending // completionId → pending
|
||||
|
||||
enumMu sync.Mutex
|
||||
enumPos map[uint32]int // FileId → 已返回条目数(目录枚举分页游标)
|
||||
}
|
||||
|
||||
// clientComputerName 是 CLIENT_NAME_REQUEST 里上报的客户端机器名。取值
|
||||
// "tsclient" 与 M1 端到端验收通过(2026-09-12 08:25,10.0.0.3)时的取值
|
||||
// 一致;后改为 "grdpclient" 的会话全部失败。因当时 CLOSE 探测错误会独立
|
||||
// 导致映射被撤销,两个变量未分离验证,先回退到已知良好值(注释勿
|
||||
// overclaim:名字的独立影响待 CLOSE 修复验证后再做 A/B)。
|
||||
const clientComputerName = "tsclient"
|
||||
|
||||
// NewHandler 创建处理器。shareName 是共享名:DEVICE_ANNOUNCE 的 DosName 取
|
||||
// 其前 8 字符(FreeRDP 同款),DeviceData 为 ASCII 全名 + NUL——服务端按
|
||||
// DosName 建 \\tsclient\<DosName> 的 UNC 映射。label 是卷标(可含中文)。
|
||||
func NewHandler(shareName, label string) *Handler {
|
||||
shareName = sanitizeShareName(shareName)
|
||||
return &Handler{
|
||||
devName: shareName,
|
||||
label: label,
|
||||
versionMinor: RDPDR_VERSION_MINOR_RDP10X,
|
||||
deviceID: 1, // DeviceId 从 1 起(FreeRDP 同款;0 可能与服务端内部路由冲突)
|
||||
nextFileID: 1,
|
||||
pending: make(map[uint32]*pending),
|
||||
enumPos: make(map[uint32]int),
|
||||
}
|
||||
}
|
||||
|
||||
// SetFilesystem 挂接异步文件系统桥(连接前调用)。
|
||||
func (h *Handler) SetFilesystem(fs Filesystem) { h.fs = fs }
|
||||
|
||||
// min16 返回较小者(Go 1.21 前无泛型 min,wasm 目标锁定旧工具链时需要)。
|
||||
func min16(a, b uint16) uint16 {
|
||||
if a < b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// sanitizeShareName 把共享名中 DEVICE_ANNOUNCE 不允许的字符替换为 '_'
|
||||
//(MS-RDPEFS 2.2.1.3;FreeRDP drive_main.c 同款过滤表,含空格与逗号)。
|
||||
func sanitizeShareName(s string) string {
|
||||
const forbidden = `\/:*?"<>|, ` + "\t"
|
||||
r := []rune(s)
|
||||
for i, c := range r {
|
||||
if strings.ContainsRune(forbidden, c) {
|
||||
r[i] = '_'
|
||||
}
|
||||
}
|
||||
return string(r)
|
||||
}
|
||||
|
||||
// SetDeviceID 指定宣告用的 DeviceId(默认 1)。
|
||||
func (h *Handler) SetDeviceID(id uint32) { h.deviceID = id }
|
||||
|
||||
// GetType 实现 plugin.ChannelTransport。
|
||||
func (h *Handler) GetType() (string, uint32) {
|
||||
return "rdpdr", 0x80000000 | 0x40000000 | 0x00800000 // INITIALIZED|ENCRYPT_RDP|COMPRESS_RDP
|
||||
}
|
||||
|
||||
// Sender 实现 plugin.ChannelTransport。
|
||||
func (h *Handler) Sender(s core.ChannelSender) { h.channelSender = s }
|
||||
|
||||
func (h *Handler) send(b []byte) {
|
||||
if h.channelSender == nil {
|
||||
return
|
||||
}
|
||||
if _, err := h.channelSender.SendToChannel("rdpdr", b); err != nil {
|
||||
slog.Warn("rdpdr send", "err", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Process 实现 plugin.ChannelTransport:分发服务端消息。
|
||||
func (h *Handler) Process(data []byte) {
|
||||
if len(data) < 4 {
|
||||
return
|
||||
}
|
||||
component := binary.LittleEndian.Uint16(data[0:])
|
||||
packetID := binary.LittleEndian.Uint16(data[2:])
|
||||
if component != RDPDR_CTYP_CORE {
|
||||
slog.Debug("rdpdr: non-core component", "component", component, "packetID", packetID)
|
||||
return
|
||||
}
|
||||
switch packetID {
|
||||
case PAKID_CORE_SERVER_ANNOUNCE:
|
||||
h.processServerAnnounce(data)
|
||||
case PAKID_CORE_CLIENTID_CONFIRM:
|
||||
// 服务端回显其接受的版本与 ClientId(FreeRDP 采纳该版本),
|
||||
// 1.0x 语义下设备列表等 USER_LOGGEDON 再发
|
||||
if len(data) >= 12 {
|
||||
h.versionMajor = binary.LittleEndian.Uint16(data[4:])
|
||||
h.versionMinor = binary.LittleEndian.Uint16(data[6:])
|
||||
h.clientID = binary.LittleEndian.Uint32(data[8:])
|
||||
}
|
||||
slog.Debug("rdpdr: server clientid confirm",
|
||||
"major", h.versionMajor, "minor", h.versionMinor, "clientID", h.clientID)
|
||||
h.clientIDConfirmed = true
|
||||
case PAKID_CORE_USER_LOGGEDON:
|
||||
slog.Debug("rdpdr: user loggedon")
|
||||
if h.clientIDConfirmed && !h.deviceListSent {
|
||||
h.sendDeviceList()
|
||||
}
|
||||
case PAKID_CORE_DEVICE_REPLY:
|
||||
if len(data) >= 12 {
|
||||
slog.Debug("rdpdr: device reply", "deviceID", binary.LittleEndian.Uint32(data[4:]),
|
||||
"result", binary.LittleEndian.Uint32(data[8:]))
|
||||
}
|
||||
case PAKID_CORE_SERVER_CAPABILITY:
|
||||
h.processServerCapability(data)
|
||||
case PAKID_CORE_DEVICE_IOREQUEST:
|
||||
h.processIORequest(data)
|
||||
default:
|
||||
slog.Debug("rdpdr: unhandled", "packetID", packetID, "len", len(data))
|
||||
}
|
||||
}
|
||||
|
||||
// ── 握手:announce reply → name request → 设备列表 ───────────────────────
|
||||
|
||||
func (h *Handler) processServerAnnounce(data []byte) {
|
||||
if len(data) < 12 {
|
||||
return
|
||||
}
|
||||
h.versionMajor = binary.LittleEndian.Uint16(data[4:])
|
||||
h.versionMinor = binary.LittleEndian.Uint16(data[6:])
|
||||
h.clientID = binary.LittleEndian.Uint32(data[8:])
|
||||
slog.Debug("rdpdr: server announce", "major", h.versionMajor,
|
||||
"minor", h.versionMinor, "clientID", h.clientID)
|
||||
|
||||
// 客户端响应版本取 min(自身上限, 服务端)(FreeRDP rdpdr_main.c 同款):
|
||||
// major 上限 1,minor 上限 0x000D。旧实现回 1.5(RDP5.1 时代语义)与
|
||||
// GENERAL 能力集布局错位,均已按 FreeRDP 源码修正。
|
||||
h.versionMajor = min16(1, h.versionMajor)
|
||||
h.versionMinor = min16(RDPDR_VERSION_MINOR_RDP10X, h.versionMinor)
|
||||
|
||||
// Client Announce Reply:VersionMajor(2) VersionMinor(2) ClientId(4)
|
||||
b := make([]byte, 12)
|
||||
binary.LittleEndian.PutUint16(b[0:], RDPDR_CTYP_CORE)
|
||||
binary.LittleEndian.PutUint16(b[2:], PAKID_CORE_CLIENTID_CONFIRM)
|
||||
binary.LittleEndian.PutUint16(b[4:], h.versionMajor)
|
||||
binary.LittleEndian.PutUint16(b[6:], h.versionMinor)
|
||||
binary.LittleEndian.PutUint32(b[8:], h.clientID)
|
||||
h.send(b)
|
||||
|
||||
// Client Name Request:UnicodeFlag(4)=1 CodePage(4)=0
|
||||
// ComputerNameLen(4,含 NUL) ComputerName(UTF-16LE, NUL 结尾)
|
||||
// 机器名取值依据见 clientComputerName 注释(M1 已知良好值 "tsclient";
|
||||
// "grdpclient" 变体失败与 CLOSE 探测错误混在一起,未分离归因)。
|
||||
uname := append(utf16Bytes(clientComputerName), 0, 0)
|
||||
b2 := make([]byte, 16+len(uname))
|
||||
binary.LittleEndian.PutUint16(b2[0:], RDPDR_CTYP_CORE)
|
||||
binary.LittleEndian.PutUint16(b2[2:], PAKID_CORE_CLIENT_NAME)
|
||||
binary.LittleEndian.PutUint32(b2[4:], 1) // UnicodeFlag
|
||||
binary.LittleEndian.PutUint32(b2[8:], 0) // CodePage
|
||||
binary.LittleEndian.PutUint32(b2[12:], uint32(len(uname)))
|
||||
copy(b2[16:], uname)
|
||||
h.send(b2)
|
||||
|
||||
// 设备列表在 USER_LOGGEDON(且 CLIENTID_CONFIRM 已到)后发
|
||||
// (MS-RDPEFS 3.2.5.1.3;提前宣告服务端报 0xC0000001 拒装设备)
|
||||
}
|
||||
|
||||
func (h *Handler) sendDeviceList() {
|
||||
h.deviceListSent = true
|
||||
// DEVICE_ANNOUNCE{DeviceType(4) DeviceId(4) DosName(8) DeviceDataLength(4)
|
||||
// DeviceData},与 FreeRDP drive_main.c/rdpdr_main.c 逐字节对齐:
|
||||
// DosName = 共享名前 8 字节(短则 NUL 填充,高位字节替换 '_');
|
||||
// DeviceData = ASCII 全名 + 1 字节 NUL(FreeRDP 同款,Win10 接受;
|
||||
// V02 协商下 DeviceDataLength 为 0 会遭服务端 0xC0000001 拒装)。
|
||||
dos := []byte(h.devName)
|
||||
if len(dos) > 8 {
|
||||
dos = dos[:8]
|
||||
}
|
||||
for i := range dos {
|
||||
if dos[i] > 0x7F {
|
||||
dos[i] = '_'
|
||||
}
|
||||
}
|
||||
var dosName [8]byte
|
||||
copy(dosName[:], dos)
|
||||
devData := append([]byte(h.devName), 0)
|
||||
// 头 4 + DeviceCount 4 + DeviceType 4 + DeviceId 4 + DosName 8 +
|
||||
// DeviceDataLength 4 = 28,随后 DeviceData
|
||||
b := make([]byte, 28+len(devData))
|
||||
binary.LittleEndian.PutUint16(b[0:], RDPDR_CTYP_CORE)
|
||||
binary.LittleEndian.PutUint16(b[2:], PAKID_CORE_DEVICELIST_ANNOUNCE)
|
||||
binary.LittleEndian.PutUint32(b[4:], 1) // DeviceCount
|
||||
binary.LittleEndian.PutUint32(b[8:], RDPDR_DTYP_FILESYSTEM)
|
||||
binary.LittleEndian.PutUint32(b[12:], h.deviceID)
|
||||
copy(b[16:], dosName[:])
|
||||
binary.LittleEndian.PutUint32(b[24:], uint32(len(devData)))
|
||||
copy(b[28:], devData)
|
||||
h.send(b)
|
||||
slog.Debug("rdpdr: device list announced", "name", h.devName, "deviceID", h.deviceID,
|
||||
"devDataLen", len(devData))
|
||||
}
|
||||
|
||||
// ── 能力协商 ─────────────────────────────────────────────────────────────
|
||||
|
||||
func (h *Handler) processServerCapability(data []byte) {
|
||||
// 解析服务端 GENERAL capset,取 ioCode1(能力响应须与之求交)
|
||||
if len(data) >= 12 {
|
||||
// Server Capability:头 8 + numCapabilities(2) + Padding(2),随后 capset 列表
|
||||
off := 8
|
||||
for off+8 <= len(data) {
|
||||
typ := binary.LittleEndian.Uint16(data[off:])
|
||||
length := int(binary.LittleEndian.Uint16(data[off+2:]))
|
||||
if length < 8 || off+length > len(data) {
|
||||
break
|
||||
}
|
||||
if typ == CAP_GENERAL_TYPE && length >= 44 {
|
||||
// capset 内:osType(4) osVersion(4) protoMajor(2) protoMinor(2)
|
||||
// ioCode1(4) ioCode2(4) extendedPDU(4) ...
|
||||
h.serverIOCode1 = binary.LittleEndian.Uint32(data[off+20:])
|
||||
slog.Debug("rdpdr: server caps", "num", binary.LittleEndian.Uint16(data[4:]),
|
||||
"ioCode1", h.serverIOCode1,
|
||||
"extendedPDU", binary.LittleEndian.Uint32(data[off+28:]))
|
||||
}
|
||||
off += length
|
||||
}
|
||||
}
|
||||
|
||||
// Client Core Capability Response(FreeRDP rdpdr_capabilities.c 逐字节对齐):
|
||||
// 头 8 + GENERAL 44 + DRIVE 8 = 60。
|
||||
// 旧实现三处错误会使服务端拒建共享映射:GENERAL 的协议版本误写 4 字节
|
||||
//(服务端解析成 major=5 minor=0 ioCode1=0)、DRIVE capset 声明 10 写 12、
|
||||
// ioCode1=0/extendedPDU=4/extraFlags1=0。
|
||||
b := make([]byte, 8+44+8)
|
||||
binary.LittleEndian.PutUint16(b[0:], RDPDR_CTYP_CORE)
|
||||
binary.LittleEndian.PutUint16(b[2:], PAKID_CORE_CLIENT_CAPABILITY)
|
||||
binary.LittleEndian.PutUint16(b[4:], 2) // numCapabilities
|
||||
binary.LittleEndian.PutUint16(b[6:], 0) // Padding
|
||||
|
||||
// GENERAL:header(2+2+4) + osType(4) osVersion(4) protoMajor(2) protoMinor(2)
|
||||
// ioCode1(4) ioCode2(4) extendedPDU(4) extraFlags1(4) extraFlags2(4)
|
||||
// specialTypeDeviceCap(4) = 44
|
||||
binary.LittleEndian.PutUint16(b[8:], CAP_GENERAL_TYPE)
|
||||
binary.LittleEndian.PutUint16(b[10:], 44)
|
||||
binary.LittleEndian.PutUint32(b[12:], 2) // Version = GENERAL_CAPABILITY_VERSION_02
|
||||
// OsType @16 = 0, OsVersion @20 = 0
|
||||
binary.LittleEndian.PutUint16(b[24:], h.versionMajor)
|
||||
binary.LittleEndian.PutUint16(b[26:], h.versionMinor)
|
||||
binary.LittleEndian.PutUint32(b[28:], clientIOCode1&h.serverIOCode1)
|
||||
// IoCode2 @32 = 0
|
||||
binary.LittleEndian.PutUint32(b[36:], RDPDR_DEVICE_REMOVE_PDUS|
|
||||
RDPDR_CLIENT_DISPLAY_NAME_PDU|RDPDR_USER_LOGGEDON_PDU)
|
||||
binary.LittleEndian.PutUint32(b[40:], RDPDR_ENABLE_ASYNCIO)
|
||||
// ExtraFlags2 @44 = 0, SpecialTypeDeviceCap @48 = 0
|
||||
|
||||
// DRIVE:仅 8 字节头 {type, CapabilityLength=8, Version=2},无额外字段
|
||||
binary.LittleEndian.PutUint16(b[52:], CAP_DRIVE_TYPE)
|
||||
binary.LittleEndian.PutUint16(b[54:], 8)
|
||||
binary.LittleEndian.PutUint32(b[56:], 2)
|
||||
|
||||
h.send(b)
|
||||
slog.Debug("rdpdr: client caps sent")
|
||||
}
|
||||
|
||||
// ── Device I/O Request 分发 ──────────────────────────────────────────────
|
||||
|
||||
func (h *Handler) processIORequest(data []byte) {
|
||||
if len(data) < 24 {
|
||||
return
|
||||
}
|
||||
deviceID := binary.LittleEndian.Uint32(data[4:])
|
||||
fileID := binary.LittleEndian.Uint32(data[8:])
|
||||
completionID := binary.LittleEndian.Uint32(data[12:])
|
||||
major := binary.LittleEndian.Uint32(data[16:])
|
||||
minor := binary.LittleEndian.Uint32(data[20:])
|
||||
if deviceID != h.deviceID {
|
||||
slog.Warn("rdpdr: io for unknown device", "deviceID", deviceID)
|
||||
return
|
||||
}
|
||||
p := &pending{deviceID: deviceID, fileID: fileID, major: major}
|
||||
slog.Debug("rdpdr: io", "fileID", fileID, "completion", completionID,
|
||||
"major", major, "minor", minor, "len", len(data))
|
||||
|
||||
switch major {
|
||||
case IRP_MJ_CREATE:
|
||||
// 头 24 + DesiredAccess(4) AllocationSize(8) FileAttributes(4)
|
||||
// SharedAccess(4) CreateDisposition(4) CreateOptions(4) PathLength(4) = 56
|
||||
if len(data) < 56 {
|
||||
h.completeStatus(completionID, p, STATUS_INVALID_PARAMETER, nil)
|
||||
return
|
||||
}
|
||||
pathLen := int(binary.LittleEndian.Uint32(data[52:]))
|
||||
path := ""
|
||||
if pathLen > 0 && 56+pathLen <= len(data) {
|
||||
path = utf16ToString(data[56 : 56+pathLen])
|
||||
}
|
||||
// FileId 由客户端在 CREATE 时分配(服务端请求里的 FileId 为 0),
|
||||
// 后续所有 IO 携带该号,桥接侧以此为句柄键。
|
||||
p.fileID = h.allocFileID()
|
||||
h.track(completionID, p)
|
||||
if h.fs != nil {
|
||||
h.fs.Open(completionID, p.fileID, path)
|
||||
} else {
|
||||
h.takePending(completionID)
|
||||
h.completeStatus(completionID, p, STATUS_DEVICE_NOT_READY, nil)
|
||||
}
|
||||
case IRP_MJ_READ:
|
||||
if len(data) < 24+4+8 {
|
||||
h.completeStatus(completionID, p, STATUS_INVALID_PARAMETER, nil)
|
||||
return
|
||||
}
|
||||
length := binary.LittleEndian.Uint32(data[24:])
|
||||
offset := binary.LittleEndian.Uint64(data[28:])
|
||||
h.track(completionID, p)
|
||||
if h.fs != nil {
|
||||
h.fs.Read(completionID, fileID, offset, length)
|
||||
}
|
||||
case IRP_MJ_CLOSE:
|
||||
h.track(completionID, p)
|
||||
if h.fs != nil {
|
||||
h.fs.Close(completionID, fileID)
|
||||
}
|
||||
case IRP_MJ_DIRECTORY_CONTROL:
|
||||
if minor == IRP_MN_QUERY_DIRECTORY && len(data) >= 56 {
|
||||
p.info = binary.LittleEndian.Uint32(data[24:])
|
||||
initial := data[28] != 0
|
||||
if initial {
|
||||
h.enumMu.Lock()
|
||||
h.enumPos[fileID] = 0
|
||||
h.enumMu.Unlock()
|
||||
}
|
||||
h.track(completionID, p)
|
||||
if h.fs != nil {
|
||||
h.fs.List(completionID, fileID, initial)
|
||||
}
|
||||
} else {
|
||||
// 变更通知:声明不支持,服务端退化为轮询
|
||||
h.completeStatus(completionID, p, STATUS_NOT_IMPLEMENTED, nil)
|
||||
}
|
||||
case IRP_MJ_QUERY_VOLUME_INFORMATION:
|
||||
if len(data) < 24+4 {
|
||||
h.completeStatus(completionID, p, STATUS_INVALID_PARAMETER, nil)
|
||||
return
|
||||
}
|
||||
p.info = binary.LittleEndian.Uint32(data[24:])
|
||||
h.track(completionID, p)
|
||||
if h.fs != nil {
|
||||
h.fs.Volume(completionID, p.info)
|
||||
}
|
||||
case IRP_MJ_QUERY_INFORMATION:
|
||||
if len(data) < 24+4 {
|
||||
h.completeStatus(completionID, p, STATUS_INVALID_PARAMETER, nil)
|
||||
return
|
||||
}
|
||||
p.info = binary.LittleEndian.Uint32(data[24:])
|
||||
h.track(completionID, p)
|
||||
if h.fs != nil {
|
||||
h.fs.Stat(completionID, fileID)
|
||||
}
|
||||
case IRP_MJ_WRITE, IRP_MJ_SET_INFORMATION:
|
||||
// 只读阶段(M1):明确拒绝写路径
|
||||
h.completeStatus(completionID, p, STATUS_ACCESS_DENIED, nil)
|
||||
case IRP_MJ_DEVICE_CONTROL, IRP_MJ_LOCK_CONTROL:
|
||||
// FreeRDP 的 Discard 语义:未实现的 FSCTL/锁请求以 SUCCESS 空数据
|
||||
// 完成——NOT_IMPLEMENTED 会让服务端把整个设备标记为"不支持"。
|
||||
h.completeStatus(completionID, p, STATUS_SUCCESS, nil)
|
||||
default:
|
||||
slog.Debug("rdpdr: unhandled IRP", "major", major)
|
||||
h.completeStatus(completionID, p, STATUS_NOT_IMPLEMENTED, nil)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) track(completionID uint32, p *pending) {
|
||||
h.pendingMu.Lock()
|
||||
h.pending[completionID] = p
|
||||
h.pendingMu.Unlock()
|
||||
}
|
||||
|
||||
func (h *Handler) takePending(completionID uint32) *pending {
|
||||
h.pendingMu.Lock()
|
||||
p := h.pending[completionID]
|
||||
delete(h.pending, completionID)
|
||||
h.pendingMu.Unlock()
|
||||
return p
|
||||
}
|
||||
|
||||
// ── 完成回调(由异步桥在 wasm 侧调用)────────────────────────────────────
|
||||
|
||||
func (h *Handler) completeStatus(completionID uint32, p *pending, status uint32, extra []byte) {
|
||||
if p == nil {
|
||||
p = &pending{}
|
||||
}
|
||||
if p.major == IRP_MJ_CLOSE {
|
||||
h.enumMu.Lock()
|
||||
delete(h.enumPos, p.fileID)
|
||||
h.enumMu.Unlock()
|
||||
// Device Close Response 带固定 5 字节零 Padding(MS-RDPEFS
|
||||
// 2.2.1.4.4;FreeRDP drive_process_irp_close 同款 Stream_Zero(5))。
|
||||
// 缺 padding 的 16 字节短响应虽在浏览期被服务端容忍,但为与服务端
|
||||
// 探测解析器逐字节一致(bb998f3 只修正了状态码,长度仍与 FreeRDP
|
||||
// 不同),这里统一补齐。
|
||||
extra = append(extra, 0, 0, 0, 0, 0)
|
||||
}
|
||||
slog.Debug("rdpdr: complete", "completion", completionID, "major", p.major,
|
||||
"info", p.info, "status", status, "extra", len(extra))
|
||||
b := make([]byte, 16, 16+len(extra))
|
||||
binary.LittleEndian.PutUint16(b[0:], RDPDR_CTYP_CORE)
|
||||
binary.LittleEndian.PutUint16(b[2:], PAKID_CORE_DEVICE_IOCOMPLETION)
|
||||
binary.LittleEndian.PutUint32(b[4:], p.deviceID)
|
||||
binary.LittleEndian.PutUint32(b[8:], completionID)
|
||||
binary.LittleEndian.PutUint32(b[12:], status)
|
||||
b = append(b, extra...)
|
||||
h.send(b)
|
||||
}
|
||||
|
||||
// CompleteStatus 完成一个纯状态响应(CLOSE/错误路径等)。
|
||||
func (h *Handler) CompleteStatus(completionID uint32, status uint32) {
|
||||
h.completeStatus(completionID, h.takePending(completionID), status, nil)
|
||||
}
|
||||
|
||||
// CompleteBytes 完成带数据块的响应(READ:Length(4) 前缀 + 数据)。
|
||||
func (h *Handler) CompleteBytes(completionID uint32, status uint32, data []byte) {
|
||||
p := h.takePending(completionID)
|
||||
extra := make([]byte, 4+len(data))
|
||||
binary.LittleEndian.PutUint32(extra, uint32(len(data)))
|
||||
copy(extra[4:], data)
|
||||
h.completeStatus(completionID, p, status, extra)
|
||||
}
|
||||
|
||||
// CompleteJSON 完成结构化响应;kind 决定编码:
|
||||
// - "create" json="dir"|"file":CREATE 响应(FileId+Information(FILE_OPENED))
|
||||
// - "list" json=[{name,dir,size,mtime,created,accessed}...]:按信息类编码目录条目
|
||||
// - "stat" json={size,mtime,created,accessed,dir}:QUERY_INFORMATION 响应
|
||||
// - "volume" json 忽略:QUERY_VOLUME_INFORMATION 响应(卷标取 Handler 配置)
|
||||
// - "status" json 忽略:纯状态响应(CLOSE 等无载荷完成)——CLOSE 必须
|
||||
// 以 STATUS_SUCCESS 完成,返回错误状态(如 NOT_IMPLEMENTED)会让服务端
|
||||
// 判定设备异常、撤销 \\tsclient\<名> 映射(表现为"试图访问无效的地址"
|
||||
// 且后续零 IRP,2026-09-12 10.0.0.3 服务端重启后探测序列新增 CLOSE 步骤
|
||||
// 时暴露)。
|
||||
//
|
||||
// status != STATUS_SUCCESS 时一律回纯状态响应。
|
||||
func (h *Handler) CompleteJSON(completionID uint32, status uint32, kind, json string) {
|
||||
p := h.takePending(completionID)
|
||||
if p == nil {
|
||||
// 未知完成号(重复回调/通道重置):回空 pending 的纯状态响应兜底
|
||||
h.completeStatus(completionID, &pending{}, status, nil)
|
||||
return
|
||||
}
|
||||
if status != STATUS_SUCCESS {
|
||||
h.completeStatus(completionID, p, status, nil)
|
||||
return
|
||||
}
|
||||
switch kind {
|
||||
case "status":
|
||||
h.completeStatus(completionID, p, STATUS_SUCCESS, nil)
|
||||
case "create":
|
||||
h.enumMu.Lock()
|
||||
h.enumPos[p.fileID] = 0
|
||||
h.enumMu.Unlock()
|
||||
extra := make([]byte, 5)
|
||||
binary.LittleEndian.PutUint32(extra, p.fileID)
|
||||
extra[4] = 1 // Information = FILE_OPENED
|
||||
h.completeStatus(completionID, p, STATUS_SUCCESS, extra)
|
||||
case "list":
|
||||
entries, err := decodeEntries(json)
|
||||
if err != nil {
|
||||
h.completeStatus(completionID, p, STATUS_INVALID_PARAMETER, nil)
|
||||
return
|
||||
}
|
||||
h.enumMu.Lock()
|
||||
pos := h.enumPos[p.fileID]
|
||||
if pos > len(entries) || pos < 0 {
|
||||
pos = 0 // 目录内容变化的兜底
|
||||
}
|
||||
if pos >= len(entries) {
|
||||
h.enumPos[p.fileID] = 0
|
||||
h.enumMu.Unlock()
|
||||
h.completeStatus(completionID, p, STATUS_NO_MORE_FILES, nil)
|
||||
return
|
||||
}
|
||||
batch := entries[pos:]
|
||||
h.enumPos[p.fileID] = len(entries)
|
||||
h.enumMu.Unlock()
|
||||
data := encodeDirEntries(p.info, batch)
|
||||
if data == nil {
|
||||
h.completeStatus(completionID, p, STATUS_NOT_IMPLEMENTED, nil)
|
||||
return
|
||||
}
|
||||
extra := make([]byte, 4+len(data))
|
||||
binary.LittleEndian.PutUint32(extra, uint32(len(data)))
|
||||
copy(extra[4:], data)
|
||||
h.completeStatus(completionID, p, STATUS_SUCCESS, extra)
|
||||
case "stat":
|
||||
e, err := decodeEntry(json)
|
||||
if err != nil {
|
||||
h.completeStatus(completionID, p, STATUS_INVALID_PARAMETER, nil)
|
||||
return
|
||||
}
|
||||
data := encodeFileInfo(p.info, e)
|
||||
if data == nil {
|
||||
h.completeStatus(completionID, p, STATUS_NOT_IMPLEMENTED, nil)
|
||||
return
|
||||
}
|
||||
extra := make([]byte, 4+len(data))
|
||||
binary.LittleEndian.PutUint32(extra, uint32(len(data)))
|
||||
copy(extra[4:], data)
|
||||
h.completeStatus(completionID, p, STATUS_SUCCESS, extra)
|
||||
case "volume":
|
||||
data := encodeVolumeInfo(p.info, h.label)
|
||||
if data == nil {
|
||||
h.completeStatus(completionID, p, STATUS_NOT_IMPLEMENTED, nil)
|
||||
return
|
||||
}
|
||||
extra := make([]byte, 4+len(data))
|
||||
binary.LittleEndian.PutUint32(extra, uint32(len(data)))
|
||||
copy(extra[4:], data)
|
||||
h.completeStatus(completionID, p, STATUS_SUCCESS, extra)
|
||||
default:
|
||||
h.completeStatus(completionID, p, STATUS_NOT_IMPLEMENTED, nil)
|
||||
}
|
||||
}
|
||||
|
||||
// allocFileID 为新 CREATE 分配 FileId(服务端请求中 FileId=0,响应才带号)。
|
||||
func (h *Handler) allocFileID() uint32 {
|
||||
id := h.nextFileID
|
||||
h.nextFileID++
|
||||
return id
|
||||
}
|
||||
@@ -0,0 +1,428 @@
|
||||
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))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user