init: 自 zomaintain/backend/rdplib 平移独立成库; module path 从上游 github.com/nakagami/grdp 改为 git.zeroonesoft.cn/golib/rdplib

This commit is contained in:
w11
2026-09-19 18:55:43 +08:00
commit 1096fbce9d
94 changed files with 36790 additions and 0 deletions
+256
View File
@@ -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
}
}
+695
View File
@@ -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
}
+428
View File
@@ -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))
}
}