- 支持 Windows Agent 构建、安装脚本和统一上报协议 - 增强 RDP 剪贴板、虚拟盘和文件传输能力 - 新增 AIOps 上下文工具、告警 RCA trace 落库和回放接口 - 重构 docs 模块文档并补充终端、堡垒机、监控、AI Agent 设计
423 lines
13 KiB
Go
423 lines
13 KiB
Go
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
|
|
}
|