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