Files
rdplib/plugin/rdpdr/rdpdr.go
T

696 lines
26 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Package rdpdr 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
}