init: 自 zomaintain/backend/rdplib 平移独立成库; module path 从上游 github.com/nakagami/grdp 改为 git.zeroonesoft.cn/golib/rdplib
This commit is contained in:
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user