feat: 完善堡垒机与 Agent 智能运维能力
- 支持 Windows Agent 构建、安装脚本和统一上报协议 - 增强 RDP 剪贴板、虚拟盘和文件传输能力 - 新增 AIOps 上下文工具、告警 RCA trace 落库和回放接口 - 重构 docs 模块文档并补充终端、堡垒机、监控、AI Agent 设计
This commit is contained in:
1 parent
a9513c2056
commit
e0d652d211
169 files changed
+12192
-9116
No files matched your search
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
Reference in new issue
Block a user