feat: 完善堡垒机与 Agent 智能运维能力

- 支持 Windows Agent 构建、安装脚本和统一上报协议

- 增强 RDP 剪贴板、虚拟盘和文件传输能力

- 新增 AIOps 上下文工具、告警 RCA trace 落库和回放接口

- 重构 docs 模块文档并补充终端、堡垒机、监控、AI Agent 设计
This commit is contained in:
Logan.Li committed 2026-07-05 23:49:36 +08:00
1 parent a9513c2056
commit e0d652d211
169 files changed
+12192 -9116

No files matched your search

+422
View File
@@ -0,0 +1,422 @@
package cliprdr
import (
"bytes"
"encoding/binary"
"fmt"
"log"
"strings"
"sync"
"unicode/utf16"
)
// Sender 负责把 CLIPRDR PDU 通过 MCS SendDataRequest 发往指定 channel ID。
// 由协议栈注入(通常封装 buildMCSSendDataRequest + transport.WriteX224Data + writeMu)。// Logan.Li
type Sender interface {
SendChannelPDU(channelID uint16, data []byte) error
}
// Handler 实现 RDP 剪贴板虚拟通道(CF_UNICODETEXT / CF_TEXT 文本格式)。
//
// - onRemote: 远端剪贴板文本到达时回调(协议栈应转成 WS type=clipboard 帧推给浏览器)
// - getLocal: 远端请求本地剪贴板时回调取值(协议栈从 inputLoop 维护的本地剪贴板缓存读取)
//
// 两个回调都是线程安全的调用点:Handler 内部用 mu 保护自身状态,
// 调用方负责 Sender 的并发写(writeMu)。
type Handler struct {
sender Sender
channelID uint16
mu sync.Mutex
useLongFormatNames bool
serverCapsReceived bool
monitorReady bool
localText string // 本地剪贴板文本(由 SetLocalText 更新,供远端请求时回填)
suppressNextChange bool // 抑制 echo 回环:远端→本地 触发的本地变更不再回推远端
onRemote func(text string) // 远端文本到达回调(可能为 nil)
initialFormatListSent bool // 初始化阶段只允许发送一次空 FormatList,匹配 FreeRDP/IronRDP 时序。// Logan.Li
}
// NewHandler 创建剪贴板处理器。
// - onRemote: 远端剪贴板内容到达时调用(转成 WS clipboard 帧推送浏览器)
// - getLocal: 已废弃,本地文本请用 SetLocalText 主动设置(浏览器同步剪贴板时写入)
func NewHandler(onRemote func(text string)) *Handler {
return &Handler{onRemote: onRemote}
}
// Attach 绑定发送通道与会话分配的 channel ID(在 MCS ChannelJoin 成功后调用)。
// 附加后主动发 CB_CLIP_CAPS 声明客户端能力(必须用 Long Format Names),
// 否则 Windows 不知道格式协商方式,会忽略后续 FormatList。 // Logan.Li
func (h *Handler) Attach(sender Sender, channelID uint16) {
h.mu.Lock()
h.sender = sender
h.channelID = channelID
h.useLongFormatNames = true // 客户端强制 Long Format Names,与发送的 caps 一致
h.mu.Unlock()
log.Printf("[cliprdr] attached channelID=%d", channelID)
}
// Init 在会话激活后主动发起剪贴板握手。
// MS-RDPECLIP 规定服务端先发 CB_CLIP_CAPS + CB_MONITOR_READY,但部分 Windows 版本
// (尤其开启了剪贴板重定向策略的)会等客户端先声明能力。主动发 caps + formatList
// 可兼容两种情况,不会产生冲突(服务端收到多余的 caps 会忽略)。// Logan.Li
func (h *Handler) Init() {
h.mu.Lock()
if h.sender == nil {
h.mu.Unlock()
return
}
monitorReady := h.monitorReady
needInitial := !h.initialFormatListSent
if needInitial {
h.initialFormatListSent = true
}
h.mu.Unlock()
if monitorReady {
log.Printf("[cliprdr] Debug: %s", fmt.Sprintln("cliprdr init: server MonitorReady already handled"))
return
}
// 部分 Windows 会先等客户端声明能力;延迟主动初始化兼顾标准时序和真实服务器兼容性。// Logan.Li
h.sendClipCaps()
if needInitial {
h.sendEmptyFormatList()
}
log.Printf("[cliprdr] Debug: %s", fmt.Sprintln("cliprdr init: proactive caps and initial FormatList sent"))
}
// SetLocalText 更新本地剪贴板文本(浏览器把本地剪贴板同步过来时调用)。
// 设置后会向远端发送 FormatList 通知远端"本地剪贴板有新内容",
// 除非 suppress 为 true(用于抑制远端→本地 echo 回环)。
func (h *Handler) SetLocalText(text string) {
h.mu.Lock()
h.localText = text
suppress := h.suppressNextChange
h.suppressNextChange = false
sender := h.sender
h.mu.Unlock()
if suppress {
log.Printf("[cliprdr] Debug: %s", fmt.Sprintln("cliprdr: local text set with suppress (echo from remote), skip FormatList"))
return
}
if sender != nil {
h.sendFormatList()
log.Printf("[cliprdr] Debug: %s", fmt.Sprintln("cliprdr: local text changed, sent FormatList", "len", len(text)))
}
}
// ChannelID 返回绑定的通道 ID(未 Attach 返回 0)。
func (h *Handler) ChannelID() uint16 {
h.mu.Lock()
defer h.mu.Unlock()
return h.channelID
}
// RequestRemoteText 在用户触发远端 Ctrl+C/Ctrl+X 后主动请求文本内容,
// 兼容部分 Windows 不主动发送 CB_FORMAT_LIST 的场景。// Logan.Li
func (h *Handler) RequestRemoteText() {
h.mu.Lock()
ready := h.sender != nil
h.mu.Unlock()
if !ready {
return
}
h.sendFormatDataRequest(cfUnicodeText)
}
// Process 处理从 MCS SendDataIndication 收到的 cliprdr PDU(已去掉 MCS/SEC 头的原始通道数据)。
// 由协议栈 readLoop 在识别到 cliprdr channel ID 后调用。
func (h *Handler) Process(data []byte) {
if len(data) < 8 {
return
}
msgType := binary.LittleEndian.Uint16(data[0:2])
msgFlags := binary.LittleEndian.Uint16(data[2:4])
dataLen := binary.LittleEndian.Uint32(data[4:8])
if int(dataLen) > len(data)-8 {
log.Printf("[cliprdr] Warn: %s", fmt.Sprintln("cliprdr: PDU dataLen exceeds payload", "dataLen", dataLen, "have", len(data)-8))
return
}
body := data[8 : 8+dataLen]
log.Printf("[cliprdr] Debug: %s", fmt.Sprintln("cliprdr recv", "msgType", msgType, "msgFlags", msgFlags, "dataLen", dataLen))
switch msgType {
case msgClipCaps:
h.processClipCaps(body)
case msgMonitorReady:
h.processMonitorReady()
case msgFormatList:
h.processFormatList(body)
case msgFormatListResponse:
// 远端确认收到本地的 FormatList,无需动作
if msgFlags&flagResponseOK == 0 {
log.Printf("[cliprdr] Warn: %s", fmt.Sprintln("cliprdr: FormatListResponse FAIL"))
}
case msgFormatDataRequest:
h.processFormatDataRequest(body)
case msgFormatDataResponse:
h.processFormatDataResponse(body, msgFlags)
case msgLockClipdata, msgUnlockClipdata:
// 文本模式不处理锁
default:
log.Printf("[cliprdr] Debug: %s", fmt.Sprintln("cliprdr: unhandled msgType", "msgType", msgType))
}
}
// --- CB_CLIP_CAPS(2.2.2.1)-------------------------------------------------
func (h *Handler) processClipCaps(body []byte) {
if len(body) < 4 {
return
}
cCapSets := binary.LittleEndian.Uint16(body[0:2])
offset := 4 // cCapabilitySets(2) + pad1(2)
for i := 0; i < int(cCapSets); i++ {
if offset+4 > len(body) {
break
}
capType := binary.LittleEndian.Uint16(body[offset:])
capLen := binary.LittleEndian.Uint16(body[offset+2:])
if capType == capsTypeGeneral && capLen >= 12 && offset+12 <= len(body) {
generalFlags := binary.LittleEndian.Uint32(body[offset+8:])
h.mu.Lock()
h.useLongFormatNames = generalFlags&flagUseLongFormatNames != 0
useLong := h.useLongFormatNames
h.mu.Unlock()
log.Printf("[cliprdr] Debug: %s", fmt.Sprintln("cliprdr: server caps", "generalFlags", generalFlags, "longNames", useLong))
}
offset += int(capLen)
}
h.mu.Lock()
h.serverCapsReceived = true
localReady := h.localText != ""
ready := h.monitorReady
h.mu.Unlock()
// 非标准顺序:MonitorReady 先到时,在拿到 caps 后补发 FormatList
if ready && localReady {
h.sendFormatList()
}
}
// --- CB_MONITOR_READY(2.2.2.2)--------------------------------------------
func (h *Handler) processMonitorReady() {
log.Printf("[cliprdr] Debug: %s", fmt.Sprintln("cliprdr: server Monitor Ready"))
h.mu.Lock()
h.monitorReady = true
capsReady := h.serverCapsReceived
localReady := h.localText != ""
needInitial := !h.initialFormatListSent
if needInitial {
h.initialFormatListSent = true
}
h.mu.Unlock()
// MS-RDPECLIP §1.3.2.1: server 先发 CB_CLIP_CAPS 再发 CB_MONITOR_READY。
// 拿到 caps 才知道是否用 Long Format Names,故 FormatList 等 caps 就绪后再发。
h.sendClipCaps()
if needInitial && !localReady {
h.sendEmptyFormatList()
log.Printf("[cliprdr] Debug: %s", fmt.Sprintln("cliprdr: sent initial empty FormatList"))
}
if capsReady && localReady {
h.sendFormatList()
}
}
// --- Format List(2.2.3)---------------------------------------------------
func (h *Handler) sendFormatList() {
h.mu.Lock()
useLong := h.useLongFormatNames
h.initialFormatListSent = true
h.mu.Unlock()
body := &bytes.Buffer{}
if useLong {
// Long Format Name: formatId(4) + null-terminated UTF-16LE name
binary.Write(body, binary.LittleEndian, uint32(cfUnicodeText))
binary.Write(body, binary.LittleEndian, uint16(0)) // 空名 = 标准格式
binary.Write(body, binary.LittleEndian, uint32(cfText))
binary.Write(body, binary.LittleEndian, uint16(0))
} else {
// Short Format Name: formatId(4) + name[32]
binary.Write(body, binary.LittleEndian, uint32(cfUnicodeText))
body.Write(make([]byte, 32))
binary.Write(body, binary.LittleEndian, uint32(cfText))
body.Write(make([]byte, 32))
}
h.sendPDU(msgFormatList, 0, body.Bytes())
}
func (h *Handler) sendEmptyFormatList() {
// 初始化空 FormatList 是 FreeRDP 允许的首包,用来完成剪贴板客户端初始化。// Logan.Li
h.sendPDU(msgFormatList, 0, nil)
}
func (h *Handler) processFormatList(body []byte) {
formats := h.parseFormatList(body)
log.Printf("[cliprdr] Debug: %s", fmt.Sprintln("cliprdr: server FormatList", "formats", formats))
// 始终回复 OK
h.sendPDU(msgFormatListResponse, flagResponseOK, nil)
// 优先请求 Unicode 文本,其次 ANSI
for _, f := range formats {
if f == cfUnicodeText {
h.sendFormatDataRequest(cfUnicodeText)
return
}
}
for _, f := range formats {
if f == cfText {
h.sendFormatDataRequest(cfText)
return
}
}
}
func (h *Handler) parseFormatList(body []byte) []uint32 {
h.mu.Lock()
useLong := h.useLongFormatNames
h.mu.Unlock()
var formats []uint32
if useLong {
// Long: formatId(4) + null-term UTF-16LE name
offset := 0
for offset+4 <= len(body) {
formats = append(formats, binary.LittleEndian.Uint32(body[offset:]))
offset += 4
// 跳过 name(null-terminated UTF-16LE)
for offset+1 < len(body) {
if body[offset] == 0 && body[offset+1] == 0 {
offset += 2
break
}
offset += 2
}
}
} else {
// Short: formatId(4) + name[32] = 36 字节/项
for offset := 0; offset+36 <= len(body); offset += 36 {
formats = append(formats, binary.LittleEndian.Uint32(body[offset:]))
}
}
return formats
}
// --- Format Data Request / Response(2.2.5)-------------------------------
func (h *Handler) sendFormatDataRequest(formatID uint32) {
b := make([]byte, 4)
binary.LittleEndian.PutUint32(b, formatID)
h.sendPDU(msgFormatDataRequest, 0, b)
log.Printf("[cliprdr] Debug: %s", fmt.Sprintln("cliprdr: sent FormatDataRequest", "formatID", formatID))
}
func (h *Handler) processFormatDataRequest(body []byte) {
if len(body) < 4 {
h.sendPDU(msgFormatDataResponse, flagResponseFail, nil)
return
}
requestedFormat := binary.LittleEndian.Uint32(body[0:4])
h.mu.Lock()
text := h.localText
h.mu.Unlock()
log.Printf("[cliprdr] Debug: %s", fmt.Sprintln("cliprdr: server requests format", "formatID", requestedFormat, "localLen", len(text)))
switch requestedFormat {
case cfUnicodeText:
encoded := encodeUTF16LE(text + "\x00")
h.sendPDU(msgFormatDataResponse, flagResponseOK, encoded)
case cfText:
h.sendPDU(msgFormatDataResponse, flagResponseOK, []byte(text+"\x00"))
default:
h.sendPDU(msgFormatDataResponse, flagResponseFail, nil)
}
}
func (h *Handler) processFormatDataResponse(body []byte, msgFlags uint16) {
if msgFlags&flagResponseOK == 0 {
log.Printf("[cliprdr] Warn: %s", fmt.Sprintln("cliprdr: FormatDataResponse FAIL"))
return
}
// 尝试按 UTF-16LE 解码(CF_UNICODETEXT)
text := decodeUTF16LE(body)
text = strings.TrimRight(text, "\x00")
if text == "" {
return
}
log.Printf("[cliprdr] Debug: %s", fmt.Sprintln("cliprdr: remote clipboard text", "len", len(text)))
// 标记抑制下一次本地变更(避免 echo 回环:远端→本地触发的本地变更不再推回远端)
h.mu.Lock()
h.suppressNextChange = true
cb := h.onRemote
h.mu.Unlock()
if cb != nil {
cb(text)
}
}
// --- Send helpers --------------------------------------------------------
func (h *Handler) sendClipCaps() {
// General capability set: type(2) + length(2) + version(4) + flags(4) = 12
cap := &bytes.Buffer{}
binary.Write(cap, binary.LittleEndian, uint16(capsTypeGeneral))
binary.Write(cap, binary.LittleEndian, uint16(12))
binary.Write(cap, binary.LittleEndian, uint32(capsVersion2))
binary.Write(cap, binary.LittleEndian, uint32(flagUseLongFormatNames))
body := &bytes.Buffer{}
binary.Write(body, binary.LittleEndian, uint16(1)) // cCapabilitySets
binary.Write(body, binary.LittleEndian, uint16(0)) // pad1
body.Write(cap.Bytes())
h.sendPDU(msgClipCaps, 0, body.Bytes())
}
func (h *Handler) sendPDU(msgType, msgFlags uint16, body []byte) {
h.mu.Lock()
sender := h.sender
chID := h.channelID
h.mu.Unlock()
if sender == nil {
return
}
buf := &bytes.Buffer{}
binary.Write(buf, binary.LittleEndian, msgType)
binary.Write(buf, binary.LittleEndian, msgFlags)
binary.Write(buf, binary.LittleEndian, uint32(len(body)))
buf.Write(body)
if err := sender.SendChannelPDU(chID, buf.Bytes()); err != nil {
log.Printf("[cliprdr] Warn: %s", fmt.Sprintln("cliprdr: send PDU failed", "msgType", msgType, "err", err))
}
}
// --- UTF-16LE helpers ----------------------------------------------------
func decodeUTF16LE(b []byte) string {
if len(b) < 2 {
return ""
}
if len(b)%2 != 0 {
b = b[:len(b)-1]
}
u16 := make([]uint16, len(b)/2)
for i := range u16 {
u16[i] = binary.LittleEndian.Uint16(b[i*2:])
}
return string(utf16.Decode(u16))
}
func encodeUTF16LE(s string) []byte {
u16 := utf16.Encode([]rune(s))
b := make([]byte, len(u16)*2)
for i, v := range u16 {
binary.LittleEndian.PutUint16(b[i*2:], v)
}
return b
}
+65
View File
@@ -0,0 +1,65 @@
// Package cliprdr 实现 RDP 剪贴板虚拟通道扩展(MS-RDPECLIP)的客户端侧,
// 支持浏览器与远端 Windows 之间的双向文本剪贴板同步。// Logan.Li
//
// 协议序列(MS-RDPECLIP §1.3.2):
//
// 初始化:
// Server ── CB_CLIP_CAPS ──► Client
// Server ── CB_MONITOR_READY ──► Client
// Client ── CB_CLIP_CAPS ──► Server
// Client ── CB_FORMAT_LIST ──► Server
// Server ── CB_FORMAT_LIST_RESPONSE ──► Client
//
// 远端→本地(远端复制,本地粘贴):
// Server ── CB_FORMAT_LIST ──► Client (远端声明可用格式)
// Client ── CB_FORMAT_LIST_RESPONSE ──► Server
// Client ── CB_FORMAT_DATA_REQUEST ──► Server (本地请求文本数据)
// Server ── CB_FORMAT_DATA_RESPONSE ──► Client (远端返回剪贴板内容)
//
// 本地→远端(本地复制,远端粘贴):
// Client ── CB_FORMAT_LIST ──► Server (本地声明可用格式)
// Server ── CB_FORMAT_DATA_REQUEST ──► Client (远端请求文本数据)
// Client ── CB_FORMAT_DATA_RESPONSE ──► Server (本地返回剪贴板内容)
package cliprdr
// CLIPRDR PDU msgType(MS-RDPECLIP 2.2.1)
const (
msgMonitorReady = 0x0001
msgFormatList = 0x0002
msgFormatListResponse = 0x0003
msgFormatDataRequest = 0x0004
msgFormatDataResponse = 0x0005
msgTempDirectory = 0x0006
msgClipCaps = 0x0007
msgFilecontentsRequest = 0x0008
msgFilecontentsResponse = 0x0009
msgLockClipdata = 0x000A
msgUnlockClipdata = 0x000B
)
// msgFlags(MS-RDPECLIP 2.2.2)
const (
flagResponseOK = 0x0001
flagResponseFail = 0x0002
flagAsciiNames = 0x0004
)
// 通用能力集 GeneralFlags(MS-RDPECLIP 2.2.2.1.1.1.1)
const (
capsTypeGeneral = 0x0001
capsVersion2 = 0x00000002
flagUseLongFormatNames = 0x00000002
)
// Windows 标准剪贴板格式 ID(WinUser.h)
const (
cfText = 1 // CF_TEXT ANSI 文本
cfUnicodeText = 13 // CF_UNICODETEXT UTF-16 文本
)
// 通道声明常量(供协议栈在 MCS Connect-Initial 里声明通道时复用,避免魔法值分散)
const (
ChannelName = "cliprdr"
// ChannelOptions 与 grdp/FreeRDP 保持一致:INITIALIZED | ENCRYPT_RDP | SHOW_PROTOCOL
ChannelOptions = 0x80000000 | 0x40000000 | 0x00200000
)
+602
View File
@@ -0,0 +1,602 @@
package rdpdr
import (
"encoding/binary"
"log"
"path"
"strings"
"time"
"unicode/utf16"
)
const (
irpCreate uint32 = 0x00000000
irpClose uint32 = 0x00000002
irpRead uint32 = 0x00000003
irpWrite uint32 = 0x00000004
irpQueryInfo uint32 = 0x00000005
irpSetInfo uint32 = 0x00000006
irpDeviceCtrl uint32 = 0x0000000e
irpQueryVolume uint32 = 0x0000000a
irpDirControl uint32 = 0x0000000c
irpLockControl uint32 = 0x00000011
irpMnQueryDir uint32 = 0x00000001
irpMnNotifyChangeDir uint32 = 0x00000002
statusSuccess uint32 = 0x00000000
statusNoMoreFiles uint32 = 0x80000006
statusAccessDenied uint32 = 0xc0000022
statusObjectNameNotFound uint32 = 0xc0000034
statusNoSuchFile uint32 = 0xc000000f
statusFileIsDirectory uint32 = 0xc00000ba
statusNotSupported uint32 = 0xc00000bb
statusInvalidDeviceReq uint32 = 0xc0000010
statusInvalidParameter uint32 = 0xc000000d
statusEndOfFile uint32 = 0xc0000011
fileOpen uint32 = 0x00000001
fileCreate uint32 = 0x00000002
fileOpenIf uint32 = 0x00000003
fileOverwrite uint32 = 0x00000004
fileOverwriteIf uint32 = 0x00000005
fileDirectoryFile uint32 = 0x00000001
fileNonDirectoryFile uint32 = 0x00000040
fileDirectoryInformation uint32 = 1
fileFullDirectoryInformation uint32 = 2
fileBothDirectoryInformation uint32 = 3
fileBasicInformation uint32 = 4
fileStandardInformation uint32 = 5
fileInternalInformation uint32 = 6
fileEaInformation uint32 = 7
fileAccessInformation uint32 = 8
fileNameInformation uint32 = 9
fileNamesInformation uint32 = 12
filePositionInformation uint32 = 14
fileModeInformation uint32 = 16
fileAlignmentInformation uint32 = 17
fileEndOfFileInformation uint32 = 20
fileNetworkOpenInformation uint32 = 34
fileAttributeTagInformation uint32 = 35
fileFsVolumeInformation uint32 = 1
fileFsSizeInformation uint32 = 3
fileFsDeviceInformation uint32 = 4
fileFsAttributeInformation uint32 = 5
fileFsFullSizeInformation uint32 = 7
fileAttrDirectory uint32 = 0x00000010
fileAttrNormal uint32 = 0x00000080
fileAttrArchive uint32 = 0x00000020
fileOpened byte = 0x01
fileCreated byte = 0x02
fileOverwritten byte = 0x03
downloadDirName = "Download"
uploadDirName = "Upload"
)
const windowsFiletimeOffset = 116444736000000000
type ioRequest struct {
deviceID uint32
fileID uint32
completionID uint32
majorFn uint32
minorFn uint32
payload []byte
}
type openFile struct {
path string
name string
isDir bool
data []byte
written bool
dirEntries []dirEntry
dirIndex int
}
type dirEntry struct {
name string
isDir bool
size uint64
}
func (h *Handler) handleIORequest(body []byte) {
if len(body) < 20 {
return
}
req := ioRequest{
deviceID: binary.LittleEndian.Uint32(body[0:4]),
fileID: binary.LittleEndian.Uint32(body[4:8]),
completionID: binary.LittleEndian.Uint32(body[8:12]),
majorFn: binary.LittleEndian.Uint32(body[12:16]),
minorFn: binary.LittleEndian.Uint32(body[16:20]),
payload: body[20:],
}
log.Printf("[rdpdr] io request device=%d file=%d completion=%d major=0x%x minor=0x%x payload=%d", req.deviceID, req.fileID, req.completionID, req.majorFn, req.minorFn, len(req.payload))
switch req.majorFn {
case irpCreate:
h.handleCreate(req)
case irpClose:
h.handleClose(req)
case irpRead:
h.handleRead(req)
case irpWrite:
h.handleWrite(req)
case irpQueryInfo:
h.handleQueryInfo(req)
case irpSetInfo:
h.handleSetInfo(req)
case irpDeviceCtrl:
h.handleDeviceControl(req)
case irpQueryVolume:
h.handleQueryVolume(req)
case irpDirControl:
if req.minorFn == irpMnQueryDir {
h.handleQueryDirectory(req)
return
}
if req.minorFn == irpMnNotifyChangeDir {
h.handleNotifyChangeDirectory(req)
return
}
h.sendIOCompletion(req.deviceID, req.completionID, statusNotSupported, nil)
case irpLockControl:
h.handleLockControl(req)
default:
h.sendIOCompletion(req.deviceID, req.completionID, statusNotSupported, nil)
}
}
func (h *Handler) handleCreate(req ioRequest) {
var out [5]byte
if len(req.payload) < 32 {
h.sendIOCompletion(req.deviceID, req.completionID, statusInvalidParameter, out[:])
return
}
disposition := binary.LittleEndian.Uint32(req.payload[20:24])
options := binary.LittleEndian.Uint32(req.payload[24:28])
pathLen := binary.LittleEndian.Uint32(req.payload[28:32])
if uint32(len(req.payload)) < 32+pathLen {
h.sendIOCompletion(req.deviceID, req.completionID, statusInvalidParameter, out[:])
return
}
vpath := normalizeVirtualPath(decodeUTF16(req.payload[32 : 32+pathLen]))
isDir := options&fileDirectoryFile != 0
kind, name := classifyVirtualPath(vpath)
if kind == "missing" {
h.sendIOCompletion(req.deviceID, req.completionID, statusObjectNameNotFound, out[:])
return
}
switch kind {
case "root", "download", "upload":
isDir = true
case "file", "upload_file":
// Explorer 拖拽复制时可能带上目录相关 CreateOptions,虚拟盘文件必须按真实文件打开。// Logan.Li
isDir = false
}
var initialData []byte
if kind == "upload_file" {
data, ok := h.getUpload(name)
if !ok {
h.sendIOCompletion(req.deviceID, req.completionID, statusObjectNameNotFound, out[:])
return
}
initialData = data
}
if isDir && options&fileNonDirectoryFile != 0 {
h.sendIOCompletion(req.deviceID, req.completionID, statusFileIsDirectory, out[:])
return
}
if kind == "file" && disposition == fileOpen {
// 虚拟 Download 目录只负责接收远端写入,读取历史文件留给上传目录实现。// Logan.Li
h.sendIOCompletion(req.deviceID, req.completionID, statusObjectNameNotFound, out[:])
return
}
info := fileOpened
if disposition == fileCreate {
info = fileCreated
}
if disposition == fileOverwrite || disposition == fileOverwriteIf {
info = fileOverwritten
}
fileID := h.storeOpenFile(&openFile{path: vpath, name: name, isDir: isDir, data: initialData})
binary.LittleEndian.PutUint32(out[0:4], fileID)
out[4] = info
log.Printf("[rdpdr] create path=%q fileID=%d dir=%t", vpath, fileID, isDir)
h.sendIOCompletion(req.deviceID, req.completionID, statusSuccess, out[:])
}
func (h *Handler) handleClose(req ioRequest) {
file, ok := h.removeOpenFile(req.fileID)
if ok && !file.isDir && file.written && len(file.data) > 0 && h.onDownload != nil {
data := append([]byte(nil), file.data...)
h.onDownload(Download{Name: file.name, Data: data})
log.Printf("[rdpdr] download captured name=%s size=%d", file.name, len(data))
}
var out [5]byte
h.sendIOCompletion(req.deviceID, req.completionID, statusSuccess, out[:])
}
func (h *Handler) handleRead(req ioRequest) {
if len(req.payload) < 20 {
h.sendIOCompletion(req.deviceID, req.completionID, statusInvalidParameter, nil)
return
}
length := int(binary.LittleEndian.Uint32(req.payload[0:4]))
offset := int(binary.LittleEndian.Uint64(req.payload[4:12]))
file, ok := h.getOpenFile(req.fileID)
if !ok || file.isDir {
log.Printf("[rdpdr] read missing file=%d offset=%d length=%d", req.fileID, offset, length)
h.sendIOCompletion(req.deviceID, req.completionID, statusNoSuchFile, nil)
return
}
if length <= 0 {
var out [4]byte
log.Printf("[rdpdr] read zero file=%d path=%q offset=%d", req.fileID, file.path, offset)
h.sendIOCompletion(req.deviceID, req.completionID, statusSuccess, out[:])
return
}
if offset >= len(file.data) {
var out [4]byte
// Explorer 复制虚拟盘文件时更稳定地接受 0 字节成功读作为 EOF。// Logan.Li
log.Printf("[rdpdr] read eof file=%d path=%q offset=%d size=%d", req.fileID, file.path, offset, len(file.data))
h.sendIOCompletion(req.deviceID, req.completionID, statusSuccess, out[:])
return
}
end := offset + length
if end > len(file.data) {
end = len(file.data)
}
out := make([]byte, 4+end-offset)
binary.LittleEndian.PutUint32(out[0:4], uint32(end-offset))
copy(out[4:], file.data[offset:end])
log.Printf("[rdpdr] read file=%d path=%q offset=%d requested=%d returned=%d size=%d", req.fileID, file.path, offset, length, end-offset, len(file.data))
h.sendIOCompletion(req.deviceID, req.completionID, statusSuccess, out)
}
func (h *Handler) handleWrite(req ioRequest) {
if len(req.payload) < 32 {
h.sendIOCompletion(req.deviceID, req.completionID, statusInvalidParameter, nil)
return
}
length := int(binary.LittleEndian.Uint32(req.payload[0:4]))
offset := int(binary.LittleEndian.Uint64(req.payload[4:12]))
chunk := req.payload[32:]
if length > len(chunk) {
length = len(chunk)
}
file, ok := h.getOpenFile(req.fileID)
if !ok || file.isDir {
h.sendIOCompletion(req.deviceID, req.completionID, statusNoSuchFile, nil)
return
}
need := offset + length
if need > len(file.data) {
next := make([]byte, need)
copy(next, file.data)
file.data = next
}
copy(file.data[offset:need], chunk[:length])
file.written = true
var out [5]byte
binary.LittleEndian.PutUint32(out[0:4], uint32(length))
h.sendIOCompletion(req.deviceID, req.completionID, statusSuccess, out[:])
}
func (h *Handler) handleQueryInfo(req ioRequest) {
if len(req.payload) < 4 {
h.sendIOCompletion(req.deviceID, req.completionID, statusInvalidParameter, nil)
return
}
infoClass := binary.LittleEndian.Uint32(req.payload[0:4])
file, ok := h.getOpenFile(req.fileID)
if !ok {
h.sendIOCompletion(req.deviceID, req.completionID, statusNoSuchFile, nil)
return
}
attrs := fileAttrArchive
if file.isDir {
attrs = fileAttrDirectory
}
size := uint64(len(file.data))
log.Printf("[rdpdr] query info file=%d class=%d path=%q dir=%t size=%d", req.fileID, infoClass, file.path, file.isDir, size)
switch infoClass {
case fileBasicInformation:
out := make([]byte, 40)
binary.LittleEndian.PutUint32(out[0:4], 36)
writeFileTimes(out[4:36])
binary.LittleEndian.PutUint32(out[36:40], attrs)
h.sendIOCompletion(req.deviceID, req.completionID, statusSuccess, out)
case fileStandardInformation:
out := make([]byte, 26)
binary.LittleEndian.PutUint32(out[0:4], 22)
binary.LittleEndian.PutUint64(out[4:12], align4096(size))
binary.LittleEndian.PutUint64(out[12:20], size)
binary.LittleEndian.PutUint32(out[20:24], 1)
if file.isDir {
out[25] = 1
}
h.sendIOCompletion(req.deviceID, req.completionID, statusSuccess, out)
case fileAttributeTagInformation:
out := make([]byte, 12)
binary.LittleEndian.PutUint32(out[0:4], 8)
binary.LittleEndian.PutUint32(out[4:8], attrs)
h.sendIOCompletion(req.deviceID, req.completionID, statusSuccess, out)
case fileInternalInformation:
out := make([]byte, 12)
binary.LittleEndian.PutUint32(out[0:4], 8)
binary.LittleEndian.PutUint64(out[4:12], uint64(req.fileID))
h.sendIOCompletion(req.deviceID, req.completionID, statusSuccess, out)
case fileEaInformation:
out := make([]byte, 8)
binary.LittleEndian.PutUint32(out[0:4], 4)
h.sendIOCompletion(req.deviceID, req.completionID, statusSuccess, out)
case fileAccessInformation, fileModeInformation, fileAlignmentInformation:
out := make([]byte, 8)
binary.LittleEndian.PutUint32(out[0:4], 4)
h.sendIOCompletion(req.deviceID, req.completionID, statusSuccess, out)
case fileNameInformation:
name := encodeUTF16(fileDisplayName(file))
out := make([]byte, 8+len(name))
binary.LittleEndian.PutUint32(out[0:4], uint32(4+len(name)))
binary.LittleEndian.PutUint32(out[4:8], uint32(len(name)))
copy(out[8:], name)
h.sendIOCompletion(req.deviceID, req.completionID, statusSuccess, out)
case filePositionInformation:
out := make([]byte, 12)
binary.LittleEndian.PutUint32(out[0:4], 8)
h.sendIOCompletion(req.deviceID, req.completionID, statusSuccess, out)
case fileNetworkOpenInformation:
out := make([]byte, 60)
binary.LittleEndian.PutUint32(out[0:4], 56)
writeFileTimes(out[4:36])
binary.LittleEndian.PutUint64(out[36:44], align4096(size))
binary.LittleEndian.PutUint64(out[44:52], size)
binary.LittleEndian.PutUint32(out[52:56], attrs)
h.sendIOCompletion(req.deviceID, req.completionID, statusSuccess, out)
default:
log.Printf("[rdpdr] query info unsupported class=%d file=%d path=%q", infoClass, req.fileID, file.path)
h.sendIOCompletion(req.deviceID, req.completionID, statusNotSupported, nil)
}
}
func (h *Handler) handleSetInfo(req ioRequest) {
var out [4]byte
h.sendIOCompletion(req.deviceID, req.completionID, statusSuccess, out[:])
}
func (h *Handler) handleQueryVolume(req ioRequest) {
if len(req.payload) < 4 {
h.sendIOCompletion(req.deviceID, req.completionID, statusInvalidParameter, nil)
return
}
infoClass := binary.LittleEndian.Uint32(req.payload[0:4])
switch infoClass {
case fileFsVolumeInformation:
label := encodeUTF16(h.driveName)
out := make([]byte, 21+len(label))
binary.LittleEndian.PutUint32(out[0:4], uint32(17+len(label)))
binary.LittleEndian.PutUint64(out[4:12], filetimeNow())
binary.LittleEndian.PutUint32(out[12:16], 0x41494f50)
binary.LittleEndian.PutUint32(out[16:20], uint32(len(label)))
copy(out[21:], label)
h.sendIOCompletion(req.deviceID, req.completionID, statusSuccess, out)
case fileFsSizeInformation:
out := make([]byte, 28)
binary.LittleEndian.PutUint32(out[0:4], 24)
binary.LittleEndian.PutUint64(out[4:12], 1024*1024)
binary.LittleEndian.PutUint64(out[12:20], 1024*1024)
binary.LittleEndian.PutUint32(out[20:24], 8)
binary.LittleEndian.PutUint32(out[24:28], 512)
h.sendIOCompletion(req.deviceID, req.completionID, statusSuccess, out)
case fileFsDeviceInformation:
out := make([]byte, 12)
binary.LittleEndian.PutUint32(out[0:4], 8)
binary.LittleEndian.PutUint32(out[4:8], 0x00000007)
h.sendIOCompletion(req.deviceID, req.completionID, statusSuccess, out)
case fileFsAttributeInformation:
fsName := encodeUTF16("AIOPS")
out := make([]byte, 16+len(fsName))
binary.LittleEndian.PutUint32(out[0:4], uint32(12+len(fsName)))
binary.LittleEndian.PutUint32(out[4:8], 0x00000007)
binary.LittleEndian.PutUint32(out[8:12], 255)
binary.LittleEndian.PutUint32(out[12:16], uint32(len(fsName)))
copy(out[16:], fsName)
h.sendIOCompletion(req.deviceID, req.completionID, statusSuccess, out)
case fileFsFullSizeInformation:
out := make([]byte, 36)
binary.LittleEndian.PutUint32(out[0:4], 32)
binary.LittleEndian.PutUint64(out[4:12], 1024*1024)
binary.LittleEndian.PutUint64(out[12:20], 1024*1024)
binary.LittleEndian.PutUint64(out[20:28], 1024*1024)
binary.LittleEndian.PutUint32(out[28:32], 8)
binary.LittleEndian.PutUint32(out[32:36], 512)
h.sendIOCompletion(req.deviceID, req.completionID, statusSuccess, out)
default:
h.sendIOCompletion(req.deviceID, req.completionID, statusNotSupported, nil)
}
}
func (h *Handler) handleDeviceControl(req ioRequest) {
var out [4]byte
ioctl := uint32(0)
if len(req.payload) >= 12 {
ioctl = binary.LittleEndian.Uint32(req.payload[8:12])
}
// 资源管理器复制时会探测若干 FSCTL;明确返回“不支持”比 invalid parameter 更接近本地盘行为。// Logan.Li
log.Printf("[rdpdr] device control file=%d ioctl=0x%x payload=%d", req.fileID, ioctl, len(req.payload))
h.sendIOCompletion(req.deviceID, req.completionID, statusInvalidDeviceReq, out[:])
}
func (h *Handler) handleNotifyChangeDirectory(req ioRequest) {
// Windows 资源管理器会长期挂起目录变更通知;立即成功返回会触发重复刷新和“地址无效”。// Logan.Li
log.Printf("[rdpdr] notify change pending file=%d", req.fileID)
}
func (h *Handler) handleLockControl(req ioRequest) {
var out [4]byte
// 上传暂存文件是只读内存文件,锁请求直接成功,避免 Explorer 复制流程中断。// Logan.Li
log.Printf("[rdpdr] lock control file=%d payload=%d", req.fileID, len(req.payload))
h.sendIOCompletion(req.deviceID, req.completionID, statusSuccess, out[:])
}
func (h *Handler) handleQueryDirectory(req ioRequest) {
if len(req.payload) < 32 {
h.sendIOCompletion(req.deviceID, req.completionID, statusInvalidParameter, nil)
return
}
infoClass := binary.LittleEndian.Uint32(req.payload[0:4])
initial := req.payload[4] != 0
file, ok := h.getOpenFile(req.fileID)
if !ok || !file.isDir {
h.sendIOCompletion(req.deviceID, req.completionID, statusNoSuchFile, nil)
return
}
if initial {
file.dirIndex = 0
if file.path == "" {
file.dirEntries = []dirEntry{{name: "."}, {name: ".."}, {name: downloadDirName, isDir: true}, {name: uploadDirName, isDir: true}}
} else if strings.EqualFold(file.path, uploadDirName) {
file.dirEntries = append([]dirEntry{{name: "."}, {name: ".."}}, h.listUploads()...)
} else {
file.dirEntries = []dirEntry{{name: "."}, {name: ".."}}
}
}
log.Printf("[rdpdr] query dir file=%d class=%d path=%q initial=%t index=%d entries=%d", req.fileID, infoClass, file.path, initial, file.dirIndex, len(file.dirEntries))
if file.dirIndex >= len(file.dirEntries) {
var out [5]byte
h.sendIOCompletion(req.deviceID, req.completionID, statusNoMoreFiles, out[:])
return
}
entry := file.dirEntries[file.dirIndex]
file.dirIndex++
h.sendIOCompletion(req.deviceID, req.completionID, statusSuccess, buildDirInfo(infoClass, entry))
}
func buildDirInfo(infoClass uint32, entry dirEntry) []byte {
name := encodeUTF16(entry.name)
nameWithNull := append(append([]byte(nil), name...), 0, 0)
attrs := fileAttrNormal
if entry.isDir || entry.name == "." || entry.name == ".." {
attrs = fileAttrDirectory
}
if infoClass == fileNamesInformation {
out := make([]byte, 16+len(nameWithNull))
binary.LittleEndian.PutUint32(out[0:4], uint32(12+len(nameWithNull)))
binary.LittleEndian.PutUint32(out[12:16], uint32(len(nameWithNull)))
copy(out[16:], nameWithNull)
return out
}
fixed := 64
if infoClass == fileFullDirectoryInformation {
fixed = 68
}
if infoClass == fileBothDirectoryInformation {
fixed = 93
}
out := make([]byte, 4+fixed+len(nameWithNull))
binary.LittleEndian.PutUint32(out[0:4], uint32(fixed+len(nameWithNull)))
writeFileTimes(out[12:44])
binary.LittleEndian.PutUint64(out[44:52], entry.size)
binary.LittleEndian.PutUint64(out[52:60], align4096(entry.size))
binary.LittleEndian.PutUint32(out[60:64], attrs)
binary.LittleEndian.PutUint32(out[64:68], uint32(len(nameWithNull)))
copy(out[4+fixed:], nameWithNull)
return out
}
func fileDisplayName(file *openFile) string {
if file.name != "" {
return file.name
}
if file.path != "" {
parts := strings.Split(file.path, "\\")
return parts[len(parts)-1]
}
return "\\"
}
func classifyVirtualPath(vpath string) (string, string) {
if vpath == "" {
return "root", ""
}
parts := strings.Split(vpath, "\\")
if len(parts) == 1 && strings.EqualFold(parts[0], downloadDirName) {
return "download", downloadDirName
}
if len(parts) == 1 && strings.EqualFold(parts[0], uploadDirName) {
return "upload", uploadDirName
}
if len(parts) >= 2 && strings.EqualFold(parts[0], downloadDirName) {
name := parts[len(parts)-1]
if name == "" || name == "." || name == ".." {
return "missing", ""
}
return "file", name
}
if len(parts) >= 2 && strings.EqualFold(parts[0], uploadDirName) {
name := parts[len(parts)-1]
if name == "" || name == "." || name == ".." {
return "missing", ""
}
return "upload_file", name
}
return "missing", ""
}
func normalizeVirtualPath(raw string) string {
clean := strings.Trim(strings.ReplaceAll(raw, "/", "\\"), "\\\x00 ")
if clean == "" || clean == "." {
return ""
}
parts := strings.FieldsFunc(clean, func(r rune) bool { return r == '\\' })
safe := make([]string, 0, len(parts))
for _, part := range parts {
if part == "" || part == "." || part == ".." {
continue
}
safe = append(safe, part)
}
return strings.ReplaceAll(path.Clean(strings.Join(safe, "/")), "/", "\\")
}
func encodeUTF16(s string) []byte {
u16 := utf16.Encode([]rune(s))
out := make([]byte, len(u16)*2)
for i, v := range u16 {
binary.LittleEndian.PutUint16(out[i*2:], v)
}
return out
}
func decodeUTF16(data []byte) string {
u16 := make([]uint16, len(data)/2)
for i := range u16 {
u16[i] = binary.LittleEndian.Uint16(data[i*2:])
}
for len(u16) > 0 && u16[len(u16)-1] == 0 {
u16 = u16[:len(u16)-1]
}
return string(utf16.Decode(u16))
}
func writeFileTimes(out []byte) {
ft := filetimeNow()
for i := 0; i+8 <= len(out); i += 8 {
binary.LittleEndian.PutUint64(out[i:i+8], ft)
}
}
func filetimeNow() uint64 {
return uint64(time.Now().UnixNano()/100) + windowsFiletimeOffset
}
func align4096(v uint64) uint64 {
return (v + 4095) &^ 4095
}
@@ -0,0 +1,109 @@
package rdpdr
import (
"encoding/binary"
"testing"
)
type captureSender struct {
data [][]byte
}
func (s *captureSender) SendChannelPDU(_ uint16, data []byte) error {
s.data = append(s.data, append([]byte(nil), data...))
return nil
}
func TestClassifyVirtualPathUploadFile(t *testing.T) {
kind, name := classifyVirtualPath(`Upload\php.ini`)
if kind != "upload_file" || name != "php.ini" {
t.Fatalf("Upload file should be classified as upload_file, got kind=%q name=%q", kind, name)
}
}
func TestHandleCreateUploadFileOpensAsFile(t *testing.T) {
sender := &captureSender{}
handler := NewHandler("goclient", "AIOps")
handler.Attach(sender, 1004)
handler.AddUpload("php.ini", []byte("ok"))
encodedPath := encodeUTF16(`Upload\php.ini`)
payload := make([]byte, 32+len(encodedPath))
binary.LittleEndian.PutUint32(payload[20:24], fileOpen)
binary.LittleEndian.PutUint32(payload[24:28], fileDirectoryFile)
binary.LittleEndian.PutUint32(payload[28:32], uint32(len(encodedPath)))
copy(payload[32:], encodedPath)
handler.handleCreate(ioRequest{
deviceID: 1,
completionID: 7,
majorFn: irpCreate,
payload: payload,
})
if len(sender.data) == 0 {
t.Fatal("expected create completion")
}
completion := sender.data[len(sender.data)-1]
if len(completion) < 21 {
t.Fatalf("completion too short: %d", len(completion))
}
if status := binary.LittleEndian.Uint32(completion[12:16]); status != statusSuccess {
t.Fatalf("expected success, got status=0x%08x", status)
}
fileID := binary.LittleEndian.Uint32(completion[16:20])
file, ok := handler.getOpenFile(fileID)
if !ok {
t.Fatalf("open file %d not stored", fileID)
}
if file.isDir {
t.Fatalf("upload file opened as directory: %#v", file)
}
}
func TestHandleReadEOFReturnsZeroLengthSuccess(t *testing.T) {
sender := &captureSender{}
handler := NewHandler("goclient", "AIOps")
handler.Attach(sender, 1004)
fileID := handler.storeOpenFile(&openFile{path: `Upload\x.yaml`, name: "x.yaml", data: []byte("abc")})
payload := make([]byte, 20)
binary.LittleEndian.PutUint32(payload[0:4], 4096)
binary.LittleEndian.PutUint64(payload[4:12], 3)
handler.handleRead(ioRequest{
deviceID: 1,
fileID: fileID,
completionID: 9,
payload: payload,
})
completion := sender.data[len(sender.data)-1]
if status := binary.LittleEndian.Uint32(completion[12:16]); status != statusSuccess {
t.Fatalf("expected EOF success, got status=0x%08x", status)
}
if got := binary.LittleEndian.Uint32(completion[16:20]); got != 0 {
t.Fatalf("expected zero read length at EOF, got %d", got)
}
}
func TestHandleDeviceControlReturnsInvalidDeviceRequest(t *testing.T) {
sender := &captureSender{}
handler := NewHandler("goclient", "AIOps")
handler.Attach(sender, 1004)
payload := make([]byte, 32)
binary.LittleEndian.PutUint32(payload[8:12], 0x000900a8)
handler.handleDeviceControl(ioRequest{
deviceID: 1,
fileID: 2,
completionID: 10,
payload: payload,
})
completion := sender.data[len(sender.data)-1]
if status := binary.LittleEndian.Uint32(completion[12:16]); status != statusInvalidDeviceReq {
t.Fatalf("expected invalid device request, got status=0x%08x", status)
}
if got := binary.LittleEndian.Uint32(completion[16:20]); got != 0 {
t.Fatalf("expected empty device control output, got %d", got)
}
}
+388
View File
@@ -0,0 +1,388 @@
// Package rdpdr 实现 RDP 设备重定向静态虚拟通道的最小握手。// Logan.Li
package rdpdr
import (
"bytes"
"encoding/binary"
"fmt"
"log"
"sync"
"unicode/utf16"
)
type Sender interface {
SendChannelPDU(channelID uint16, data []byte) error
}
const ChannelName = "rdpdr"
const (
componentRDPDR = 0x4472
pakIDServerAnnounce = 0x496e
pakIDClientIDConfirm = 0x4343
pakIDClientName = 0x434e
pakIDServerCoreCap = 0x5350
pakIDClientCoreCap = 0x4350
pakIDDeviceListAnnounce = 0x4441
pakIDDeviceReply = 0x6472
pakIDDeviceIORequest = 0x4952
pakIDDeviceIOCompletion = 0x4943
pakIDUserLoggedOn = 0x554c
capGeneralType = 0x0001
capDriveType = 0x0004
deviceTypeDisk = 0x00000008
)
type Download struct {
Name string
Data []byte
}
type Handler struct {
sender Sender
channelID uint16
clientID uint32
gotServerCap bool
gotIDConfirm bool
serverIOCode1 uint32
computerName string
driveName string
devicePreannounced bool
deviceAnnounced bool
mu sync.Mutex
nextFileID uint32
files map[uint32]*openFile
uploads map[string][]byte
onDownload func(Download)
}
func NewHandler(computerName string, driveName string) *Handler {
if computerName == "" {
computerName = "goclient"
}
if driveName == "" {
driveName = "AIOPS"
}
return &Handler{computerName: computerName, driveName: driveName, nextFileID: 1, files: make(map[uint32]*openFile), uploads: make(map[string][]byte)}
}
func (h *Handler) SetDownloadHandler(fn func(Download)) {
h.onDownload = fn
}
func (h *Handler) AddUpload(name string, data []byte) {
if name == "" || len(data) == 0 {
return
}
h.mu.Lock()
h.uploads[name] = append([]byte(nil), data...)
h.mu.Unlock()
log.Printf("[rdpdr] upload staged name=%s size=%d", name, len(data))
}
func (h *Handler) listUploads() []dirEntry {
h.mu.Lock()
defer h.mu.Unlock()
out := make([]dirEntry, 0, len(h.uploads))
for name, data := range h.uploads {
out = append(out, dirEntry{name: name, size: uint64(len(data))})
}
return out
}
func (h *Handler) getUpload(name string) ([]byte, bool) {
h.mu.Lock()
defer h.mu.Unlock()
data, ok := h.uploads[name]
if !ok {
return nil, false
}
return append([]byte(nil), data...), true
}
func (h *Handler) Attach(sender Sender, channelID uint16) {
h.sender = sender
h.channelID = channelID
log.Printf("[rdpdr] attached channelID=%d", channelID)
}
func (h *Handler) PreannounceDevices() {
h.mu.Lock()
if h.devicePreannounced || h.sender == nil || h.channelID == 0 {
h.mu.Unlock()
return
}
h.devicePreannounced = true
h.mu.Unlock()
// 部分 Windows 会在图形首帧后才启动 rdpdr;预公告客户端能力和磁盘设备,尽早完成驱动器重定向。// Logan.Li
log.Printf("[rdpdr] preannounce devices client=%s drive=%s", h.computerName, h.driveName)
h.sendClientName()
h.sendCoreCapResponse()
h.sendDeviceList(true)
}
func (h *Handler) Process(data []byte) {
if len(data) < 4 {
return
}
component := binary.LittleEndian.Uint16(data[0:2])
packetID := binary.LittleEndian.Uint16(data[2:4])
body := data[4:]
if component != componentRDPDR {
log.Printf("[rdpdr] ignore component=0x%x packet=0x%x len=%d", component, packetID, len(data))
return
}
log.Printf("[rdpdr] recv packet=0x%x len=%d", packetID, len(body))
switch packetID {
case pakIDServerAnnounce:
h.handleServerAnnounce(body)
case pakIDServerCoreCap:
h.handleServerCoreCap(body)
case pakIDClientIDConfirm:
h.handleClientIDConfirm(body)
case pakIDDeviceReply:
h.handleDeviceReply(body)
case pakIDDeviceIORequest:
h.handleIORequest(body)
case pakIDUserLoggedOn:
h.sendDeviceListOnce()
default:
log.Printf("[rdpdr] unhandled packet=0x%x len=%d", packetID, len(body))
}
}
func (h *Handler) handleServerAnnounce(body []byte) {
if len(body) < 8 {
return
}
h.clientID = binary.LittleEndian.Uint32(body[4:8])
log.Printf("[rdpdr] server announce clientID=%d version=%d.%d", h.clientID, binary.LittleEndian.Uint16(body[0:2]), binary.LittleEndian.Uint16(body[2:4]))
buf := make([]byte, 12)
binary.LittleEndian.PutUint16(buf[0:2], componentRDPDR)
binary.LittleEndian.PutUint16(buf[2:4], pakIDClientIDConfirm)
binary.LittleEndian.PutUint16(buf[4:6], 1)
binary.LittleEndian.PutUint16(buf[6:8], 0x000c)
binary.LittleEndian.PutUint32(buf[8:12], h.clientID)
h.send(buf)
h.sendClientName()
}
func (h *Handler) sendClientName() {
runes := utf16.Encode([]rune(h.computerName + "\x00"))
name := make([]byte, len(runes)*2)
for i, r := range runes {
binary.LittleEndian.PutUint16(name[i*2:], r)
}
buf := make([]byte, 16+len(name))
binary.LittleEndian.PutUint16(buf[0:2], componentRDPDR)
binary.LittleEndian.PutUint16(buf[2:4], pakIDClientName)
binary.LittleEndian.PutUint32(buf[4:8], 1)
binary.LittleEndian.PutUint32(buf[12:16], uint32(len(name)))
copy(buf[16:], name)
h.send(buf)
}
func (h *Handler) handleServerCoreCap(body []byte) {
if len(body) < 4 {
return
}
count := binary.LittleEndian.Uint16(body[0:2])
offset := 4
for i := 0; i < int(count) && offset+8 <= len(body); i++ {
capType := binary.LittleEndian.Uint16(body[offset:])
capLen := int(binary.LittleEndian.Uint16(body[offset+2:]))
if capLen < 8 || offset+capLen > len(body) {
break
}
if capType == capGeneralType && capLen >= 24 {
h.serverIOCode1 = binary.LittleEndian.Uint32(body[offset+20 : offset+24])
}
offset += capLen
}
h.gotServerCap = true
h.trySendReady()
}
func (h *Handler) handleClientIDConfirm(body []byte) {
if len(body) >= 8 {
h.clientID = binary.LittleEndian.Uint32(body[4:8])
}
h.gotIDConfirm = true
h.trySendReady()
}
func (h *Handler) trySendReady() {
if !h.gotServerCap || !h.gotIDConfirm {
return
}
h.sendCoreCapResponse()
// 能力协商完成后立即公告虚拟盘,避免首次登录后资源管理器等待网络位置刷新。// Logan.Li
h.sendDeviceListOnce()
}
func (h *Handler) sendCoreCapResponse() {
const generalCapLen = 44
const driveCapLen = 8
buf := bytes.Buffer{}
writeLE16(&buf, componentRDPDR)
writeLE16(&buf, pakIDClientCoreCap)
writeLE16(&buf, 2)
writeLE16(&buf, 0)
writeLE16(&buf, capGeneralType)
writeLE16(&buf, generalCapLen)
writeLE32(&buf, 2)
writeLE32(&buf, 0)
writeLE32(&buf, 0)
writeLE16(&buf, 1)
writeLE16(&buf, 0x000c)
ioCode1 := h.serverIOCode1 & 0x0000ffff
if ioCode1 == 0 {
ioCode1 = 0x0000ffff
}
writeLE32(&buf, ioCode1)
writeLE32(&buf, 0)
writeLE32(&buf, 0x00000007)
writeLE32(&buf, 0x00000001)
writeLE32(&buf, 0)
writeLE32(&buf, 0)
writeLE16(&buf, capDriveType)
writeLE16(&buf, driveCapLen)
writeLE32(&buf, 2)
h.send(buf.Bytes())
}
func (h *Handler) sendDeviceList(includeDevices bool) {
deviceData := []byte(nil)
deviceCount := uint32(0)
totalLen := 8
if includeDevices {
deviceCount = 1
deviceData = []byte(h.driveName)
totalLen += 20 + len(deviceData)
}
if deviceCount == 0 {
// FreeRDP 在没有预登录设备时直接跳过空公告,避免 Windows 记录一个无设备列表。// Logan.Li
log.Printf("[rdpdr] skip empty device list announce")
return
}
buf := make([]byte, totalLen)
binary.LittleEndian.PutUint16(buf[0:2], componentRDPDR)
binary.LittleEndian.PutUint16(buf[2:4], pakIDDeviceListAnnounce)
binary.LittleEndian.PutUint32(buf[4:8], deviceCount)
if includeDevices {
// 公告一个磁盘设备,Windows 里显示为 \\goclient\AIOPS。// Logan.Li
binary.LittleEndian.PutUint32(buf[8:12], deviceTypeDisk)
binary.LittleEndian.PutUint32(buf[12:16], 1)
copy(buf[16:24], []byte(dosDeviceName(h.driveName)))
binary.LittleEndian.PutUint32(buf[24:28], uint32(len(deviceData)))
copy(buf[28:], deviceData)
}
h.send(buf)
log.Printf("[rdpdr] device list announced devices=%d name=%s", deviceCount, h.driveName)
}
func (h *Handler) sendDeviceListOnce() {
h.mu.Lock()
if h.deviceAnnounced {
h.mu.Unlock()
return
}
h.deviceAnnounced = true
h.mu.Unlock()
h.sendDeviceList(true)
}
func dosDeviceName(name string) string {
if name == "" {
return "AIOPS"
}
if len(name) > 8 {
return name[:8]
}
return name
}
func (h *Handler) send(data []byte) {
if h.sender == nil || h.channelID == 0 {
return
}
if err := h.sender.SendChannelPDU(h.channelID, data); err != nil {
log.Printf("[rdpdr] send failed: %v", err)
}
}
func (h *Handler) sendIOCompletion(deviceID, completionID, status uint32, output []byte) {
buf := make([]byte, 16+len(output))
binary.LittleEndian.PutUint16(buf[0:2], componentRDPDR)
binary.LittleEndian.PutUint16(buf[2:4], pakIDDeviceIOCompletion)
binary.LittleEndian.PutUint32(buf[4:8], deviceID)
binary.LittleEndian.PutUint32(buf[8:12], completionID)
binary.LittleEndian.PutUint32(buf[12:16], status)
copy(buf[16:], output)
h.send(buf)
}
func (h *Handler) allocFileID() uint32 {
h.mu.Lock()
defer h.mu.Unlock()
id := h.nextFileID
h.nextFileID++
return id
}
func (h *Handler) storeOpenFile(file *openFile) uint32 {
id := h.allocFileID()
h.mu.Lock()
h.files[id] = file
h.mu.Unlock()
return id
}
func (h *Handler) getOpenFile(id uint32) (*openFile, bool) {
h.mu.Lock()
defer h.mu.Unlock()
file, ok := h.files[id]
return file, ok
}
func (h *Handler) removeOpenFile(id uint32) (*openFile, bool) {
h.mu.Lock()
defer h.mu.Unlock()
file, ok := h.files[id]
if ok {
delete(h.files, id)
}
return file, ok
}
func (h *Handler) handleDeviceReply(body []byte) {
if len(body) < 8 {
return
}
deviceID := binary.LittleEndian.Uint32(body[0:4])
status := binary.LittleEndian.Uint32(body[4:8])
if status == statusSuccess {
h.mu.Lock()
h.deviceAnnounced = true
h.mu.Unlock()
}
log.Printf("[rdpdr] device reply deviceID=%d status=0x%08x", deviceID, status)
}
func writeLE16(w *bytes.Buffer, v uint16) {
_ = binary.Write(w, binary.LittleEndian, v)
}
func writeLE32(w *bytes.Buffer, v uint32) {
_ = binary.Write(w, binary.LittleEndian, v)
}
func (h *Handler) String() string {
return fmt.Sprintf("rdpdr(channel=%d)", h.channelID)
}
+8
View File
@@ -31,9 +31,14 @@ type ClientEvent struct {
Code string `json:"code,omitempty"`
Key string `json:"key,omitempty"`
Down bool `json:"down,omitempty"`
Ctrl bool `json:"ctrl,omitempty"`
Alt bool `json:"alt,omitempty"`
Shift bool `json:"shift,omitempty"`
Meta bool `json:"meta,omitempty"`
Width int `json:"width,omitempty"`
Height int `json:"height,omitempty"`
Text string `json:"text,omitempty"`
Name string `json:"name,omitempty"`
}
// ServerFrame 是后端推给浏览器 Canvas 的统一帧协议 // Logan.Li
@@ -46,6 +51,9 @@ type ServerFrame struct {
Image string `json:"image,omitempty"`
Message string `json:"message,omitempty"`
Text string `json:"text,omitempty"`
Name string `json:"name,omitempty"`
URL string `json:"url,omitempty"`
Size int64 `json:"size,omitempty"`
RGBA []byte `json:"-"`
}
+41
View File
@@ -0,0 +1,41 @@
package filestage
import (
"sync"
"time"
"github.com/google/uuid"
)
// Item 暂存文件条目 // Logan.Li
type Item struct {
Name string
Data []byte
Expire time.Time
}
var store sync.Map
// Put 暂存文件并返回一次性 token(默认 30 分钟有效)// Logan.Li
func Put(name string, data []byte, ttl time.Duration) string {
if ttl <= 0 {
ttl = 30 * time.Minute
}
token := uuid.NewString()
store.Store(token, Item{Name: name, Data: data, Expire: time.Now().Add(ttl)})
return token
}
// Get 读取暂存文件 // Logan.Li
func Get(token string) (Item, bool) {
raw, ok := store.Load(token)
if !ok {
return Item{}, false
}
item := raw.(Item)
if time.Now().After(item.Expire) {
store.Delete(token)
return Item{}, false
}
return item, true
}
+7 -10
View File
@@ -136,17 +136,10 @@ func (s *InputSender) AutoExec(command string) error {
if err := s.Key(0xE05B, true); err != nil {
return err
}
if err := s.Key(0xE05B, false); err != nil {
if err := s.Key(0x0013, true); err != nil {
return err
}
time.Sleep(80 * time.Millisecond)
if err := s.Key(0xE05B, true); err != nil {
return err
}
if err := s.Key(0x0015, true); err != nil {
return err
}
if err := s.Key(0x0015, false); err != nil {
if err := s.Key(0x0013, false); err != nil {
return err
}
if err := s.Key(0xE05B, false); err != nil {
@@ -157,5 +150,9 @@ func (s *InputSender) AutoExec(command string) error {
return err
}
time.Sleep(80 * time.Millisecond)
return s.Key(0x001C, true)
if err := s.Key(0x001C, true); err != nil {
return err
}
time.Sleep(20 * time.Millisecond)
return s.Key(0x001C, false)
}
+84 -26
View File
@@ -37,6 +37,17 @@ const (
gccSCSecurity = 0x0C02
gccSCNet = 0x0C03
// 静态虚拟通道声明(MS-RDPBCGR 2.2.7.1.1 Channel Pool)
// channel name 固定 8 字节 ASCII,不足补 0;options 控制通道加密/压缩行为 // Logan.Li
channelNameRdpdr = "rdpdr"
channelNameRdpsnd = "rdpsnd"
channelNameCliprdr = "cliprdr"
channelNameDrdynvc = "drdynvc"
channelOptionInitialized = 0x80000000
channelOptionEncryptRDP = 0x40000000
channelOptionShowProtocol = 0x00200000
channelOptionCompressRDP = 0x00800000
secFlagInfoPkt = 0x0040
secFlagLicensePkt = 0x0080
@@ -65,11 +76,15 @@ var errFinalizeActive = errors.New("RDP 会话已进入图形更新流")
// MCSProbeInfo 记录 NLA 后 MCS/GCC 连接期响应,用于确认图形会话层入口是否跑通 // Logan.Li
type MCSProbeInfo struct {
HandshakeInfo
UserDataBytes int
ServerCoreVersion uint32
ServerChannelID uint16
ServerChannelCount uint16
ServerChannelIDs []uint16
UserDataBytes int
ServerCoreVersion uint32
ServerChannelID uint16
ServerChannelCount uint16
ServerChannelIDs []uint16
// ChannelNameByID 记录服务端在 Connect-Response 里分配的 channel ID 对应的名称。
// 客户端在 Connect-Initial 里声明的 channel def 顺序与服务端返回的 ServerChannelIDs 一一对应,
// 据此建立 ID→名称映射,供 readLoop 按通道分发虚拟通道数据(cliprdr/rdpdr)。// Logan.Li
ChannelNameByID map[uint16]string
ServerEncryption uint32
ServerEncryptionLvl uint32
UserChannelID uint16
@@ -179,7 +194,7 @@ func probeMCSConnectOnce(ctx context.Context, cfg Config, address string, opts c
mcsInfo.DemandCapabilities = demand.capabilityCount
mcsInfo.DemandSource = string(demand.sourceDescriptor)
rdpDebugf(cfg, "demand active shared=0x%08x caps=%d source=%q firstSecurity=0x%04x firstPayload=%d", demand.sharedID, demand.capabilityCount, string(demand.sourceDescriptor), first.securityFlags, len(first.payload))
postTypes, err := sendConfirmActiveAndFinalize(tlsConn, cfg, userID, demand)
postTypes, _, err := sendConfirmActiveAndFinalize(tlsConn, cfg, userID, demand)
if err != nil {
return MCSProbeInfo{}, fmt.Errorf("Client Finalize 失败: shared=0x%08x caps=%d source=%q: %w", demand.sharedID, demand.capabilityCount, string(demand.sourceDescriptor), err)
}
@@ -258,7 +273,7 @@ func sendClientInfoAndReadDemand(rw io.ReadWriter, cfg Config, userID uint16) (m
return first, demand, nil
}
func sendConfirmActiveAndFinalize(rw io.ReadWriter, cfg Config, userID uint16, demand demandActiveProbe) ([]uint8, error) {
func sendConfirmActiveAndFinalize(rw io.ReadWriter, cfg Config, userID uint16, demand demandActiveProbe) ([]uint8, []mcsDataIndication, error) {
rdpDebugf(cfg, "finalize start userID=%d shared=0x%08x", userID, demand.sharedID)
pdus := []struct {
name string
@@ -274,22 +289,22 @@ func sendConfirmActiveAndFinalize(rw io.ReadWriter, cfg Config, userID uint16, d
for _, pdu := range pdus {
rdpDebugf(cfg, "finalize send %s bytes=%d", pdu.name, len(pdu.data))
if err := transport.WriteX224Data(rw, buildMCSSendDataRequest(userID, mcsGlobalChannelID, pdu.data)); err != nil {
return nil, fmt.Errorf("发送 %s 失败: %w", pdu.name, err)
return nil, nil, fmt.Errorf("发送 %s 失败: %w", pdu.name, err)
}
if isRDPStepMode(cfg) {
if stepTypes, err := readFinalizeStep(rw, pdu.name, 500*time.Millisecond); err != nil {
return append(types, stepTypes...), err
return append(types, stepTypes...), nil, err
} else if len(stepTypes) > 0 {
types = append(types, stepTypes...)
}
}
}
readTypes, err := waitFinalizeResponses(rw, cfg, 5*time.Second)
readTypes, pending, err := waitFinalizeResponses(rw, cfg, 5*time.Second)
if err != nil {
return types, err
return types, pending, err
}
types = append(types, readTypes...)
return types, nil
return types, pending, nil
}
func isRDPStepMode(cfg Config) bool {
@@ -321,7 +336,7 @@ func readFinalizeStep(rw io.ReadWriter, name string, timeout time.Duration) ([]u
return []uint8{got.type2}, nil
}
func waitFinalizeResponses(rw io.ReadWriter, cfg Config, timeout time.Duration) ([]uint8, error) {
func waitFinalizeResponses(rw io.ReadWriter, cfg Config, timeout time.Duration) ([]uint8, []mcsDataIndication, error) {
if conn, ok := rw.(interface{ SetReadDeadline(time.Time) error }); ok {
_ = conn.SetReadDeadline(time.Now().Add(timeout))
defer conn.SetReadDeadline(time.Time{})
@@ -333,28 +348,36 @@ func waitFinalizeResponses(rw io.ReadWriter, cfg Config, timeout time.Duration)
{type2: pduType2FontMap},
}
var types []uint8
var pending []mcsDataIndication
for _, want := range expected {
for {
payload, err := readNextSlowPathPayload(rw, cfg, timeout)
if err != nil {
if errors.Is(err, errFinalizeActive) || len(types) > 0 {
rdpDebugf(cfg, "finalize accept active session types=%v err=%v", types, err)
return types, nil
return types, pending, nil
}
return types, fmt.Errorf("读取 Client Finalize 响应失败: %w", err)
return types, pending, fmt.Errorf("读取 Client Finalize 响应失败: %w", err)
}
data, err := parseMCSDataIndication(payload)
if err != nil {
return types, err
return types, pending, err
}
// 虚拟通道数据(cliprdr/rdpdr 等,channel ≠ 1003 global)在 finalize 阶段
// 先缓存,等 SessionEngine 创建对应 handler 后再分发。 // Logan.Li
if data.channelID != mcsGlobalChannelID {
rdpDebugf(cfg, "finalize skip virtual channel data channel=%d payload=%d", data.channelID, len(data.payload))
pending = append(pending, data)
continue
}
got, err := parseShareDataPDUInfo(data.payload)
if err != nil {
return types, err
return types, pending, err
}
types = append(types, got.type2)
rdpDebugf(cfg, "finalize recv type2=0x%02x action=0x%04x raw=%s", got.type2, got.controlAction, got.rawPrefix)
if got.type2 == pduType2SetErrorInfo {
return types, fmt.Errorf("服务端返回 ErrorInfo: code=0x%08x(%s) seen=%v raw=%s", got.errorInfo, rdpErrorInfoName(got.errorInfo), types, got.rawPrefix)
return types, pending, fmt.Errorf("服务端返回 ErrorInfo: code=0x%08x(%s) seen=%v raw=%s", got.errorInfo, rdpErrorInfoName(got.errorInfo), types, got.rawPrefix)
}
if got.type2 == pduType2SaveSessionInfo {
continue
@@ -368,7 +391,7 @@ func waitFinalizeResponses(rw io.ReadWriter, cfg Config, timeout time.Duration)
break
}
}
return types, nil
return types, pending, nil
}
func readNextSlowPathPayload(r io.Reader, cfg Config, timeout time.Duration) ([]byte, error) {
@@ -492,13 +515,13 @@ func buildConfirmCapabilities(cfg Config) ([]byte, uint16) {
writeCapability(&out, capType, body)
count++
}
if cfg.MCSMode == "grdp-comment" || cfg.MCSMode == "step-grdp-comment" {
if cfg.MCSMode == "comment" || cfg.MCSMode == "step-comment" || cfg.MCSMode == "grdp-comment" || cfg.MCSMode == "step-grdp-comment" {
return buildGRDPCommentCapabilities(cfg, width, height)
}
addCapability(0x0001, buildGeneralCapability())
bitmap := bytes.Buffer{}
preferredBPP := uint16(24)
if cfg.MCSMode == "bpp32" || cfg.MCSMode == "step-bpp32" || cfg.MCSMode == "grdp-modern" || cfg.MCSMode == "step-grdp-modern" || cfg.MCSMode == "grdp-raw" || cfg.MCSMode == "step-grdp-raw" {
if cfg.MCSMode == "bpp32" || cfg.MCSMode == "step-bpp32" || cfg.MCSMode == "modern" || cfg.MCSMode == "step-modern" || cfg.MCSMode == "raw" || cfg.MCSMode == "step-raw" || cfg.MCSMode == "grdp-modern" || cfg.MCSMode == "step-grdp-modern" || cfg.MCSMode == "grdp-raw" || cfg.MCSMode == "step-grdp-raw" {
preferredBPP = 32
}
writeLE16(&bitmap, preferredBPP)
@@ -509,7 +532,7 @@ func buildConfirmCapabilities(cfg Config) ([]byte, uint16) {
writeLE16(&bitmap, uint16(height))
writeLE16(&bitmap, 0)
writeLE16(&bitmap, 1)
if cfg.MCSMode == "grdp-raw" || cfg.MCSMode == "step-grdp-raw" {
if cfg.MCSMode == "raw" || cfg.MCSMode == "step-raw" || cfg.MCSMode == "grdp-raw" || cfg.MCSMode == "step-grdp-raw" {
writeLE16(&bitmap, 0)
} else {
writeLE16(&bitmap, 1)
@@ -544,7 +567,7 @@ func buildConfirmCapabilities(cfg Config) ([]byte, uint16) {
addCapability(0x000e, mustHex("01000000"))
return out.Bytes(), count
}
if cfg.MCSMode == "grdp-modern" || cfg.MCSMode == "step-grdp-modern" || cfg.MCSMode == "grdp-raw" || cfg.MCSMode == "step-grdp-raw" {
if cfg.MCSMode == "modern" || cfg.MCSMode == "step-modern" || cfg.MCSMode == "raw" || cfg.MCSMode == "step-raw" || cfg.MCSMode == "grdp-modern" || cfg.MCSMode == "step-grdp-modern" || cfg.MCSMode == "grdp-raw" || cfg.MCSMode == "step-grdp-raw" {
addCapability(0x0003, buildGRDPOrderCapability())
addCapability(0x0005, mustHex("0000000002000200"))
addCapability(0x0007, mustHex("0000000000000000"))
@@ -575,7 +598,7 @@ func buildConfirmCapabilities(cfg Config) ([]byte, uint16) {
addCapability(0x0010, make([]byte, 48))
addCapability(0x0013, buildBitmapCacheRev2Capability())
addCapability(0x0011, mustHex("0000000000000000"))
addCapability(0x0014, mustHex("0000000000000000"))
addCapability(0x0014, mustHex("0000000040060000"))
addCapability(0x001a, mustHex("00000000"))
return out.Bytes(), count
}
@@ -887,6 +910,15 @@ func parseMCSChannelJoinConfirm(data []byte, expectedUserID uint16, expectedChan
return channelID, nil
}
// ParseMCSChannel 解析 MCS SendDataIndication,返回 channel ID(探针诊断用)。// Logan.Li
func ParseMCSChannel(data []byte) (uint16, error) {
ind, err := parseMCSDataIndication(data)
if err != nil {
return 0, err
}
return ind.channelID, nil
}
func parseMCSDataIndication(data []byte) (mcsDataIndication, error) {
r := bytes.NewReader(data)
pdu, err := readMCSPDUHeader(r)
@@ -1118,7 +1150,7 @@ func buildGCCClientCore(cfg Config) []byte {
}
highColorDepth := uint16(24)
supportedColorDepths := uint16(0x000f)
if cfg.MCSMode == "bpp32" || cfg.MCSMode == "step-bpp32" || cfg.MCSMode == "grdp-modern" || cfg.MCSMode == "step-grdp-modern" {
if cfg.MCSMode == "bpp32" || cfg.MCSMode == "step-bpp32" || cfg.MCSMode == "modern" || cfg.MCSMode == "step-modern" || cfg.MCSMode == "grdp-modern" || cfg.MCSMode == "step-grdp-modern" {
highColorDepth = 32
supportedColorDepths = 0x001f
}
@@ -1148,9 +1180,28 @@ func buildGCCClientCore(cfg Config) []byte {
return withGCCBlock(gccCSCore, body.Bytes())
}
// declaredStaticChannels 是客户端在 MCS Connect-Initial 里声明的静态虚拟通道。
// 顺序与服务端 Connect-Response 的 SCNet.ChannelIdArray 一一对应,保持接近 MSTSC/FreeRDP 的通道组合。// Logan.Li
var declaredStaticChannels = []struct {
Name string
Options uint32
}{
{Name: channelNameRdpdr, Options: channelOptionInitialized | channelOptionEncryptRDP},
{Name: channelNameRdpsnd, Options: channelOptionInitialized | channelOptionEncryptRDP},
{Name: channelNameCliprdr, Options: channelOptionInitialized | channelOptionEncryptRDP | channelOptionShowProtocol},
{Name: channelNameDrdynvc, Options: channelOptionInitialized | channelOptionCompressRDP},
}
func buildGCCClientNetwork() []byte {
body := bytes.Buffer{}
writeLE32(&body, 0)
writeLE32(&body, uint32(len(declaredStaticChannels))) // channelCount
for _, ch := range declaredStaticChannels {
// channel name: 8 字节 ASCII,不足补 0 // Logan.Li
nameBuf := make([]byte, 8)
copy(nameBuf, ch.Name)
body.Write(nameBuf)
writeLE32(&body, ch.Options)
}
return withGCCBlock(gccCSNet, body.Bytes())
}
@@ -1256,6 +1307,13 @@ func parseGCCServerBlock(blockType uint16, body []byte, info *MCSProbeInfo) {
for i := 0; i < int(info.ServerChannelCount) && r.Len() >= 2; i++ {
channelID, _ := readLE16(r)
info.ServerChannelIDs = append(info.ServerChannelIDs, channelID)
// 与 buildGCCClientNetwork 声明的 declaredStaticChannels 按顺序对应,建立 ID→名称映射。 // Logan.Li
if i < len(declaredStaticChannels) {
if info.ChannelNameByID == nil {
info.ChannelNameByID = make(map[uint16]string)
}
info.ChannelNameByID[channelID] = declaredStaticChannels[i].Name
}
}
case 0x0C04:
info.MessageChannelID, _ = readLE16(r)
+28 -12
View File
@@ -19,6 +19,11 @@ type Session struct {
SharedID uint32
Width int
Height int
// ChannelNameByID 记录服务端分配的静态虚拟通道 ID→名称(cliprdr 等),
// 供 readLoop 按通道分发数据给对应 Handler。空表示服务端未授权任何虚拟通道。// Logan.Li
ChannelNameByID map[uint16]string
// PendingChannelPDUs 缓存握手收尾阶段提前到达的虚拟通道包,避免 cliprdr 初始化被吞。// Logan.Li
PendingChannelPDUs []mcsDataIndication
}
// OpenSession 完成 CredSSP + MCS/GCC 握手并进入可收帧状态 // Logan.Li
@@ -31,7 +36,7 @@ func OpenSession(ctx context.Context, cfg Config) (*Session, error) {
cfg.MCSMode = strings.TrimSpace(os.Getenv("RDP_MCS_MODE"))
}
if cfg.MCSMode == "" {
cfg.MCSMode = "grdp-modern"
cfg.MCSMode = "modern"
}
address := fmt.Sprintf("%s:%d", cfg.Host, port)
strategies := []credssp.Options{
@@ -80,6 +85,9 @@ func openSessionOnce(ctx context.Context, cfg Config, address string, opts creds
}
mcsInfo, _ := parseMCSConnectResponse(payload)
joinIDs := append([]uint16{mcsGlobalChannelID, userID}, mcsInfo.ServerChannelIDs...)
if mcsInfo.MessageChannelID != 0 {
joinIDs = append(joinIDs, mcsInfo.MessageChannelID)
}
for _, channelID := range joinIDs {
if _, err := joinMCSChannel(tlsConn, userID, channelID); err != nil {
_ = tlsConn.Close()
@@ -91,14 +99,17 @@ func openSessionOnce(ctx context.Context, cfg Config, address string, opts creds
_ = tlsConn.Close()
return nil, err
}
if _, err := sendConfirmActiveAndFinalize(tlsConn, cfg, userID, demand); err != nil {
_, pending, err := sendConfirmActiveAndFinalize(tlsConn, cfg, userID, demand)
if err != nil {
_ = tlsConn.Close()
return nil, err
}
if err := requestDisplayUpdates(tlsConn, cfg, userID, demand.sharedID); err != nil {
morePending, err := requestDisplayUpdates(tlsConn, cfg, userID, demand.sharedID)
if err != nil {
_ = tlsConn.Close()
return nil, err
}
pending = append(pending, morePending...)
width := cfg.Width
if width <= 0 {
width = 1280
@@ -109,29 +120,34 @@ func openSessionOnce(ctx context.Context, cfg Config, address string, opts creds
}
_ = tlsConn.SetDeadline(time.Time{})
return &Session{
Conn: tlsConn,
UserID: userID,
SharedID: demand.sharedID,
Width: width,
Height: height,
Conn: tlsConn,
UserID: userID,
SharedID: demand.sharedID,
Width: width,
Height: height,
ChannelNameByID: mcsInfo.ChannelNameByID,
PendingChannelPDUs: pending,
}, nil
}
func requestDisplayUpdates(rw io.ReadWriter, cfg Config, userID uint16, sharedID uint32) error {
func requestDisplayUpdates(rw io.ReadWriter, cfg Config, userID uint16, sharedID uint32) ([]mcsDataIndication, error) {
var pending []mcsDataIndication
if conn, ok := rw.(interface{ SetReadDeadline(time.Time) error }); ok {
_ = conn.SetReadDeadline(time.Now().Add(3 * time.Second))
pkt, err := ReadWirePacket(rw)
_ = conn.SetReadDeadline(time.Time{})
if err == nil && !pkt.FastPath {
_, _ = parseMCSDataIndication(pkt.Payload)
if ind, perr := parseMCSDataIndication(pkt.Payload); perr == nil && ind.channelID != mcsGlobalChannelID {
pending = append(pending, ind)
}
}
}
body := buildSuppressOutputBody(cfg, true)
pdu := buildDataPDU(userID, sharedID, pduType2SuppressOutput, body)
if err := transport.WriteX224Data(rw, buildMCSSendDataRequest(userID, mcsGlobalChannelID, pdu)); err != nil {
return fmt.Errorf("发送 SuppressOutput 失败: %w", err)
return pending, fmt.Errorf("发送 SuppressOutput 失败: %w", err)
}
return nil
return pending, nil
}
func buildSuppressOutputBody(cfg Config, allow bool) []byte {
+391 -18
View File
@@ -2,16 +2,82 @@ package rdp
import (
"context"
"encoding/base64"
"encoding/binary"
"fmt"
"io"
"log"
"strings"
"sync"
"time"
"unicode/utf16"
"ezops/pkg/rdp/channels/cliprdr"
"ezops/pkg/rdp/channels/rdpdr"
"ezops/pkg/rdp/display"
"ezops/pkg/rdp/filestage"
"ezops/pkg/rdp/trace"
"ezops/pkg/rdp/transport"
)
// channelSender 适配 cliprdr.Sender 接口:把虚拟通道 PDU 通过 MCS SendDataRequest
// 发往指定 channelID,复用会话 writeMu 避免与 FastPath 输入并发写连接。// Logan.Li
type channelSender struct {
conn io.ReadWriter
userID uint16
writeMu *sync.Mutex
showProtocol bool
}
// 虚拟通道数据标志(MS-RDPBCGR 2.2.6.1 Virtual Channel PDU Header)
const (
secFlagChannelPkt = 0x0004
channelFlagFirst = 0x00000001
channelFlagLast = 0x00000002
channelFlagShow = 0x00000010
channelChunkLength = 1600 // 单个 MCS PDU 的虚拟通道数据上限
)
// SendChannelPDU 按 MS-RDPBCGR 静态虚拟通道格式发送:
// 所有静态虚拟通道发送都带 Channel PDU Header,showProtocol 只控制 flags。// Logan.Li
func (s *channelSender) SendChannelPDU(channelID uint16, data []byte) error {
s.writeMu.Lock()
defer s.writeMu.Unlock()
totalLen := uint32(len(data))
baseFlag := uint32(0)
if s.showProtocol {
baseFlag |= channelFlagShow
}
remaining := len(data)
offset := 0
first := true
for remaining > 0 {
chunkSize := remaining
if chunkSize > channelChunkLength {
chunkSize = channelChunkLength
}
flag := baseFlag
if first {
flag |= channelFlagFirst
first = false
}
remaining -= chunkSize
if remaining == 0 {
flag |= channelFlagLast
}
// 虚拟通道数据格式: [Channel PDU Header(totalLen+flags)] + [data]。// Logan.Li
buf := make([]byte, 8+chunkSize)
binary.LittleEndian.PutUint32(buf[0:], totalLen) // Channel PDU totalLen
binary.LittleEndian.PutUint32(buf[4:], flag) // Channel PDU flags
copy(buf[8:], data[offset:offset+chunkSize])
if err := transport.WriteX224Data(s.conn, buildMCSSendDataRequest(s.userID, channelID, buf)); err != nil {
return err
}
offset += chunkSize
}
return nil
}
// SessionEngine 是自研完整 RDP 图形会话引擎 // Logan.Li
type SessionEngine struct{}
@@ -43,11 +109,77 @@ func (e *SessionEngine) Run(ctx context.Context, cfg Config, sink FrameSink, eve
input := NewInputSenderWithLock(sess.Conn, writeMu)
receiver := NewFastPathReceiver()
// 剪贴板虚拟通道:服务端授权了 cliprdr 通道才创建 Handler。// Logan.Li
var clip *cliprdr.Handler
var clipChannelID uint16
var drive *rdpdr.Handler
var driveChannelID uint16
log.Printf("[RDP] mcs channels userID=%d global=%d granted=%d map=%v", sess.UserID, mcsGlobalChannelID, len(sess.ChannelNameByID), sess.ChannelNameByID)
for id, name := range sess.ChannelNameByID {
if name == cliprdr.ChannelName {
clipChannelID = id
}
if name == rdpdr.ChannelName {
driveChannelID = id
}
}
if driveChannelID != 0 {
drive = rdpdr.NewHandler("Go-client", "AIOps")
drive.SetDownloadHandler(func(file rdpdr.Download) {
if text, ok := decodeRDPClipboardFallback(file); ok {
_ = sink.SendFrame(ServerFrame{Type: "clipboard", Text: text, Message: "已读取远端剪贴板"})
log.Printf("[RDP] rdpdr clipboard fallback ready len=%d", len(text))
return
}
token := filestage.Put(file.Name, file.Data, 30*time.Minute)
_ = sink.SendFrame(ServerFrame{
Type: "rdp_file_download",
Name: file.Name,
URL: "/api/v1/rdp-staged/" + token,
Size: int64(len(file.Data)),
Message: "已捕获远端文件:" + file.Name,
})
log.Printf("[RDP] rdpdr download ready name=%s size=%d token=%s", file.Name, len(file.Data), token)
})
drive.Attach(&channelSender{conn: sess.Conn, userID: sess.UserID, writeMu: writeMu}, driveChannelID)
log.Printf("[RDP] rdpdr channel attached id=%d", driveChannelID)
drive.PreannounceDevices()
go func() {
time.Sleep(2 * time.Second)
drive.PreannounceDevices()
}()
}
if clipChannelID != 0 {
clip = cliprdr.NewHandler(func(text string) {
// 远端→本地:把远端剪贴板文本通过 WS clipboard 帧推给浏览器
_ = sink.SendFrame(ServerFrame{Type: "clipboard", Text: text})
log.Printf("[RDP] clipboard remote→local len=%d", len(text))
})
clip.Attach(&channelSender{conn: sess.Conn, userID: sess.UserID, writeMu: writeMu, showProtocol: true}, clipChannelID)
log.Printf("[RDP] cliprdr channel attached id=%d", clipChannelID)
go func() {
time.Sleep(1500 * time.Millisecond)
clip.Init()
}()
for _, ind := range sess.PendingChannelPDUs {
if ind.channelID == clipChannelID {
e.dispatchVirtualChannelPDU(ind, clip, clipChannelID)
}
if drive != nil && ind.channelID == driveChannelID {
e.dispatchRdpdrPDU(ind, drive, driveChannelID)
}
}
} else {
log.Printf("[RDP] cliprdr channel NOT granted by server, clipboard sync disabled (channels=%v)", sess.ChannelNameByID)
}
// 会话激活后主动发起 cliprdr 握手,兼容等待客户端先声明能力的 Windows 版本。// Logan.Li
readErr := make(chan error, 1)
go func() {
readErr <- e.readLoop(ctx, sess, receiver, sink, writeMu)
readErr <- e.readLoop(ctx, sess, receiver, sink, writeMu, clip, clipChannelID, drive, driveChannelID)
}()
go e.inputLoop(ctx, input, sink, events)
go e.inputLoop(ctx, input, sink, events, clip, drive)
for {
select {
@@ -62,7 +194,7 @@ func (e *SessionEngine) Run(ctx context.Context, cfg Config, sink FrameSink, eve
}
}
func (e *SessionEngine) inputLoop(ctx context.Context, input *InputSender, sink FrameSink, events <-chan ClientEvent) {
func (e *SessionEngine) inputLoop(ctx context.Context, input *InputSender, sink FrameSink, events <-chan ClientEvent, clip *cliprdr.Handler, drive *rdpdr.Handler) {
for {
select {
case <-ctx.Done():
@@ -74,18 +206,19 @@ func (e *SessionEngine) inputLoop(ctx context.Context, input *InputSender, sink
if event.Type != "mousemove" && event.Type != "mouse_move" {
trace.Logf("engine=rdp input type=%s x=%d y=%d button=%d code=%s down=%t", event.Type, event.X, event.Y, event.Button, event.Code, event.Down)
}
if err := e.handleEvent(input, sink, event); err != nil {
if err := e.handleEvent(input, sink, event, clip, drive); err != nil {
_ = sink.SendFrame(ServerFrame{Type: "status", Message: "输入事件失败:" + err.Error()})
}
}
}
}
func (e *SessionEngine) readLoop(ctx context.Context, sess *Session, receiver *FastPathReceiver, sink FrameSink, writeMu *sync.Mutex) error {
func (e *SessionEngine) readLoop(ctx context.Context, sess *Session, receiver *FastPathReceiver, sink FrameSink, writeMu *sync.Mutex, clip *cliprdr.Handler, clipChannelID uint16, drive *rdpdr.Handler, driveChannelID uint16) error {
framebuffer := display.NewFramebuffer(sess.Width, sess.Height)
ticker := time.NewTicker(16 * time.Millisecond)
defer ticker.Stop()
refreshRequested := false
usefulFrameSeen := false
flushFrame := func() {
frame, ok := framebuffer.FlushRGBA()
if !ok {
@@ -105,15 +238,23 @@ func (e *SessionEngine) readLoop(ctx context.Context, sess *Session, receiver *F
if rect.Width <= 0 || rect.Height <= 0 {
continue
}
if !refreshRequested && isBlackFullScreen(rect, sess.Width, sess.Height) {
refreshRequested = true
go func() {
time.Sleep(400 * time.Millisecond)
if err := e.requestRefresh(sess, writeMu); err != nil {
trace.Logf("engine=rdp refresh request failed err=%v", err)
}
}()
if isBlackFullScreen(rect, sess.Width, sess.Height) {
if !refreshRequested {
refreshRequested = true
go func() {
time.Sleep(400 * time.Millisecond)
if err := e.requestRefresh(sess, writeMu); err != nil {
trace.Logf("engine=rdp refresh request failed err=%v", err)
}
}()
}
if !usefulFrameSeen {
// 登录初期的纯黑全屏帧通常是 RDP 激活噪声,跳过可避免浏览器闪黑。 // Logan.Li
trace.Logf("engine=rdp skip startup black fullscreen")
continue
}
}
usefulFrameSeen = true
framebuffer.Draw(display.Bitmap{
X: rect.DestLeft,
Y: rect.DestTop,
@@ -161,6 +302,22 @@ func (e *SessionEngine) readLoop(ctx context.Context, sess *Session, receiver *F
if got.packet.FastPath {
rects, err = receiver.Handle(got.packet.SecFlag, got.packet.Payload)
} else {
// SlowPath:先解析 channelID 判断是图形(global 1003)还是虚拟通道(cliprdr 等) // Logan.Li
if ind, perr := parseMCSDataIndication(got.packet.Payload); perr == nil {
if clip != nil && ind.channelID == clipChannelID {
e.dispatchVirtualChannelPDU(ind, clip, clipChannelID)
continue
}
if drive != nil && ind.channelID == driveChannelID {
e.dispatchRdpdrPDU(ind, drive, driveChannelID)
continue
}
if ind.channelID != mcsGlobalChannelID {
log.Printf("[RDP] virtual channel ignored channel=%d name=%s payload=%d", ind.channelID, sess.ChannelNameByID[ind.channelID], len(ind.payload))
continue
}
logSlowPathSummary(ind, clipChannelID)
}
rects, err = receiver.HandleSlowPath(got.packet.Payload)
}
if err != nil {
@@ -193,6 +350,129 @@ func isBlackFullScreen(rect BitmapRect, width, height int) bool {
return limit > 0
}
func (e *SessionEngine) dispatchRdpdrPDU(ind mcsDataIndication, drive *rdpdr.Handler, driveChannelID uint16) {
if drive == nil || ind.channelID != driveChannelID {
return
}
prefixLen := len(ind.payload)
if prefixLen > 12 {
prefixLen = 12
}
log.Printf("[RDP] rdpdr payload prefix=%x len=%d", ind.payload[:prefixLen], len(ind.payload))
if len(ind.payload) >= 4 && binary.LittleEndian.Uint16(ind.payload[0:2]) == 0x4472 {
log.Printf("[RDP] rdpdr recv channel=%d rawLen=%d raw=true", ind.channelID, len(ind.payload))
drive.Process(ind.payload)
return
}
if len(ind.payload) >= 8 && binary.LittleEndian.Uint32(ind.payload[0:4]) <= uint32(len(ind.payload)) && binary.LittleEndian.Uint16(ind.payload[4:6]) == 0x4472 {
log.Printf("[RDP] rdpdr recv channel=%d rawLen=%d lengthOnlyHeader=true", ind.channelID, len(ind.payload))
drive.Process(ind.payload[4:])
return
}
if len(ind.payload) >= 8 && binary.LittleEndian.Uint16(ind.payload[4:6]) == 0x4472 {
log.Printf("[RDP] rdpdr recv channel=%d rawLen=%d securityHeader=true", ind.channelID, len(ind.payload))
drive.Process(ind.payload[4:])
return
}
if p, flag, ok := parseVirtualChannelPDU(ind.payload); ok {
log.Printf("[RDP] rdpdr recv channel=%d rawLen=%d flag=0x%x", ind.channelID, len(ind.payload), flag)
if flag&channelFlagFirst != 0 && flag&channelFlagLast != 0 {
drive.Process(p)
} else {
log.Printf("[RDP] rdpdr fragmented chunk flag=0x%x len=%d", flag, len(p))
}
}
}
func (e *SessionEngine) dispatchVirtualChannelPDU(ind mcsDataIndication, clip *cliprdr.Handler, clipChannelID uint16) {
if clip == nil || ind.channelID != clipChannelID {
return
}
prefixLen := len(ind.payload)
if prefixLen > 16 {
prefixLen = 16
}
log.Printf("[RDP] cliprdr payload prefix=%x len=%d", ind.payload[:prefixLen], len(ind.payload))
if p, flag, ok := parseVirtualChannelPDU(ind.payload); ok {
log.Printf("[RDP] cliprdr recv channel=%d rawLen=%d flag=0x%x", ind.channelID, len(ind.payload), flag)
if flag&channelFlagFirst != 0 && flag&channelFlagLast != 0 {
clip.Process(p)
} else {
log.Printf("[RDP] cliprdr fragmented chunk flag=0x%x len=%d", flag, len(p))
}
} else {
log.Printf("[RDP] cliprdr parse failed channel=%d rawLen=%d", ind.channelID, len(ind.payload))
}
}
func parseVirtualChannelPDU(payload []byte) ([]byte, uint32, bool) {
if len(payload) < 8 {
return nil, 0, false
}
offset := 0
if hasVirtualChannelSecurityHeader(payload) {
offset = 4
}
totalLen := int(binary.LittleEndian.Uint32(payload[offset:]))
flag := binary.LittleEndian.Uint32(payload[offset+4:])
dataStart := offset + 8
if totalLen < 0 || dataStart+totalLen > len(payload) {
return nil, 0, false
}
return payload[dataStart : dataStart+totalLen], flag, true
}
func hasVirtualChannelSecurityHeader(payload []byte) bool {
if len(payload) < 12 || binary.LittleEndian.Uint16(payload[0:2]) != secFlagChannelPkt {
return false
}
totalLen := int(binary.LittleEndian.Uint32(payload[4:8]))
flag := binary.LittleEndian.Uint32(payload[8:12])
if totalLen < 0 || 12+totalLen > len(payload) {
return false
}
// 只有后移 4 字节后能看到合法 Channel PDU flags,才认为存在 security header。// Logan.Li
return flag&(channelFlagFirst|channelFlagLast) != 0
}
func logSlowPathSummary(ind mcsDataIndication, clipChannelID uint16) {
prefixLen := len(ind.payload)
if prefixLen > 24 {
prefixLen = 24
}
prefix := fmt.Sprintf("%x", ind.payload[:prefixLen])
type1 := uint16(0)
type2 := byte(0)
if len(ind.payload) >= 15 {
type1 = binary.LittleEndian.Uint16(ind.payload[6:8])
type2 = ind.payload[14]
}
log.Printf("[RDP] slowpath packet channel=%d payload=%d sec=0x%04x type1=0x%04x type2=0x%02x prefix=%s (clipID=%d)", ind.channelID, len(ind.payload), ind.securityFlags, type1, type2, prefix, clipChannelID)
}
// isChannelData 判断一个 SlowPath MCS 包是否发往指定虚拟通道 ID。// Logan.Li
func isChannelData(payload []byte, channelID uint16) bool {
ind, err := parseMCSDataIndication(payload)
if err != nil {
return false
}
return ind.channelID == channelID
}
// extractChannelPayload 从 SlowPath MCS 包中提取虚拟通道负载(已去掉 MCS/SEC 头)。// Logan.Li
func extractChannelPayload(payload []byte, channelID uint16) []byte {
ind, err := parseMCSDataIndication(payload)
if err != nil || ind.channelID != channelID {
return nil
}
// 虚拟通道数据:payload 前 4 字节是 securityHeader(encrypt/decrypt 标志),
// 之后的 raw data 即 CLIPRDR PDU。NLA 模式下通道不额外加密,直接取 data 段。 // Logan.Li
if len(ind.payload) < 4 {
return nil
}
return ind.payload[4:]
}
func (e *SessionEngine) requestRefresh(sess *Session, writeMu *sync.Mutex) error {
body := buildRefreshRectBody(Config{Width: sess.Width, Height: sess.Height})
pdu := buildDataPDU(sess.UserID, sess.SharedID, pduType2RefreshRect, body)
@@ -218,7 +498,7 @@ func (e *SessionEngine) ackSurfaceFrames(sess *Session, frameIDs []uint32, write
return nil
}
func (e *SessionEngine) handleEvent(input *InputSender, sink FrameSink, event ClientEvent) error {
func (e *SessionEngine) handleEvent(input *InputSender, sink FrameSink, event ClientEvent, clip *cliprdr.Handler, drive *rdpdr.Handler) error {
switch event.Type {
case "mousemove", "mouse_move":
return input.MouseMove(event.X, event.Y)
@@ -229,16 +509,71 @@ func (e *SessionEngine) handleEvent(input *InputSender, sink FrameSink, event Cl
case "wheel":
return input.MouseWheel(event.X, event.Y, event.DeltaY)
case "key":
if event.Meta {
event.Ctrl = true
}
if event.Down && (event.Ctrl || event.Meta) && (event.Code == "KeyC" || event.Code == "KeyX") && clip != nil {
go func() {
time.Sleep(250 * time.Millisecond)
clip.RequestRemoteText()
}()
}
if event.Code == "MetaLeft" || event.Code == "MetaRight" {
event.Code = "ControlLeft"
event.Key = "Control"
}
// 可打印字符按 Unicode 下发,避免远端键盘布局/修饰键状态导致审计有记录但远端无输入。 // Logan.Li
if event.Down && len([]rune(event.Key)) == 1 && !event.Ctrl && !event.Alt && !event.Meta {
return input.TypeText(event.Key)
}
if !event.Down && len([]rune(event.Key)) == 1 && !event.Ctrl && !event.Alt && !event.Meta {
return nil
}
sc, ok := ScanCodeFromJS(event.Code)
if !ok {
return nil
}
return input.Key(sc, event.Down)
case "clipboard":
if event.Text != "" {
return input.TypeText(event.Text)
case "remote_copy":
if err := sendCopyShortcut(input); err != nil {
return err
}
return nil
if clip != nil {
go func() {
time.Sleep(250 * time.Millisecond)
clip.RequestRemoteText()
}()
}
go func() {
time.Sleep(900 * time.Millisecond)
log.Printf("[RDP] clipboard explicit fallback auto_exec start")
if err := input.AutoExec(buildClipboardFallbackCommand()); err != nil {
log.Printf("[RDP] clipboard explicit fallback auto_exec failed: %v", err)
return
}
log.Printf("[RDP] clipboard explicit fallback auto_exec sent")
}()
return sink.SendFrame(ServerFrame{Type: "status", Message: "已发送 Ctrl+C 到远端,等待剪贴板回传"})
case "clipboard":
// 本地→远端:优先走 cliprdr 协议级同步(通知远端本地剪贴板变更),
// 远端粘贴时主动拉取;cliprdr 不可用时回退到 Unicode 键盘模拟。// Logan.Li
if event.Text == "" {
return nil
}
if clip != nil {
clip.SetLocalText(event.Text)
}
return input.TypeText(event.Text)
case "rdp_file_upload":
if drive == nil || event.Name == "" || event.Text == "" {
return nil
}
data, err := base64.StdEncoding.DecodeString(event.Text)
if err != nil {
return err
}
drive.AddUpload(event.Name, data)
return sink.SendFrame(ServerFrame{Type: "status", Message: "已放入 AIOps\\Upload:" + event.Name})
case "auto_exec":
if event.Text == "" {
return nil
@@ -253,6 +588,44 @@ func (e *SessionEngine) handleEvent(input *InputSender, sink FrameSink, event Cl
}
}
func sendCopyShortcut(input *InputSender) error {
// 工具按钮专用复制快捷键,避免复用前端 key 序列导致修饰键状态漂移。// Logan.Li
if err := input.Key(0x001D, true); err != nil {
return err
}
if err := input.Key(0x002E, true); err != nil {
return err
}
if err := input.Key(0x002E, false); err != nil {
return err
}
return input.Key(0x001D, false)
}
func buildClipboardFallbackCommand() string {
ps := `$ErrorActionPreference='SilentlyContinue';$v='';try{$v=Get-Clipboard -Raw -ErrorAction Stop}catch{};if([string]::IsNullOrEmpty($v)){try{Add-Type -AssemblyName System.Windows.Forms;$v=[Windows.Forms.Clipboard]::GetText()}catch{}};if(-not [string]::IsNullOrEmpty($v)){$utf8=New-Object System.Text.UTF8Encoding -ArgumentList $false;$paths=@('\\tsclient\AIOps\Download\aiops-clipboard.txt','\\Go-client\AIOps\Download\aiops-clipboard.txt');foreach($p in $paths){try{[System.IO.File]::WriteAllText($p,$v,$utf8);break}catch{}}}`
encoded := encodePowerShellCommand(ps)
return "powershell -NoProfile -STA -WindowStyle Hidden -EncodedCommand " + encoded
}
func encodePowerShellCommand(command string) string {
u16 := utf16.Encode([]rune(command))
raw := make([]byte, len(u16)*2)
for i, v := range u16 {
binary.LittleEndian.PutUint16(raw[i*2:], v)
}
return base64.StdEncoding.EncodeToString(raw)
}
func decodeRDPClipboardFallback(file rdpdr.Download) (string, bool) {
if file.Name != "aiops-clipboard.txt" || len(file.Data) == 0 {
return "", false
}
text := string(file.Data)
text = strings.TrimPrefix(text, "\ufeff")
return text, text != ""
}
// ProbeSessionFrames 用于诊断:建立会话后读取若干秒位图帧数量 // Logan.Li
func ProbeSessionFrames(ctx context.Context, cfg Config, seconds int) (int, error) {
sess, err := OpenSession(ctx, cfg)