Files
aiops/backend/pkg/rdp/channels/cliprdr/cliprdr.go
T
Logan.Li e0d652d211 feat: 完善堡垒机与 Agent 智能运维能力
- 支持 Windows Agent 构建、安装脚本和统一上报协议

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

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

- 重构 docs 模块文档并补充终端、堡垒机、监控、AI Agent 设计
2026-07-05 23:49:36 +08:00

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
}