feat(rdp): 新增 RDP 远程桌面/SMB/账单同步能力 + 清理编译产物

主要变更:
- RDP 远程桌面:新增 backend/pkg/rdp(RDP 引擎 + credssp/display/rle/trace)、
  backend/controller/rdp*(连接/凭证/文件/会话/实时)、frontend RDP 组件与 API
- Windows SMB 文件传输:backend/pkg/winsmb
- 云账单同步:backend/pkg/billingsync + billing_sync/aliyun_billing,前端账单详情
- Agent CLI Kubernetes 工具:kubernetes_action/get、pod_execute/logs
- 服务器导入助手:server_import_helpers、ImportCredentialSettings
- 国际化与组件重构(cloud/deploy/monitor/server/system 等多模块)
- 清理编译产物二进制:移除 backend/server、backend/agent/bin/aiops-agent-*
- .gitignore:新增 backend/agent/ 与 .cache/(避免 334MB go-build 缓存误入库)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
LoganandClaude committed 2026-07-04 01:50:36 +08:00
1 parent 777d9e4e71
commit 76ec9387f0
202 files changed
+16555 -2543

No files matched your search

+37
View File
@@ -0,0 +1,37 @@
# RDP 协议栈分层
`backend/pkg/rdp` 是 AIOps 资产远程桌面的协议层。当前固定使用自研 RDP 引擎,第三方 `grdp` 兜底和 `backend/third_party` 已移除。根包只保留兼容入口、引擎创建与临时会话编排,协议细节逐步迁入域目录。// Logan.Li
## 引擎选择
| 环境变量 | 行为 |
|----------|------|
| `RDP_ENGINE` | 保留兼容,不再参与引擎选择 |
| `RDP_DEBUG=1` | 打开协议级 trace 日志,统一输出 `[RDP:TRACE]` |
## 目录职责
| 目录 | 职责 |
|------|------|
| `domain/` | 对外契约:连接配置、浏览器输入事件、服务端帧、引擎接口、诊断结果、录屏摘要 |
| `transport/` | TCP 上的 TPKT、X.224、TLS 升级与安全协议协商 |
| `credssp/` | NLA/CredSSP/NTLMv2 认证,自研 ASN.1 与 NTLM 消息处理 |
| `rle/` | RDP 位图 RLE 解压算法 |
| `trace/` | 协议级调试日志开关,避免高频 bitmap 默认刷屏 |
| 根包 | 引擎选择、兼容入口、临时会话编排、MCS/GCC、Fast-Path 收发、键鼠输入、位图解码、探针入口 |
## 依赖方向
控制器只依赖根包 `ezops/pkg/rdp` 的 `Engine`、`Config`、`ClientEvent`、`ServerFrame`。根包向内组合 `domain`、`transport`、`credssp`、`rle`。底层域目录不能反向依赖控制器、数据库、WebSocket 或审计模型。
## 后续拆分目标
1. 协议基础帧、TLS、X.224 只放 `transport/`。
2. 登录认证只放 `credssp/`,不得调用 Gin/GORM。
3. 浏览器与控制器共享的 DTO 先放 `domain/`,根包用类型别名兼容旧调用。
4. `pdu/` 承接 DemandActive、ConfirmActive、Synchronize、Control、FontList、BitmapUpdate 等状态机。
5. `graphics/` 承接 bitmap/RLE/PNG 或后续 raw frame 编码,输入只消费标准化矩形。
6. `input/` 承接鼠标、键盘、Unicode、组合键与自动执行。
7. `channels/` 承接 cliprdr、drdynvc、rdpgfx,避免虚拟通道逻辑污染会话主流程。
8. `recorder/` 承接 RDP 事件录制、回放和截图探针。
9. 新增测试优先覆盖底层域目录,再用根包探针做资产联调。
+297
View File
@@ -0,0 +1,297 @@
package rdp
import (
"bytes"
"encoding/base64"
"encoding/binary"
"image"
"image/png"
"io"
"ezops/pkg/rdp/rle"
"ezops/pkg/rdp/trace"
)
const (
bitmapCompression = 0x0001
noBitmapCompressionHdr = 0x0400
bitmapNoProcessing = 0x8000
fastPathUpdateBitmap = 0x1
fastPathUpdateSurfaceCmds = 0x4
pduType2Update = 0x02
)
// BitmapRect 表示一块待绘制到 Canvas 的位图区域 // Logan.Li
type BitmapRect struct {
DestLeft int
DestTop int
Width int
Height int
BitsPerPixel int
Data []byte
}
func bppFromWire(v uint16) int {
switch v {
case 8:
return 1
case 15, 16:
return 2
case 24:
return 3
case 32:
return 4
default:
if v <= 4 {
return int(v)
}
return 4
}
}
type wireBitmap struct {
DestLeft uint16
DestTop uint16
DestRight uint16
DestBottom uint16
Width uint16
Height uint16
BitsPerPixel uint16
Flags uint16
BitmapLength uint16
BitmapDataStream []byte
}
func readWireBitmap(r *bytes.Reader) (wireBitmap, error) {
var rect wireBitmap
var err error
rect.DestLeft, err = readLE16(r)
if err != nil {
return rect, err
}
rect.DestTop, err = readLE16(r)
if err != nil {
return rect, err
}
rect.DestRight, err = readLE16(r)
if err != nil {
return rect, err
}
rect.DestBottom, err = readLE16(r)
if err != nil {
return rect, err
}
rect.Width, err = readLE16(r)
if err != nil {
return rect, err
}
rect.Height, err = readLE16(r)
if err != nil {
return rect, err
}
rect.BitsPerPixel, err = readLE16(r)
if err != nil {
return rect, err
}
rect.Flags, err = readLE16(r)
if err != nil {
return rect, err
}
rect.BitmapLength, err = readLE16(r)
if err != nil {
return rect, err
}
ln := int(rect.BitmapLength)
if rect.Flags&bitmapCompression != 0 && rect.Flags&noBitmapCompressionHdr == 0 {
if _, err = readLE16(r); err != nil {
return rect, err
}
mainBody, err := readLE16(r)
if err != nil {
return rect, err
}
if _, err = readLE16(r); err != nil {
return rect, err
}
if _, err = readLE16(r); err != nil {
return rect, err
}
ln = int(mainBody)
}
rect.BitmapDataStream = make([]byte, ln)
_, err = io.ReadFull(r, rect.BitmapDataStream)
return rect, err
}
func parseFastPathBitmap(payload []byte) ([]BitmapRect, error) {
r := bytes.NewReader(payload)
if _, err := readLE16(r); err != nil {
return nil, err
}
count, err := readLE16(r)
if err != nil {
return nil, err
}
out := make([]BitmapRect, 0, count)
for i := 0; i < int(count); i++ {
wb, err := readWireBitmap(r)
if err != nil {
return out, err
}
if rect, ok := decodeWireBitmap(wb); ok {
out = append(out, rect)
}
}
return out, nil
}
func parseSlowPathBitmap(payload []byte) ([]BitmapRect, error) {
if len(payload) < 15 {
return nil, nil
}
pduType := binaryLittleUint16(payload[2:4])
if pduType != pduTypeData {
return nil, nil
}
type2 := payload[14]
if type2 != pduType2Update {
return nil, nil
}
body := payload[18:]
if len(body) < 2 {
return nil, nil
}
updateType := binaryLittleUint16(body[0:2])
if updateType != fastPathUpdateBitmap {
return nil, nil
}
return parseFastPathBitmap(body[2:])
}
func decodeWireBitmap(wb wireBitmap) (rect BitmapRect, ok bool) {
defer func() {
if recover() != nil {
rect = BitmapRect{}
ok = false
}
}()
bpp := bppFromWire(wb.BitsPerPixel)
data := wb.BitmapDataStream
originalLen := len(data)
if wb.Flags&bitmapNoProcessing != 0 {
// Surface command 已是自上而下 BGRA // Logan.Li
} else if wb.Flags&bitmapCompression != 0 {
data = rle.DecompressInto(data, nil, int(wb.Width), int(wb.Height), bpp)
if len(data) == 0 {
trace.Logf("bitmap decode failed compressed rect=%dx%d@%d,%d wireBpp=%d bpp=%d flags=0x%04x original=%d", wb.Width, wb.Height, wb.DestLeft, wb.DestTop, wb.BitsPerPixel, bpp, wb.Flags, originalLen)
return BitmapRect{}, false
}
} else {
data = flipBitmapVertical(data, int(wb.Width), bpp, int(wb.Height))
}
if len(data) < int(wb.Width)*int(wb.Height)*bpp {
trace.Logf("bitmap decode short rect=%dx%d@%d,%d wireBpp=%d bpp=%d flags=0x%04x original=%d decoded=%d", wb.Width, wb.Height, wb.DestLeft, wb.DestTop, wb.BitsPerPixel, bpp, wb.Flags, originalLen, len(data))
return BitmapRect{}, false
}
if isMostlyBlack(data, bpp) && int(wb.Width) >= 128 && int(wb.Height) >= 128 {
trace.Logf("bitmap decode mostly black rect=%dx%d@%d,%d wireBpp=%d bpp=%d flags=0x%04x original=%d decoded=%d", wb.Width, wb.Height, wb.DestLeft, wb.DestTop, wb.BitsPerPixel, bpp, wb.Flags, originalLen, len(data))
}
return BitmapRect{
DestLeft: int(wb.DestLeft),
DestTop: int(wb.DestTop),
Width: int(wb.Width),
Height: int(wb.Height),
BitsPerPixel: bpp,
Data: data,
}, true
}
func isMostlyBlack(data []byte, bpp int) bool {
if len(data) == 0 || bpp <= 0 {
return false
}
limit := len(data)
if limit > 256*1024 {
limit = 256 * 1024
}
nonBlack := 0
for i := 0; i+bpp-1 < limit; i += bpp {
switch bpp {
case 1:
if data[i] > 4 {
nonBlack++
}
case 2:
if data[i] > 0 || data[i+1] > 0 {
nonBlack++
}
default:
if data[i] > 4 || data[i+1] > 4 || data[i+2] > 4 {
nonBlack++
}
}
if nonBlack > 32 {
return false
}
}
return true
}
func flipBitmapVertical(data []byte, width, bpp, height int) []byte {
out := append([]byte(nil), data...)
stride := width * bpp
tmp := make([]byte, stride)
for y := 0; y < height/2; y++ {
top := y * stride
bot := (height - 1 - y) * stride
copy(tmp, out[top:top+stride])
copy(out[top:top+stride], out[bot:bot+stride])
copy(out[bot:bot+stride], tmp)
}
return out
}
func bitmapRectPNG(rect BitmapRect) (string, error) {
img := image.NewRGBA(image.Rect(0, 0, rect.Width, rect.Height))
pix := img.Pix
data := rect.Data
switch rect.BitsPerPixel {
case 1, 2:
n := rect.Width * rect.Height
for i := 0; i < n && i*2+1 < len(data); i++ {
// 保持 PNG 兼容路径与 native display 的 16bpp 大端 RGB565 解析一致。 // Logan.Li
c := binary.BigEndian.Uint16(data[i*2 : i*2+2])
r := uint8((c >> 11) & 0x1F)
g := uint8((c >> 5) & 0x3F)
b := uint8(c & 0x1F)
off := i * 4
pix[off] = (r << 3) | (r >> 2)
pix[off+1] = (g << 2) | (g >> 4)
pix[off+2] = (b << 3) | (b >> 2)
pix[off+3] = 0xFF
}
default:
stride := rect.BitsPerPixel
n := rect.Width * rect.Height
for i := 0; i < n; i++ {
s := i * stride
if s+2 >= len(data) {
break
}
off := i * 4
pix[off+2] = data[s]
pix[off+1] = data[s+1]
pix[off] = data[s+2]
pix[off+3] = 0xFF
}
}
var buf bytes.Buffer
if err := png.Encode(&buf, img); err != nil {
return "", err
}
return "data:image/png;base64," + base64.StdEncoding.EncodeToString(buf.Bytes()), nil
}
func binaryLittleUint16(b []byte) uint16 {
return uint16(b[0]) | uint16(b[1])<<8
}
+187
View File
@@ -0,0 +1,187 @@
package rdp
import (
"context"
"crypto/tls"
"fmt"
"io"
"net"
"time"
"ezops/pkg/rdp/credssp"
"ezops/pkg/rdp/transport"
)
// ProbeHandshake 执行 TCP + TPKT/X.224 + RDP 安全协议协商 // Logan.Li
func ProbeHandshake(ctx context.Context, cfg Config) (HandshakeInfo, error) {
port := cfg.Port
if port == 0 {
port = 3389
}
address := fmt.Sprintf("%s:%d", cfg.Host, port)
start := time.Now()
var d net.Dialer
conn, err := d.DialContext(ctx, "tcp", address)
if err != nil {
return HandshakeInfo{Address: address, Duration: time.Since(start)}, err
}
defer conn.Close()
_ = conn.SetDeadline(time.Now().Add(15 * time.Second))
if err := transport.WriteTPKT(conn, transport.BuildConnectionRequest()); err != nil {
return HandshakeInfo{Address: address, Duration: time.Since(start)}, err
}
payload, err := transport.ReadTPKT(conn)
if err != nil {
return HandshakeInfo{Address: address, Duration: time.Since(start)}, err
}
result, err := transport.ParseConnectionConfirm(payload)
if err != nil {
return HandshakeInfo{Address: address, Duration: time.Since(start)}, err
}
return HandshakeInfo{
Address: address,
SelectedProtocol: result.ProtocolName(),
Duration: time.Since(start),
}, nil
}
// ProbeTLSHandshake 执行 X.224 协商后升级 TLS,用于验证 Windows 10/Server NLA 前置链路 // Logan.Li
func ProbeTLSHandshake(ctx context.Context, cfg Config) (HandshakeInfo, error) {
port := cfg.Port
if port == 0 {
port = 3389
}
address := fmt.Sprintf("%s:%d", cfg.Host, port)
start := time.Now()
var d net.Dialer
conn, err := d.DialContext(ctx, "tcp", address)
if err != nil {
return HandshakeInfo{Address: address, Duration: time.Since(start)}, err
}
defer conn.Close()
_ = conn.SetDeadline(time.Now().Add(15 * time.Second))
if err := transport.WriteTPKT(conn, transport.BuildConnectionRequestWithCookie(cfg.Username)); err != nil {
return HandshakeInfo{Address: address, Duration: time.Since(start)}, err
}
payload, err := transport.ReadTPKT(conn)
if err != nil {
return HandshakeInfo{Address: address, Duration: time.Since(start)}, err
}
result, err := transport.ParseConnectionConfirm(payload)
if err != nil {
return HandshakeInfo{Address: address, Duration: time.Since(start)}, err
}
if result.SelectedProtocol != transport.ProtocolTLS && result.SelectedProtocol != transport.ProtocolCredSSP {
return HandshakeInfo{Address: address, SelectedProtocol: result.ProtocolName(), Duration: time.Since(start)}, nil
}
tlsConn, err := transport.UpgradeTLS(conn, cfg.Host, 15*time.Second)
if err != nil {
return HandshakeInfo{Address: address, SelectedProtocol: result.ProtocolName(), Duration: time.Since(start)}, err
}
state := tlsConn.ConnectionState()
certSubject := ""
if len(state.PeerCertificates) > 0 {
certSubject = state.PeerCertificates[0].Subject.String()
}
_ = tlsConn.SetReadDeadline(time.Now().Add(300 * time.Millisecond))
_, _ = io.ReadAll(tlsConn)
return HandshakeInfo{
Address: address,
SelectedProtocol: result.ProtocolName(),
Duration: time.Since(start),
TLSCipher: transport.TLSCipherSuiteName(state.CipherSuite),
CertSubject: certSubject,
}, nil
}
// ProbeCredSSP 执行到 CredSSP/NTLMv2 登录完成,用于验证 Windows 10/Server NLA 凭据 // Logan.Li
func ProbeCredSSP(ctx context.Context, cfg Config) (HandshakeInfo, error) {
port := cfg.Port
if port == 0 {
port = 3389
}
address := fmt.Sprintf("%s:%d", cfg.Host, port)
start := time.Now()
strategies := []credssp.Options{
{Version: 6, PubKeyStyle: "binding"},
{Version: 6, PubKeyStyle: "binding", UseCBT: true},
{Version: 6, PubKeyStyle: "binding", ForceMIC: true},
{Version: 2, PubKeyStyle: "raw"},
}
var lastErr error
for _, opts := range strategies {
info, err := probeCredSSPOnce(ctx, cfg, address, opts)
if err == nil {
info.Duration = time.Since(start)
return info, nil
}
lastErr = err
}
if lastErr == nil {
lastErr = fmt.Errorf("CredSSP 认证失败")
}
return HandshakeInfo{Address: address, Duration: time.Since(start)}, lastErr
}
func probeCredSSPOnce(ctx context.Context, cfg Config, address string, opts credssp.Options) (HandshakeInfo, error) {
tlsConn, result, err := dialCredSSPOnce(ctx, cfg, address, opts)
if err != nil {
return HandshakeInfo{Address: address}, err
}
defer tlsConn.Close()
state := tlsConn.ConnectionState()
certSubject := ""
if len(state.PeerCertificates) > 0 {
certSubject = state.PeerCertificates[0].Subject.String()
}
return HandshakeInfo{
Address: address,
SelectedProtocol: result.ProtocolName(),
TLSCipher: transport.TLSCipherSuiteName(state.CipherSuite),
CertSubject: certSubject,
}, nil
}
func dialCredSSPOnce(ctx context.Context, cfg Config, address string, opts credssp.Options) (*tls.Conn, transport.NegotiationResult, error) {
var d net.Dialer
conn, err := d.DialContext(ctx, "tcp", address)
if err != nil {
return nil, transport.NegotiationResult{}, err
}
_ = conn.SetDeadline(time.Now().Add(30 * time.Second))
if err := transport.WriteTPKT(conn, transport.BuildConnectionRequestWithCookie(cfg.Username)); err != nil {
_ = conn.Close()
return nil, transport.NegotiationResult{}, err
}
payload, err := transport.ReadTPKT(conn)
if err != nil {
_ = conn.Close()
return nil, transport.NegotiationResult{}, err
}
result, err := transport.ParseConnectionConfirm(payload)
if err != nil {
_ = conn.Close()
return nil, transport.NegotiationResult{}, err
}
if result.SelectedProtocol != transport.ProtocolCredSSP {
_ = conn.Close()
return nil, result, fmt.Errorf("目标未选择 CredSSP/NLA: %s", result.ProtocolName())
}
tlsConn, err := transport.UpgradeTLS(conn, cfg.Host, 30*time.Second)
if err != nil {
_ = conn.Close()
return nil, result, err
}
if err := credssp.AuthenticateWithOptions(tlsConn, cfg.Domain, cfg.Username, cfg.Password, opts); err != nil {
_ = tlsConn.Close()
return nil, result, err
}
_ = tlsConn.SetDeadline(time.Time{})
return tlsConn, result, nil
}
+74
View File
@@ -0,0 +1,74 @@
package credssp
import "encoding/asn1"
const defaultCredSSPVersion = 6
// negoToken 是 CredSSP TSRequest 中的单个协商令牌 // Logan.Li
type negoToken struct {
Data []byte `asn1:"explicit,tag:0"`
}
// tsRequest 是 MS-CSSP 2.2.1.2 TSRequest 的最小实现 // Logan.Li
type tsRequest struct {
Version int `asn1:"explicit,tag:0"`
NegoTokens []negoToken `asn1:"optional,explicit,tag:1"`
AuthInfo []byte `asn1:"optional,explicit,tag:2"`
PubKeyAuth []byte `asn1:"optional,explicit,tag:3"`
ErrorCode int `asn1:"optional,explicit,tag:4"`
ClientNonce []byte `asn1:"optional,explicit,tag:5"`
}
type tsCredentials struct {
CredType int `asn1:"explicit,tag:0"`
Credentials []byte `asn1:"explicit,tag:1"`
}
type tsPasswordCreds struct {
DomainName []byte `asn1:"explicit,tag:0"`
UserName []byte `asn1:"explicit,tag:1"`
Password []byte `asn1:"explicit,tag:2"`
}
func encodeTSRequest(tokens [][]byte, authInfo, pubKeyAuth []byte) ([]byte, error) {
return encodeTSRequestWithNonce(tokens, authInfo, pubKeyAuth, nil)
}
func encodeTSRequestWithNonce(tokens [][]byte, authInfo, pubKeyAuth, clientNonce []byte) ([]byte, error) {
return encodeTSRequestVersion(defaultCredSSPVersion, tokens, authInfo, pubKeyAuth, clientNonce)
}
func encodeTSRequestVersion(version int, tokens [][]byte, authInfo, pubKeyAuth, clientNonce []byte) ([]byte, error) {
req := tsRequest{Version: version}
for _, token := range tokens {
req.NegoTokens = append(req.NegoTokens, negoToken{Data: token})
}
if len(authInfo) > 0 {
req.AuthInfo = authInfo
}
if len(pubKeyAuth) > 0 {
req.PubKeyAuth = pubKeyAuth
}
if len(clientNonce) > 0 {
req.ClientNonce = clientNonce
}
return asn1.Marshal(req)
}
func decodeTSRequest(data []byte) (*tsRequest, error) {
var req tsRequest
_, err := asn1.Unmarshal(data, &req)
return &req, err
}
func encodePasswordCredentials(domain, user, password string) ([]byte, error) {
pass, err := asn1.Marshal(tsPasswordCreds{
DomainName: utf16le(domain),
UserName: utf16le(user),
Password: utf16le(password),
})
if err != nil {
return nil, err
}
return asn1.Marshal(tsCredentials{CredType: 1, Credentials: pass})
}
+332
View File
@@ -0,0 +1,332 @@
package credssp
import (
"crypto/md5"
"crypto/rsa"
"crypto/sha1"
"crypto/sha256"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"encoding/asn1"
"fmt"
"io"
"math/big"
"strings"
)
// Options 控制 CredSSP 兼容策略,仅调试和特殊 Windows 版本需要覆盖 // Logan.Li
type Options struct {
Version int
PubKeyStyle string
ForceMIC bool
NoKeyExch bool
UseCBT bool
OmitVersion bool
}
// ChallengeInfo 暴露 NTLM challenge 诊断信息,不包含凭据 // Logan.Li
type ChallengeInfo struct {
Version int
TargetName string
Flags uint32
TargetInfoBytes int
RequiresMIC bool
DomainName string
ComputerName string
}
// ReadChallengeInfo 只读取 CredSSP/NTLM challenge,不发送认证数据 // Logan.Li
func ReadChallengeInfo(conn *tls.Conn, version int) (ChallengeInfo, error) {
if version == 0 {
version = defaultCredSSPVersion
}
negotiate := buildNegotiate()
req, err := encodeTSRequestVersion(version, [][]byte{negotiate}, nil, nil, nil)
if err != nil {
return ChallengeInfo{}, err
}
if _, err := conn.Write(req); err != nil {
return ChallengeInfo{}, err
}
challengeReq, err := readTSRequest(conn)
if err != nil {
return ChallengeInfo{}, err
}
if len(challengeReq.NegoTokens) == 0 {
return ChallengeInfo{}, fmt.Errorf("CredSSP challenge 缺少 nego token")
}
challenge, err := parseChallenge(challengeReq.NegoTokens[0].Data)
if err != nil {
return ChallengeInfo{}, err
}
return ChallengeInfo{
Version: challengeReq.Version,
TargetName: challenge.TargetName,
Flags: challenge.Flags,
TargetInfoBytes: len(challenge.TargetInfo),
RequiresMIC: requiresMIC(challenge.TargetInfo),
DomainName: utf16leString(findAVPair(challenge.TargetInfo, msvAvNbDomainName)),
ComputerName: utf16leString(findAVPair(challenge.TargetInfo, msvAvNbComputerName)),
}, nil
}
// Authenticate 在已升级 TLS 的 RDP 连接上执行最小 CredSSP/NTLMv2 认证 // Logan.Li
func Authenticate(conn *tls.Conn, domain, user, password string) error {
return AuthenticateWithOptions(conn, domain, user, password, Options{Version: defaultCredSSPVersion, PubKeyStyle: "binding"})
}
// AuthenticateWithOptions 使用指定 CredSSP 版本和 pubKeyAuth 算法执行认证 // Logan.Li
func AuthenticateWithOptions(conn *tls.Conn, domain, user, password string, opts Options) error {
if opts.Version == 0 {
opts.Version = defaultCredSSPVersion
}
if opts.PubKeyStyle == "" {
opts.PubKeyStyle = "binding"
}
domain, user = splitDomainUser(domain, user)
negotiate := buildNegotiate()
req, err := encodeTSRequestVersion(opts.Version, [][]byte{negotiate}, nil, nil, nil)
if err != nil {
return err
}
if _, err := conn.Write(req); err != nil {
return err
}
challengeReq, err := readTSRequest(conn)
if err != nil {
return fmt.Errorf("读取 NTLM challenge 失败: %w", err)
}
if len(challengeReq.NegoTokens) == 0 {
return fmt.Errorf("CredSSP challenge 缺少 nego token")
}
challenge, err := parseChallenge(challengeReq.NegoTokens[0].Data)
if err != nil {
return err
}
ntlmOpts := ntlmOptions{
ForceMIC: opts.ForceMIC,
DisableKeyExch: opts.NoKeyExch,
OmitVersion: opts.OmitVersion,
}
if opts.UseCBT {
ntlmOpts.ChannelBindings = channelBindingsHash(conn)
}
auth, sec, err := buildAuthenticate(challenge, domain, user, password, "AIOPS", negotiate, ntlmOpts)
if err != nil {
return err
}
pubKey, err := peerPublicKey(conn, opts.PubKeyStyle)
if err != nil {
return err
}
clientNonce := randomBytes(32)
pubKeyAuthPlain := buildPubKeyAuth(pubKey, clientNonce, opts.PubKeyStyle)
pubKeyAuth := sec.encrypt(pubKeyAuthPlain)
var nonce []byte
if opts.PubKeyStyle == "binding" {
nonce = clientNonce
}
req, err = encodeTSRequestVersion(opts.Version, [][]byte{auth}, nil, pubKeyAuth, nonce)
if err != nil {
return err
}
if _, err := conn.Write(req); err != nil {
return err
}
serverReq, err := readTSRequest(conn)
if err != nil {
return fmt.Errorf("读取服务端公钥校验失败(flags=0x%08x mic=%v targetInfo=%d): %w", challenge.Flags, requiresMIC(challenge.TargetInfo), len(challenge.TargetInfo), err)
}
if serverReq.ErrorCode != 0 {
return fmt.Errorf("CredSSP 返回错误码: %s", ntStatusMessage(serverReq.ErrorCode))
}
creds, err := encodePasswordCredentials(domain, user, password)
if err != nil {
return err
}
encryptedCreds := sec.encrypt(creds)
req, err = encodeTSRequestVersion(opts.Version, nil, encryptedCreds, nil, nil)
if err != nil {
return err
}
_, err = conn.Write(req)
return err
}
func buildPubKeyAuth(pubKey, clientNonce []byte, style string) []byte {
switch style {
case "raw-plus-one":
out := append([]byte(nil), pubKey...)
if len(out) > 0 {
out[0]++
}
return out
case "binding":
return credSSPBindingHash(clientNonce, pubKey)
default:
return pubKey
}
}
func ntStatusMessage(code int) string {
u := uint32(code)
switch u {
case 0xC0000064:
return "0xC0000064 用户不存在"
case 0xC000006A:
return "0xC000006A 密码错误"
case 0xC000006D:
return "0xC000006D 用户名或认证信息无效"
case 0xC000006E:
return "0xC000006E 账号登录限制"
case 0xC0000070:
return "0xC0000070 工作站限制"
case 0xC0000071:
return "0xC0000071 密码已过期"
case 0xC0000072:
return "0xC0000072 账号已禁用"
case 0xC000015B:
return "0xC000015B 当前用户未被授予远程登录权限"
case 0xC0000193:
return "0xC0000193 账号已过期"
case 0xC0000234:
return "0xC0000234 账号已锁定,请先在 Windows/域控侧解锁后再连接"
default:
return fmt.Sprintf("0x%08X", u)
}
}
func credSSPBindingHash(clientNonce, pubKey []byte) []byte {
h := sha256.New()
h.Write([]byte("CredSSP Client-To-Server Binding Hash\x00"))
h.Write(clientNonce)
h.Write(pubKey)
return h.Sum(nil)
}
func channelBindingsHash(conn *tls.Conn) []byte {
state := conn.ConnectionState()
if len(state.PeerCertificates) == 0 {
return nil
}
cert := state.PeerCertificates[0]
alg := cert.SignatureAlgorithm.String()
var digest []byte
switch {
case strings.Contains(alg, "MD5"):
sum := md5.Sum(cert.Raw)
digest = sum[:]
case strings.Contains(alg, "SHA1"):
sum := sha1.Sum(cert.Raw)
digest = sum[:]
default:
sum := sha256.Sum256(cert.Raw)
digest = sum[:]
}
appData := append([]byte("tls-server-end-point:"), digest...)
gss := make([]byte, 16+len(appData))
copy(gss[16:], appData)
gss[12] = byte(len(appData))
gss[13] = byte(len(appData) >> 8)
gss[14] = byte(len(appData) >> 16)
gss[15] = byte(len(appData) >> 24)
sum := md5.Sum(gss)
return sum[:]
}
func readTSRequest(r io.Reader) (*tsRequest, error) {
der, err := readDER(r)
if err != nil {
return nil, err
}
return decodeTSRequest(der)
}
func readDER(r io.Reader) ([]byte, error) {
header := make([]byte, 2)
if _, err := io.ReadFull(r, header); err != nil {
return nil, err
}
if header[0] != 0x30 {
return nil, fmt.Errorf("unexpected DER sequence tag: 0x%x", header[0])
}
length := int(header[1])
lenBytes := 0
if header[1]&0x80 != 0 {
lenBytes = int(header[1] & 0x7f)
if lenBytes == 0 || lenBytes > 4 {
return nil, fmt.Errorf("invalid DER length bytes: %d", lenBytes)
}
raw := make([]byte, lenBytes)
if _, err := io.ReadFull(r, raw); err != nil {
return nil, err
}
length = 0
for _, b := range raw {
length = length<<8 | int(b)
}
header = append(header, raw...)
}
body := make([]byte, length)
if _, err := io.ReadFull(r, body); err != nil {
return nil, err
}
return append(header, body...), nil
}
func peerPublicKey(conn *tls.Conn, style string) ([]byte, error) {
state := conn.ConnectionState()
if len(state.PeerCertificates) == 0 {
return nil, fmt.Errorf("RDP TLS 证书为空")
}
cert := state.PeerCertificates[0]
if style == "rsa-compat" {
if pub, ok := cert.PublicKey.(*rsa.PublicKey); ok {
return asn1.Marshal(rsaPublicKeyCompat{N: pub.N, E: pub.E})
}
}
// MS-CSSP v5/v6 绑定哈希必须使用 X.509 SubjectPublicKey,而非完整 SPKI // Logan.Li
return subjectPublicKey(cert)
}
func subjectPublicKey(cert *x509.Certificate) ([]byte, error) {
if pub, ok := cert.PublicKey.(*rsa.PublicKey); ok {
return asn1.Marshal(*pub)
}
var spki struct {
Algorithm pkix.AlgorithmIdentifier
PublicKey asn1.BitString
}
if _, err := asn1.Unmarshal(cert.RawSubjectPublicKeyInfo, &spki); err != nil {
return nil, fmt.Errorf("解析 TLS SubjectPublicKey 失败: %w", err)
}
if len(spki.PublicKey.Bytes) == 0 {
return nil, fmt.Errorf("TLS SubjectPublicKey 为空")
}
return spki.PublicKey.Bytes, nil
}
type rsaPublicKeyCompat struct {
N *big.Int `asn1:"explicit,tag:0"`
E int `asn1:"explicit,tag:1"`
}
func splitDomainUser(domain, user string) (string, string) {
if strings.Contains(user, "\\") {
parts := strings.Split(user, "\\")
if len(parts) >= 2 {
return parts[0], parts[len(parts)-1]
}
}
if strings.Contains(user, "/") {
parts := strings.Split(user, "/")
if len(parts) >= 2 {
return parts[0], parts[len(parts)-1]
}
}
return domain, user
}
+21
View File
@@ -0,0 +1,21 @@
package credssp
import "testing"
func TestEncodeDecodeTSRequest(t *testing.T) {
nego := buildNegotiate()
req, err := encodeTSRequest([][]byte{nego}, nil, nil)
if err != nil {
t.Fatal(err)
}
decoded, err := decodeTSRequest(req)
if err != nil {
t.Fatal(err)
}
if decoded.Version != defaultCredSSPVersion {
t.Fatalf("version=%d", decoded.Version)
}
if len(decoded.NegoTokens) != 1 || string(decoded.NegoTokens[0].Data[:8]) != "NTLMSSP\x00" {
t.Fatalf("invalid nego token")
}
}
+386
View File
@@ -0,0 +1,386 @@
package credssp
import (
"bytes"
"crypto/hmac"
"crypto/md5"
"crypto/rand"
"crypto/rc4"
"encoding/binary"
"fmt"
"strings"
"time"
"golang.org/x/crypto/md4"
)
const (
ntlmNegotiateUnicode uint32 = 0x00000001
ntlmRequestTarget uint32 = 0x00000004
ntlmNegotiateSign uint32 = 0x00000010
ntlmNegotiateSeal uint32 = 0x00000020
ntlmNegotiateNTLM uint32 = 0x00000200
ntlmNegotiateAlwaysSign uint32 = 0x00008000
ntlmNegotiateExtendedSessionSecurity uint32 = 0x00080000
ntlmNegotiateTargetInfo uint32 = 0x00800000
ntlmNegotiateVersion uint32 = 0x02000000
ntlmNegotiate128 uint32 = 0x20000000
ntlmNegotiateKeyExch uint32 = 0x40000000
ntlmNegotiate56 uint32 = 0x80000000
msvAvEOL uint16 = 0x0000
msvAvNbComputerName uint16 = 0x0001
msvAvNbDomainName uint16 = 0x0002
msvAvFlags uint16 = 0x0006
msvAvTimestamp uint16 = 0x0007
msvChannelBindings uint16 = 0x000A
)
const ntlmClientFlags = ntlmNegotiateUnicode |
ntlmRequestTarget |
ntlmNegotiateSign |
ntlmNegotiateSeal |
ntlmNegotiateNTLM |
ntlmNegotiateAlwaysSign |
ntlmNegotiateExtendedSessionSecurity |
ntlmNegotiateTargetInfo |
ntlmNegotiateVersion |
ntlmNegotiate128 |
ntlmNegotiateKeyExch
type ntlmChallenge struct {
Raw []byte
Flags uint32
TargetName string
ServerChallenge []byte
TargetInfo []byte
Timestamp []byte
}
type ntlmOptions struct {
ForceMIC bool
DisableKeyExch bool
ChannelBindings []byte
OmitVersion bool
}
type ntlmSecurity struct {
clientSigningKey []byte
serverSigningKey []byte
clientSealing *rc4.Cipher
serverSealing *rc4.Cipher
seq uint32
}
func buildNegotiate() []byte {
buf := bytes.NewBuffer(nil)
buf.Write([]byte("NTLMSSP\x00"))
writeU32(buf, 1)
writeU32(buf, ntlmClientFlags)
writeSecBuffer(buf, 0, 0)
writeSecBuffer(buf, 0, 0)
buf.Write(ntlmVersion())
return buf.Bytes()
}
func parseChallenge(data []byte) (*ntlmChallenge, error) {
if len(data) < 48 || string(data[:8]) != "NTLMSSP\x00" || binary.LittleEndian.Uint32(data[8:12]) != 2 {
return nil, fmt.Errorf("invalid NTLM challenge")
}
targetNameLen := int(binary.LittleEndian.Uint16(data[12:14]))
targetNameOffset := int(binary.LittleEndian.Uint32(data[16:20]))
targetInfoLen := int(binary.LittleEndian.Uint16(data[40:42]))
targetInfoOffset := int(binary.LittleEndian.Uint32(data[44:48]))
if targetInfoOffset < 0 || targetInfoOffset+targetInfoLen > len(data) {
return nil, fmt.Errorf("invalid NTLM target info")
}
targetName := ""
if targetNameOffset >= 0 && targetNameOffset+targetNameLen <= len(data) {
targetName = utf16leString(data[targetNameOffset : targetNameOffset+targetNameLen])
}
targetInfo := data[targetInfoOffset : targetInfoOffset+targetInfoLen]
return &ntlmChallenge{
Raw: append([]byte(nil), data...),
Flags: binary.LittleEndian.Uint32(data[20:24]),
TargetName: targetName,
ServerChallenge: append([]byte(nil), data[24:32]...),
TargetInfo: append([]byte(nil), targetInfo...),
Timestamp: findAVPair(targetInfo, msvAvTimestamp),
}, nil
}
func buildAuthenticate(ch *ntlmChallenge, domain, user, password, workstation string, negotiate []byte, opts ntlmOptions) ([]byte, *ntlmSecurity, error) {
if domain == "" {
domain = ch.TargetName
}
if domain == "" {
domain = utf16leString(findAVPair(ch.TargetInfo, msvAvNbDomainName))
}
respKey := ntowfv2(password, user, domain)
clientChallenge := randomBytes(8)
timestamp := ch.Timestamp
if len(timestamp) == 0 {
timestamp = windowsFileTime(time.Now())
}
blob := bytes.NewBuffer(nil)
blob.Write([]byte{0x01, 0x01, 0, 0, 0, 0, 0, 0})
blob.Write(timestamp)
blob.Write(clientChallenge)
blob.Write([]byte{0, 0, 0, 0})
targetInfo := ch.TargetInfo
if len(opts.ChannelBindings) == 16 {
targetInfo = withChannelBindings(ch.TargetInfo, opts.ChannelBindings)
}
blob.Write(targetInfo)
blob.Write([]byte{0, 0, 0, 0})
ntProof := hmacMD5(respKey, append(append([]byte(nil), ch.ServerChallenge...), blob.Bytes()...))
ntResp := append(ntProof, blob.Bytes()...)
lmResp := append(hmacMD5(respKey, append(append([]byte(nil), ch.ServerChallenge...), clientChallenge...)), clientChallenge...)
sessionBaseKey := hmacMD5(respKey, ntProof)
exportedSessionKey := randomBytes(16)
encryptedSessionKey := rc4k(sessionBaseKey, exportedSessionKey)
domainBytes := utf16le(domain)
userBytes := utf16le(user)
workstationBytes := utf16le(workstation)
flags := ch.Flags & ntlmClientFlags
flags |= ntlmNegotiateUnicode | ntlmNegotiateExtendedSessionSecurity | ntlmNegotiateNTLM
if opts.DisableKeyExch {
flags &^= ntlmNegotiateKeyExch
exportedSessionKey = sessionBaseKey
encryptedSessionKey = nil
}
includeMIC := opts.ForceMIC || requiresMIC(ch.TargetInfo)
baseLen := 72
if opts.OmitVersion {
baseLen = 64
}
if includeMIC {
baseLen += 16
}
payload := bytes.NewBuffer(nil)
offset := uint32(baseLen)
lmOffset := offset
payload.Write(lmResp)
offset += uint32(len(lmResp))
ntOffset := offset
payload.Write(ntResp)
offset += uint32(len(ntResp))
domainOffset := offset
payload.Write(domainBytes)
offset += uint32(len(domainBytes))
userOffset := offset
payload.Write(userBytes)
offset += uint32(len(userBytes))
workstationOffset := offset
payload.Write(workstationBytes)
offset += uint32(len(workstationBytes))
sessionOffset := offset
if len(encryptedSessionKey) > 0 {
payload.Write(encryptedSessionKey)
}
msg := bytes.NewBuffer(nil)
msg.Write([]byte("NTLMSSP\x00"))
writeU32(msg, 3)
writeSecBuffer(msg, uint16(len(lmResp)), lmOffset)
writeSecBuffer(msg, uint16(len(ntResp)), ntOffset)
writeSecBuffer(msg, uint16(len(domainBytes)), domainOffset)
writeSecBuffer(msg, uint16(len(userBytes)), userOffset)
writeSecBuffer(msg, uint16(len(workstationBytes)), workstationOffset)
writeSecBuffer(msg, uint16(len(encryptedSessionKey)), sessionOffset)
writeU32(msg, flags)
if !opts.OmitVersion {
msg.Write(ntlmVersion())
}
micPos := -1
if includeMIC {
micPos = msg.Len()
msg.Write(make([]byte, 16))
}
msg.Write(payload.Bytes())
if includeMIC {
micInput := bytes.NewBuffer(nil)
micInput.Write(negotiate)
micInput.Write(ch.Raw)
micInput.Write(msg.Bytes())
mic := hmacMD5(exportedSessionKey, micInput.Bytes())
copy(msg.Bytes()[micPos:micPos+16], mic)
}
clientSeal, _ := rc4.NewCipher(md5sum(append(exportedSessionKey, []byte("session key to client-to-server sealing key magic constant\x00")...)))
serverSeal, _ := rc4.NewCipher(md5sum(append(exportedSessionKey, []byte("session key to server-to-client sealing key magic constant\x00")...)))
sec := &ntlmSecurity{
clientSigningKey: md5sum(append(exportedSessionKey, []byte("session key to client-to-server signing key magic constant\x00")...)),
serverSigningKey: md5sum(append(exportedSessionKey, []byte("session key to server-to-client signing key magic constant\x00")...)),
clientSealing: clientSeal,
serverSealing: serverSeal,
}
return msg.Bytes(), sec, nil
}
func (s *ntlmSecurity) encrypt(data []byte) []byte {
sealed := make([]byte, len(data))
s.clientSealing.XORKeyStream(sealed, data)
macInput := bytes.NewBuffer(nil)
writeU32(macInput, s.seq)
macInput.Write(data)
checksum := hmacMD5(s.clientSigningKey, macInput.Bytes())[:8]
encryptedChecksum := make([]byte, 8)
s.clientSealing.XORKeyStream(encryptedChecksum, checksum)
out := bytes.NewBuffer(nil)
writeU32(out, 1)
out.Write(encryptedChecksum)
writeU32(out, s.seq)
out.Write(sealed)
s.seq++
return out.Bytes()
}
func ntowfv2(password, user, domain string) []byte {
h := md4.New()
h.Write(utf16le(password))
return hmacMD5(h.Sum(nil), utf16le(strings.ToUpper(user)+domain))
}
func hmacMD5(key, data []byte) []byte {
h := hmac.New(md5.New, key)
h.Write(data)
return h.Sum(nil)
}
func md5sum(data []byte) []byte {
sum := md5.Sum(data)
return sum[:]
}
func rc4k(key, data []byte) []byte {
c, _ := rc4.NewCipher(key)
out := make([]byte, len(data))
c.XORKeyStream(out, data)
return out
}
func randomBytes(n int) []byte {
buf := make([]byte, n)
_, _ = rand.Read(buf)
return buf
}
func windowsFileTime(t time.Time) []byte {
v := uint64(t.UnixNano()/100) + 116444736000000000
out := make([]byte, 8)
binary.LittleEndian.PutUint64(out, v)
return out
}
func findAVPair(data []byte, id uint16) []byte {
for len(data) >= 4 {
avID := binary.LittleEndian.Uint16(data[0:2])
avLen := int(binary.LittleEndian.Uint16(data[2:4]))
data = data[4:]
if avLen > len(data) {
return nil
}
if avID == msvAvEOL {
return nil
}
if avID == id {
return append([]byte(nil), data[:avLen]...)
}
data = data[avLen:]
}
return nil
}
func requiresMIC(targetInfo []byte) bool {
flags := findAVPair(targetInfo, msvAvFlags)
if len(flags) < 4 {
return false
}
return binary.LittleEndian.Uint32(flags)&0x00000002 != 0
}
func withChannelBindings(targetInfo, hash []byte) []byte {
out := bytes.NewBuffer(nil)
inserted := false
for len(targetInfo) >= 4 {
avID := binary.LittleEndian.Uint16(targetInfo[0:2])
avLen := int(binary.LittleEndian.Uint16(targetInfo[2:4]))
if 4+avLen > len(targetInfo) {
return targetInfo
}
if avID == msvAvEOL {
if !inserted {
writeU16(out, msvChannelBindings)
writeU16(out, 16)
out.Write(hash[:16])
}
out.Write(targetInfo[:4])
return out.Bytes()
}
if avID == msvChannelBindings {
writeU16(out, msvChannelBindings)
writeU16(out, 16)
out.Write(hash[:16])
inserted = true
} else {
out.Write(targetInfo[:4+avLen])
}
targetInfo = targetInfo[4+avLen:]
}
if !inserted {
writeU16(out, msvChannelBindings)
writeU16(out, 16)
out.Write(hash[:16])
}
out.Write([]byte{0, 0, 0, 0})
return out.Bytes()
}
func writeSecBuffer(buf *bytes.Buffer, length uint16, offset uint32) {
writeU16(buf, length)
writeU16(buf, length)
writeU32(buf, offset)
}
func writeU16(buf *bytes.Buffer, v uint16) {
_ = binary.Write(buf, binary.LittleEndian, v)
}
func writeU32(buf *bytes.Buffer, v uint32) {
_ = binary.Write(buf, binary.LittleEndian, v)
}
func ntlmVersion() []byte {
out := []byte{10, 0, 0, 0, 0, 0, 0, 15}
binary.LittleEndian.PutUint16(out[2:4], 19045)
return out
}
func utf16le(s string) []byte {
out := bytes.NewBuffer(nil)
for _, r := range s {
if r > 0xffff {
r = '?'
}
writeU16(out, uint16(r))
}
return out.Bytes()
}
func utf16leString(data []byte) string {
runes := make([]rune, 0, len(data)/2)
for i := 0; i+1 < len(data); i += 2 {
v := binary.LittleEndian.Uint16(data[i : i+2])
if v == 0 {
continue
}
runes = append(runes, rune(v))
}
return string(runes)
}
+55
View File
@@ -0,0 +1,55 @@
package credssp
import (
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"crypto/x509/pkix"
"encoding/asn1"
"math/big"
"testing"
"time"
)
func TestSubjectPublicKeyDiffersFromPKIX(t *testing.T) {
key, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
t.Fatal(err)
}
certDER, err := x509.CreateCertificate(rand.Reader, &x509.Certificate{
SerialNumber: big.NewInt(1),
Subject: pkix.Name{CommonName: "rdp-test"},
NotBefore: time.Now().Add(-time.Hour),
NotAfter: time.Now().Add(time.Hour),
}, &x509.Certificate{
SerialNumber: big.NewInt(1),
Subject: pkix.Name{CommonName: "rdp-test"},
NotBefore: time.Now().Add(-time.Hour),
NotAfter: time.Now().Add(time.Hour),
}, &key.PublicKey, key)
if err != nil {
t.Fatal(err)
}
cert, err := x509.ParseCertificate(certDER)
if err != nil {
t.Fatal(err)
}
subjectKey, err := subjectPublicKey(cert)
if err != nil {
t.Fatal(err)
}
pkixKey, err := x509.MarshalPKIXPublicKey(cert.PublicKey)
if err != nil {
t.Fatal(err)
}
if string(subjectKey) == string(pkixKey) {
t.Fatal("SubjectPublicKey 不应等于完整 SPKI")
}
rsaDER, err := asn1.Marshal(*cert.PublicKey.(*rsa.PublicKey))
if err != nil {
t.Fatal(err)
}
if string(subjectKey) != string(rsaDER) {
t.Fatalf("SubjectPublicKey 应与 RSA PKCS#1 DER 一致,len=%d/%d", len(subjectKey), len(rsaDER))
}
}
+145
View File
@@ -0,0 +1,145 @@
package display
import (
"bytes"
"encoding/base64"
"encoding/binary"
"image"
"image/draw"
"image/png"
)
// Bitmap 表示协议层解码后的 BGRA/RGB565 小块,display 只负责合成和输出 // Logan.Li
type Bitmap struct {
X int
Y int
Width int
Height int
BitsPerPixel int
Data []byte
}
// Frame 表示一次合成后的脏区域输出 // Logan.Li
type Frame struct {
X int
Y int
Width int
Height int
Image string
RGBA []byte
}
// Framebuffer 借鉴 Guacamole/bring 的 display/layer 思路,在后端维护完整画布 // Logan.Li
type Framebuffer struct {
width int
height int
img *image.RGBA
dirty image.Rectangle
}
// NewFramebuffer 创建固定尺寸远端画布,所有 RDP 更新先合成到这里 // Logan.Li
func NewFramebuffer(width, height int) *Framebuffer {
if width <= 0 {
width = 1280
}
if height <= 0 {
height = 800
}
return &Framebuffer{
width: width,
height: height,
img: image.NewRGBA(image.Rect(0, 0, width, height)),
}
}
// Draw 合成一个 bitmap 小块,并扩大脏矩形 // Logan.Li
func (f *Framebuffer) Draw(b Bitmap) {
if b.Width <= 0 || b.Height <= 0 || len(b.Data) == 0 {
return
}
dstRect := image.Rect(b.X, b.Y, b.X+b.Width, b.Y+b.Height).Intersect(f.img.Bounds())
if dstRect.Empty() {
return
}
src := bitmapToRGBA(b)
srcPoint := image.Pt(dstRect.Min.X-b.X, dstRect.Min.Y-b.Y)
draw.Draw(f.img, dstRect, src, srcPoint, draw.Src)
if f.dirty.Empty() {
f.dirty = dstRect
} else {
f.dirty = f.dirty.Union(dstRect)
}
}
// Flush 输出当前脏区域 PNG,并清空脏矩形 // Logan.Li
func (f *Framebuffer) Flush() (Frame, bool) {
frame, ok := f.FlushRGBA()
if !ok {
return Frame{}, false
}
img := image.NewRGBA(image.Rect(0, 0, frame.Width, frame.Height))
copy(img.Pix, frame.RGBA)
var buf bytes.Buffer
if err := png.Encode(&buf, img); err != nil {
return Frame{}, false
}
frame.Image = "data:image/png;base64," + base64.StdEncoding.EncodeToString(buf.Bytes())
return frame, true
}
// FlushRGBA 输出当前脏区域原始 RGBA,避免实时 RDP 路径重复 PNG/base64 编解码。 // Logan.Li
func (f *Framebuffer) FlushRGBA() (Frame, bool) {
if f.dirty.Empty() {
return Frame{}, false
}
rect := f.dirty.Intersect(f.img.Bounds())
f.dirty = image.Rectangle{}
if rect.Empty() {
return Frame{}, false
}
crop := image.NewRGBA(image.Rect(0, 0, rect.Dx(), rect.Dy()))
draw.Draw(crop, crop.Bounds(), f.img, rect.Min, draw.Src)
return Frame{
X: rect.Min.X,
Y: rect.Min.Y,
Width: rect.Dx(),
Height: rect.Dy(),
RGBA: append([]byte(nil), crop.Pix...),
}, true
}
func bitmapToRGBA(b Bitmap) *image.RGBA {
img := image.NewRGBA(image.Rect(0, 0, b.Width, b.Height))
pix := img.Pix
switch b.BitsPerPixel {
case 1, 2:
n := b.Width * b.Height
for i := 0; i < n && i*2+1 < len(b.Data); i++ {
// grdp 的 RLE 输出 16bpp 使用大端 RGB565,按小端会导致蓝紫/绿色偏色。 // Logan.Li
c := binary.BigEndian.Uint16(b.Data[i*2 : i*2+2])
r := uint8((c >> 11) & 0x1F)
g := uint8((c >> 5) & 0x3F)
bl := uint8(c & 0x1F)
off := i * 4
pix[off] = (r << 3) | (r >> 2)
pix[off+1] = (g << 2) | (g >> 4)
pix[off+2] = (bl << 3) | (bl >> 2)
pix[off+3] = 0xFF
}
default:
stride := b.BitsPerPixel
n := b.Width * b.Height
for i := 0; i < n; i++ {
s := i * stride
if s+2 >= len(b.Data) {
break
}
off := i * 4
pix[off] = b.Data[s+2]
pix[off+1] = b.Data[s+1]
pix[off+2] = b.Data[s]
pix[off+3] = 0xFF
}
}
return img
}
+126
View File
@@ -0,0 +1,126 @@
package domain
import (
"context"
"encoding/json"
"sync"
"time"
)
// Config 是后端 RDP 渲染引擎的连接配置,凭据只在服务端使用 // Logan.Li
type Config struct {
Host string
Port int
Username string
Password string
Domain string
Security string
Width int
Height int
MCSMode string
Debug bool
}
// ClientEvent 是浏览器发回后端的输入事件 // Logan.Li
type ClientEvent struct {
Type string `json:"type"`
X int `json:"x,omitempty"`
Y int `json:"y,omitempty"`
Button int `json:"button,omitempty"`
DeltaY int `json:"delta_y,omitempty"`
Code string `json:"code,omitempty"`
Key string `json:"key,omitempty"`
Down bool `json:"down,omitempty"`
Width int `json:"width,omitempty"`
Height int `json:"height,omitempty"`
Text string `json:"text,omitempty"`
}
// ServerFrame 是后端推给浏览器 Canvas 的统一帧协议 // Logan.Li
type ServerFrame struct {
Type string `json:"type"`
X int `json:"x,omitempty"`
Y int `json:"y,omitempty"`
Width int `json:"width,omitempty"`
Height int `json:"height,omitempty"`
Image string `json:"image,omitempty"`
Message string `json:"message,omitempty"`
Text string `json:"text,omitempty"`
RGBA []byte `json:"-"`
}
// FrameSink 接收 RDP 引擎输出,控制器负责写入 WebSocket // Logan.Li
type FrameSink interface {
SendFrame(ServerFrame) error
}
// Engine 隔离自研 RDP 协议实现,避免控制器关心握手和图形帧细节 // Logan.Li
type Engine interface {
Run(ctx context.Context, cfg Config, sink FrameSink, events <-chan ClientEvent) error
}
// HandshakeInfo 记录轻量 RDP 第一阶段握手结果,用于前端诊断 // Logan.Li
type HandshakeInfo struct {
Address string
SelectedProtocol string
Duration time.Duration
TLSCipher string
CertSubject string
}
// MemoryRecorder 保存图形帧摘要,后续可替换为 PNG/JPEG 序列录屏 // Logan.Li
type MemoryRecorder struct {
mu sync.Mutex
events []ServerFrame
limit int
}
// NewMemoryRecorder 创建内存帧记录器,限制录屏摘要常驻内存大小 // Logan.Li
func NewMemoryRecorder(limit int) *MemoryRecorder {
if limit <= 0 {
limit = 500
}
return &MemoryRecorder{limit: limit}
}
// Append 记录一帧 RDP 输出,超过上限时丢弃最早帧 // Logan.Li
func (r *MemoryRecorder) Append(frame ServerFrame) {
r.mu.Lock()
defer r.mu.Unlock()
// 实时帧只用于 WebSocket 推送,审计内存索引保留元数据避免拖动窗口时触发大对象 GC。 // Logan.Li
frame.RGBA = nil
if frame.Type == "bitmap" {
frame.Image = ""
}
if len(r.events) >= r.limit {
copy(r.events, r.events[1:])
r.events[len(r.events)-1] = frame
return
}
r.events = append(r.events, frame)
}
// AppendSnapshot 记录节流后的图形快照,用于 RDP 历史回放。 // Logan.Li
func (r *MemoryRecorder) AppendSnapshot(frame ServerFrame) {
r.mu.Lock()
defer r.mu.Unlock()
frame.RGBA = nil
if len(r.events) >= r.limit {
copy(r.events, r.events[1:])
r.events[len(r.events)-1] = frame
return
}
r.events = append(r.events, frame)
}
// JSON 输出前端回放需要的轻量帧序列 // Logan.Li
func (r *MemoryRecorder) JSON() string {
r.mu.Lock()
defer r.mu.Unlock()
b, _ := json.Marshal(struct {
Version int `json:"version"`
Time time.Time `json:"time"`
Frames []ServerFrame `json:"frames"`
}{Version: 1, Time: time.Now(), Frames: r.events})
return string(b)
}
+46
View File
@@ -0,0 +1,46 @@
package rdp
import (
"ezops/pkg/rdp/domain"
"log"
)
// Config 是后端 RDP 渲染引擎的连接配置,根包保留别名兼容控制器 // Logan.Li
type Config = domain.Config
// ClientEvent 是浏览器发回后端的输入事件,根包保留别名兼容控制器 // Logan.Li
type ClientEvent = domain.ClientEvent
// ServerFrame 是后端推给浏览器 Canvas 的统一帧协议,根包保留别名兼容控制器 // Logan.Li
type ServerFrame = domain.ServerFrame
// FrameSink 接收 RDP 引擎输出,根包保留别名兼容控制器 // Logan.Li
type FrameSink = domain.FrameSink
// Engine 隔离自研 RDP 协议实现,根包保留别名兼容控制器 // Logan.Li
type Engine = domain.Engine
// HandshakeInfo 记录轻量 RDP 第一阶段握手结果,根包保留别名兼容诊断入口 // Logan.Li
type HandshakeInfo = domain.HandshakeInfo
// MemoryRecorder 保存图形帧摘要,根包保留别名兼容审计回放 // Logan.Li
type MemoryRecorder = domain.MemoryRecorder
const EngineRDP = "rdp"
// SelectedEngineName 返回当前 RDP 引擎名称,固定使用 AIOps 自研 RDP 引擎。 // Logan.Li
func SelectedEngineName() string {
return EngineRDP
}
// NewEngine 创建自研 RDP 图形引擎,避免依赖第三方协议栈。 // Logan.Li
func NewEngine() Engine {
name := SelectedEngineName()
log.Printf("[RDP] selected engine=%s", name)
return &SessionEngine{}
}
// NewMemoryRecorder 创建内存帧记录器,根包保留构造函数兼容审计回放 // Logan.Li
func NewMemoryRecorder(limit int) *MemoryRecorder {
return domain.NewMemoryRecorder(limit)
}
+264
View File
@@ -0,0 +1,264 @@
package rdp
import (
"bytes"
"fmt"
"ezops/pkg/rdp/trace"
)
const (
fastPathOutputCompressionUsed = 0xC0
fastPathFragmentSingle = 0x00
fastPathFragmentLast = 0x10
fastPathFragmentFirst = 0x20
fastPathFragmentNext = 0x30
)
// FastPathReceiver 解析服务端 Fast-Path 更新并产出位图块 // Logan.Li
type FastPathReceiver struct {
mppc *MppcDecompressor
fragments bytes.Buffer
frameIDs []uint32
updateStats map[byte]int
}
func NewFastPathReceiver() *FastPathReceiver {
return &FastPathReceiver{mppc: NewMppcDecompressor(), updateStats: make(map[byte]int)}
}
func (f *FastPathReceiver) Handle(secFlag byte, payload []byte) ([]BitmapRect, error) {
_ = secFlag
r := bytes.NewReader(payload)
var rects []BitmapRect
for r.Len() > 0 {
updateHeader, err := readU8(r)
if err != nil {
return rects, err
}
updateCode := updateHeader & 0x0f
fragmentation := updateHeader & 0x30
compression := updateHeader & 0xC0
f.updateStats[updateCode]++
var compressionFlags byte
if compression == fastPathOutputCompressionUsed {
compressionFlags, err = readU8(r)
if err != nil {
return rects, err
}
}
size, err := readLE16(r)
if err != nil {
return rects, err
}
if f.updateStats[updateCode] <= 8 || f.updateStats[updateCode]%100 == 0 {
trace.Logf("fastpath update code=%s(0x%x) count=%d size=%d frag=0x%02x compression=0x%02x flags=0x%02x", fastPathUpdateName(updateCode), updateCode, f.updateStats[updateCode], size, fragmentation, compression, compressionFlags)
}
block := make([]byte, int(size))
if _, err = r.Read(block); err != nil {
return rects, err
}
if fragmentation != fastPathFragmentSingle {
if fragmentation == fastPathFragmentFirst {
f.fragments.Reset()
}
f.fragments.Write(block)
if fragmentation != fastPathFragmentLast {
continue
}
block = append([]byte(nil), f.fragments.Bytes()...)
}
if compressionFlags != 0 {
decompressed, err := f.mppc.Decompress(compressionFlags, block)
if err != nil {
continue
}
block = decompressed
}
switch updateCode {
case fastPathUpdateBitmap:
bs, err := parseFastPathBitmap(block)
if err != nil {
return rects, err
}
rects = append(rects, bs...)
case fastPathUpdateSurfaceCmds:
bs, frameIDs, err := parseSurfaceCommands(block)
if err != nil {
return rects, err
}
f.frameIDs = append(f.frameIDs, frameIDs...)
rects = append(rects, bs...)
default:
trace.Logf("fastpath skip update code=%s(0x%x) size=%d", fastPathUpdateName(updateCode), updateCode, len(block))
}
}
return rects, nil
}
func fastPathUpdateName(code byte) string {
switch code {
case 0x0:
return "orders"
case fastPathUpdateBitmap:
return "bitmap"
case 0x2:
return "palette"
case 0x3:
return "synchronize"
case fastPathUpdateSurfaceCmds:
return "surface"
case 0x5:
return "ptr_null"
case 0x6:
return "ptr_default"
case 0x8:
return "ptr_position"
case 0x9:
return "color"
case 0x0a:
return "ptr_cached"
case 0x0b:
return "pointer"
case 0x0c:
return "large_pointer"
default:
return "unknown"
}
}
// TakeFrameIDs 取出需要确认的 Surface frame,避免服务端因队列满停止推帧 // Logan.Li
func (f *FastPathReceiver) TakeFrameIDs() []uint32 {
if len(f.frameIDs) == 0 {
return nil
}
out := append([]uint32(nil), f.frameIDs...)
f.frameIDs = f.frameIDs[:0]
return out
}
func (f *FastPathReceiver) HandleSlowPath(payload []byte) ([]BitmapRect, error) {
ind, err := parseMCSDataIndication(payload)
if err != nil {
return nil, err
}
if len(ind.payload) < 6 {
return nil, nil
}
return parseSlowPathBitmap(ind.payload[4:])
}
func parseSurfaceCommands(data []byte) ([]BitmapRect, []uint32, error) {
r := bytes.NewReader(data)
var out []BitmapRect
var frameIDs []uint32
for r.Len() >= 2 {
cmdType, err := readLE16(r)
if err != nil {
return out, frameIDs, err
}
switch cmdType {
case 0x0001, 0x0006:
wb, err := readSurfaceBits(r)
if err != nil {
return out, frameIDs, err
}
if rect, ok := decodeWireBitmap(wb); ok {
out = append(out, rect)
}
case 0x0004:
frameAction, err := readLE16(r)
if err != nil {
return out, frameIDs, err
}
frameID, err := readLE32(r)
if err != nil {
return out, frameIDs, err
}
trace.Logf("surface frame marker action=%d frameID=%d", frameAction, frameID)
if frameAction == 0x0001 {
frameIDs = append(frameIDs, frameID)
}
default:
return out, frameIDs, fmt.Errorf("未知 SurfaceCmd: 0x%04x", cmdType)
}
}
return out, frameIDs, nil
}
func readSurfaceBits(r *bytes.Reader) (wireBitmap, error) {
destLeft, err := readLE16(r)
if err != nil {
return wireBitmap{}, err
}
destTop, err := readLE16(r)
if err != nil {
return wireBitmap{}, err
}
destRight, err := readLE16(r)
if err != nil {
return wireBitmap{}, err
}
destBottom, err := readLE16(r)
if err != nil {
return wireBitmap{}, err
}
bpp, err := readU8(r)
if err != nil {
return wireBitmap{}, err
}
flags, err := readU8(r)
if err != nil {
return wireBitmap{}, err
}
if _, err = readU8(r); err != nil {
return wireBitmap{}, err
}
codecID, err := readU8(r)
if err != nil {
return wireBitmap{}, err
}
width, err := readLE16(r)
if err != nil {
return wireBitmap{}, err
}
height, err := readLE16(r)
if err != nil {
return wireBitmap{}, err
}
dataLen, err := readLE32(r)
if err != nil {
return wireBitmap{}, err
}
if flags&0x01 != 0 {
if dataLen < 24 {
return wireBitmap{}, fmt.Errorf("SurfaceBits 扩展头长度非法: %d", dataLen)
}
if _, err = r.Seek(24, 1); err != nil {
return wireBitmap{}, err
}
dataLen -= 24
}
data := make([]byte, int(dataLen))
if _, err = r.Read(data); err != nil {
return wireBitmap{}, err
}
if codecID != 0 {
trace.Logf("surface skip unsupported codec=%d rect=%dx%d@%d,%d data=%d flags=0x%02x", codecID, width, height, destLeft, destTop, dataLen, flags)
return wireBitmap{}, nil
}
trace.Logf("surface bits codec=%d rect=%dx%d@%d,%d data=%d flags=0x%02x", codecID, width, height, destLeft, destTop, dataLen, flags)
return wireBitmap{
DestLeft: destLeft, DestTop: destTop, DestRight: destRight, DestBottom: destBottom,
Width: width, Height: height, BitsPerPixel: uint16(bpp), Flags: uint16(flags) | bitmapNoProcessing,
BitmapDataStream: data,
}, nil
}
func readU8(r *bytes.Reader) (byte, error) {
b, err := r.ReadByte()
return b, err
}
+161
View File
@@ -0,0 +1,161 @@
package rdp
import (
"io"
"sync"
"time"
"unicode/utf16"
)
const (
fastPathInputEventScancode = 0
fastPathInputEventMouse = 1
fastPathInputEventUnicode = 4
fastPathInputKBRelease = 0x01
fastPathInputKBExtended = 0x02
fastPathInputKBExtended1 = 0x04
ptrFlagsMove = 0x0800
ptrFlagsDown = 0x8000
ptrFlagsBtn1 = 0x1000
ptrFlagsBtn2 = 0x2000
ptrFlagsBtn3 = 0x4000
ptrFlagsWheel = 0x0200
)
// InputSender 通过 Fast-Path Input 向远端发送键鼠事件 // Logan.Li
type InputSender struct {
w io.Writer
mu *sync.Mutex
}
func NewInputSender(w io.Writer) *InputSender {
return &InputSender{w: w}
}
func NewInputSenderWithLock(w io.Writer, mu *sync.Mutex) *InputSender {
return &InputSender{w: w, mu: mu}
}
func (s *InputSender) Send(events []byte) error {
if s.mu != nil {
s.mu.Lock()
defer s.mu.Unlock()
}
return WriteFastPathInput(s.w, events)
}
func (s *InputSender) Key(scancode int, down bool) error {
flags := byte(0)
if !down {
flags |= fastPathInputKBRelease
}
code := uint16(scancode)
if scancode&0xFF00 == 0xE000 {
flags |= fastPathInputKBExtended
code = uint16(scancode & 0xFF)
}
buf := []byte{byte(fastPathInputEventScancode<<5) | flags}
buf = append(buf, byte(code))
return s.Send(buf)
}
func (s *InputSender) MouseMove(x, y int) error {
return s.mouseEvent(ptrFlagsMove, x, y)
}
func (s *InputSender) MouseButton(x, y, button int, down bool) error {
flags := ptrFlagsDown
// 浏览器 MouseEvent.button 使用 0/1/2 表示左/中/右键,RDP BTN2 才是右键。 // Logan.Li
switch button {
case 2:
flags |= ptrFlagsBtn2
case 1:
flags |= ptrFlagsBtn3
default:
flags |= ptrFlagsBtn1
}
if !down {
flags &^= ptrFlagsDown
}
return s.mouseEvent(flags, x, y)
}
func (s *InputSender) MouseWheel(x, y, deltaY int) error {
scroll := -deltaY * 120 / 100
if scroll == 0 && deltaY != 0 {
if deltaY > 0 {
scroll = -120
} else {
scroll = 120
}
}
flags := uint16(ptrFlagsWheel | 0x0001)
if scroll < 0 {
flags |= 0x0100
scroll = -scroll
}
buf := []byte{byte(fastPathInputEventMouse << 5)}
buf = append(buf, byte(flags), byte(flags>>8))
buf = append(buf, byte(x), byte(x>>8), byte(y), byte(y>>8))
buf = append(buf, byte(scroll), byte(scroll>>8))
return s.Send(buf)
}
func (s *InputSender) mouseEvent(flags int, x, y int) error {
buf := []byte{byte(fastPathInputEventMouse << 5)}
buf = append(buf, byte(flags), byte(flags>>8))
buf = append(buf, byte(x), byte(x>>8), byte(y), byte(y>>8))
return s.Send(buf)
}
func (s *InputSender) TypeText(text string) error {
for _, r := range utf16.Encode([]rune(text)) {
if err := s.unicodeKey(uint16(r), true); err != nil {
return err
}
if err := s.unicodeKey(uint16(r), false); err != nil {
return err
}
time.Sleep(5 * time.Millisecond)
}
return nil
}
func (s *InputSender) unicodeKey(code uint16, down bool) error {
flags := byte(0)
if !down {
flags |= fastPathInputKBRelease
}
buf := []byte{byte(fastPathInputEventUnicode<<5) | flags, byte(code), byte(code >> 8)}
return s.Send(buf)
}
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 {
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 {
return err
}
if err := s.Key(0xE05B, false); err != nil {
return err
}
time.Sleep(200 * time.Millisecond)
if err := s.TypeText(command); err != nil {
return err
}
time.Sleep(80 * time.Millisecond)
return s.Key(0x001C, true)
}
+121
View File
@@ -0,0 +1,121 @@
package rdp
// ScanCodeFromJS 将浏览器 KeyboardEvent.code 转为 RDP 扫描码 // Logan.Li
func ScanCodeFromJS(code string) (int, bool) {
sc, ok := jsScanCodes[code]
return sc, ok
}
// jsScanCodes 将浏览器 KeyboardEvent.code 映射为 RDP 扫描码;扩展键使用 0xE0xx 约定 // Logan.Li
var jsScanCodes = map[string]int{
"Escape": 0x0001,
"Digit1": 0x0002,
"Digit2": 0x0003,
"Digit3": 0x0004,
"Digit4": 0x0005,
"Digit5": 0x0006,
"Digit6": 0x0007,
"Digit7": 0x0008,
"Digit8": 0x0009,
"Digit9": 0x000A,
"Digit0": 0x000B,
"Minus": 0x000C,
"Equal": 0x000D,
"Backspace": 0x000E,
"Tab": 0x000F,
"KeyQ": 0x0010,
"KeyW": 0x0011,
"KeyE": 0x0012,
"KeyR": 0x0013,
"KeyT": 0x0014,
"KeyY": 0x0015,
"KeyU": 0x0016,
"KeyI": 0x0017,
"KeyO": 0x0018,
"KeyP": 0x0019,
"BracketLeft": 0x001A,
"BracketRight": 0x001B,
"Enter": 0x001C,
"ControlLeft": 0x001D,
"KeyA": 0x001E,
"KeyS": 0x001F,
"KeyD": 0x0020,
"KeyF": 0x0021,
"KeyG": 0x0022,
"KeyH": 0x0023,
"KeyJ": 0x0024,
"KeyK": 0x0025,
"KeyL": 0x0026,
"Semicolon": 0x0027,
"Quote": 0x0028,
"Backquote": 0x0029,
"ShiftLeft": 0x002A,
"Backslash": 0x002B,
"KeyZ": 0x002C,
"KeyX": 0x002D,
"KeyC": 0x002E,
"KeyV": 0x002F,
"KeyB": 0x0030,
"KeyN": 0x0031,
"KeyM": 0x0032,
"Comma": 0x0033,
"Period": 0x0034,
"Slash": 0x0035,
"ShiftRight": 0x0036,
"NumpadMultiply": 0x0037,
"AltLeft": 0x0038,
"Space": 0x0039,
"CapsLock": 0x003A,
"F1": 0x003B,
"F2": 0x003C,
"F3": 0x003D,
"F4": 0x003E,
"F5": 0x003F,
"F6": 0x0040,
"F7": 0x0041,
"F8": 0x0042,
"F9": 0x0043,
"F10": 0x0044,
"ScrollLock": 0x0046,
"Numpad7": 0x0047,
"Numpad8": 0x0048,
"Numpad9": 0x0049,
"NumpadSubtract": 0x004A,
"Numpad4": 0x004B,
"Numpad5": 0x004C,
"Numpad6": 0x004D,
"NumpadAdd": 0x004E,
"Numpad1": 0x004F,
"Numpad2": 0x0050,
"Numpad3": 0x0051,
"Numpad0": 0x0052,
"NumpadDecimal": 0x0053,
"F11": 0x0057,
"F12": 0x0058,
"NumpadEqual": 0x0059,
"NumpadEnter": 0xE01C,
"ControlRight": 0xE01D,
"NumpadDivide": 0xE035,
"PrintScreen": 0xE037,
"AltRight": 0xE038,
"NumLock": 0xE045,
"Pause": 0xE046,
"Home": 0xE047,
"ArrowUp": 0xE048,
"PageUp": 0xE049,
"ArrowLeft": 0xE04B,
"ArrowRight": 0xE04D,
"End": 0xE04F,
"ArrowDown": 0xE050,
"PageDown": 0xE051,
"Insert": 0xE052,
"Delete": 0xE053,
"ContextMenu": 0xE05D,
"MetaLeft": 0xE05B,
"MetaRight": 0xE05C,
"IntlRo": 0x0073,
"KanaMode": 0x0070,
"IntlYen": 0x007D,
"Convert": 0x0079,
"NonConvert": 0x007B,
}
File diff suppressed because it is too large. Load diff
+125
View File
@@ -0,0 +1,125 @@
package rdp
// MPPC 解压器实现 RDP 64K 字典压缩(MS-RDPBCGR §3.1.8.4.1)// Logan.Li
const mppcHistorySize = 65536
const (
mppcBig = 0x01
mppcCompressed = 0x20
mppcReset = 0x40
mppcFlushed = 0x80
)
// MppcDecompressor 维护单连接 MPPC 历史窗口 // Logan.Li
type MppcDecompressor struct {
history [mppcHistorySize]byte
offset int
}
func NewMppcDecompressor() *MppcDecompressor {
return &MppcDecompressor{}
}
func (d *MppcDecompressor) Decompress(flags byte, data []byte) ([]byte, error) {
if flags&mppcFlushed != 0 {
d.history = [mppcHistorySize]byte{}
d.offset = 0
} else if flags&mppcReset != 0 {
d.offset = 0
}
if flags&mppcCompressed == 0 {
for _, b := range data {
d.history[d.offset] = b
d.offset = (d.offset + 1) & (mppcHistorySize - 1)
}
return data, nil
}
output := make([]byte, 0, len(data)*3)
br := newMppcBitReader(data)
outer:
for br.bitsLeft >= 9 {
if br.readBit() == 0 {
b := byte(br.readBits(8))
output = append(output, b)
d.history[d.offset] = b
d.offset = (d.offset + 1) & (mppcHistorySize - 1)
} else {
sel := (br.readBit() << 1) | br.readBit()
var copyOffset int
switch sel {
case 0:
if br.bitsLeft < 6 {
break outer
}
copyOffset = br.readBits(6)
case 1:
if br.bitsLeft < 8 {
break outer
}
copyOffset = 64 + br.readBits(8)
case 2:
if br.bitsLeft < 13 {
break outer
}
copyOffset = 320 + br.readBits(13)
case 3:
if br.bitsLeft < 16 {
break outer
}
copyOffset = 8192 + br.readBits(16)
}
copyLength := 3
for br.bitsLeft > 0 {
if br.readBit() == 0 {
break
}
copyLength++
}
src := (d.offset - copyOffset - 1 + mppcHistorySize) & (mppcHistorySize - 1)
for i := 0; i < copyLength; i++ {
b := d.history[(src+i)&(mppcHistorySize-1)]
output = append(output, b)
d.history[d.offset] = b
d.offset = (d.offset + 1) & (mppcHistorySize - 1)
}
}
}
return output, nil
}
type mppcBitReader struct {
data []byte
byteIdx int
mask byte
bitsLeft int
}
func newMppcBitReader(data []byte) *mppcBitReader {
return &mppcBitReader{data: data, mask: 0x80, bitsLeft: len(data) * 8}
}
func (r *mppcBitReader) readBit() int {
if r.bitsLeft == 0 {
return 0
}
r.bitsLeft--
bit := 0
if r.data[r.byteIdx]&r.mask != 0 {
bit = 1
}
r.mask >>= 1
if r.mask == 0 {
r.mask = 0x80
r.byteIdx++
}
return bit
}
func (r *mppcBitReader) readBits(n int) int {
result := 0
for i := 0; i < n; i++ {
result = (result << 1) | r.readBit()
}
return result
}
+77
View File
@@ -0,0 +1,77 @@
package rdp
import (
"encoding/binary"
"fmt"
"io"
"ezops/pkg/rdp/transport"
)
const (
fastPathActionFastPath = 0x0
fastPathActionX224 = 0x3
)
// WirePacket 表示从 RDP 连接读取到的一层帧 // Logan.Li
type WirePacket struct {
FastPath bool
SecFlag byte
Payload []byte
}
// ReadWirePacket 读取 TPKT/X.224 或 FastPath 输出帧 // Logan.Li
func ReadWirePacket(r io.Reader) (WirePacket, error) {
var hdr [2]byte
if _, err := io.ReadFull(r, hdr[:]); err != nil {
return WirePacket{}, err
}
version := hdr[0]
if version == fastPathActionX224 {
var extHdr [2]byte
if _, err := io.ReadFull(r, extHdr[:]); err != nil {
return WirePacket{}, err
}
size := binary.BigEndian.Uint16(extHdr[:])
if size < 4 {
return WirePacket{}, fmt.Errorf("TPKT 长度非法: %d", size)
}
body := make([]byte, int(size)-4)
if _, err := io.ReadFull(r, body); err != nil {
return WirePacket{}, err
}
if len(body) < 3 || body[1] != 0xF0 {
return WirePacket{}, fmt.Errorf("非 X.224 Data PDU")
}
return WirePacket{Payload: body[3:]}, nil
}
secFlag := (version >> 6) & 0x3
length := int(hdr[1])
packetSize := 0
if length&0x80 != 0 {
var extByte [1]byte
if _, err := io.ReadFull(r, extByte[:]); err != nil {
return WirePacket{}, err
}
packetSize = ((length &^ 0x80) << 8) + int(extByte[0]) - 3
} else {
packetSize = length - 2
}
if packetSize < 0 {
return WirePacket{FastPath: true, SecFlag: secFlag}, nil
}
body := make([]byte, packetSize)
if _, err := io.ReadFull(r, body); err != nil {
return WirePacket{}, err
}
return WirePacket{FastPath: true, SecFlag: secFlag, Payload: body}, nil
}
// WriteFastPathInput 发送 Fast-Path Input 事件批次 // Logan.Li
func WriteFastPathInput(w io.Writer, events []byte) error {
hdr := uint16(len(events)+3) | 0x8000
frame := make([]byte, 0, len(events)+3)
frame = append(frame, fastPathActionFastPath|0x04, byte(hdr>>8), byte(hdr))
frame = append(frame, events...)
return transport.WriteRaw(w, frame)
}
+255
View File
@@ -0,0 +1,255 @@
package rle
import (
"fmt"
"sync"
)
func decompress1(output *[]uint8, width, height int, input []uint8, size int) bool {
var (
prevline, line, count int
offset, code int
x int = width
opcode int
lastopcode int8 = -1
insertmix, bicolour, isfillormix bool
mixmask, mask uint8
colour1, colour2 uint8
mix uint8 = 0xff
fom_mask uint8
)
out := *output
for len(input) != 0 {
fom_mask = 0
code = CVAL(&input)
opcode = code >> 4
/* Handle different opcode forms */
switch opcode {
case 0xc, 0xd, 0xe:
opcode -= 6
count = int(code & 0xf)
offset = 16
break
case 0xf:
opcode = code & 0xf
if opcode < 9 {
count = int(CVAL(&input))
count |= int(CVAL(&input) << 8)
} else {
count = 1
if opcode < 0xb {
count = 8
}
}
offset = 0
break
default:
opcode >>= 1
count = int(code & 0x1f)
offset = 32
break
}
/* Handle strange cases for counts */
if offset != 0 {
isfillormix = ((opcode == 2) || (opcode == 7))
if count == 0 {
if isfillormix {
count = int(CVAL(&input)) + 1
} else {
count = int(CVAL(&input) + offset)
}
} else if isfillormix {
count <<= 3
}
}
/* Read preliminary data */
switch opcode {
case 0: /* Fill */
if (lastopcode == int8(opcode)) && !((x == width) && (prevline == 0)) {
insertmix = true
}
break
case 8: /* Bicolour */
colour1 = uint8(CVAL(&input))
colour2 = uint8(CVAL(&input))
break
case 3: /* Colour */
colour2 = uint8(CVAL(&input))
break
case 6: /* SetMix/Mix */
fallthrough
case 7: /* SetMix/FillOrMix */
mix = uint8(CVAL(&input))
opcode -= 5
break
case 9: /* FillOrMix_1 */
mask = 0x03
opcode = 0x02
fom_mask = 3
break
case 0x0a: /* FillOrMix_2 */
mask = 0x05
opcode = 0x02
fom_mask = 5
break
}
lastopcode = int8(opcode)
mixmask = 0
/* Output body */
for count > 0 {
if x >= width {
if height <= 0 {
return false
}
x = 0
height--
prevline = line
line = height * width
}
switch opcode {
case 0: /* Fill */
if insertmix {
if prevline == 0 {
out[x+line] = mix
} else {
out[x+line] = out[prevline+x] ^ mix
}
insertmix = false
count--
x++
}
n := min(count, width-x)
if prevline == 0 {
clear(out[x+line : x+line+n])
} else {
copy(out[x+line:x+line+n], out[prevline+x:prevline+x+n])
}
count -= n
x += n
break
case 1: /* Mix */
n := min(count, width-x)
if prevline == 0 {
seg := out[x+line : x+line+n]
for i := range seg {
seg[i] = mix
}
} else {
src := out[prevline+x : prevline+x+n]
dst := out[x+line : x+line+n]
for i := range dst {
dst[i] = src[i] ^ mix
}
}
count -= n
x += n
break
case 2: /* Fill or Mix */
if prevline == 0 {
for count > 0 && x < width {
mixmask <<= 1
if mixmask == 0 {
mask = fom_mask
if fom_mask == 0 {
mask = uint8(CVAL(&input))
mixmask = 1
}
}
if mask&mixmask != 0 {
out[x+line] = mix
} else {
out[x+line] = 0
}
count--
x++
}
} else {
for count > 0 && x < width {
mixmask = mixmask << 1
if mixmask == 0 {
mask = fom_mask
if fom_mask == 0 {
mask = uint8(CVAL(&input))
mixmask = 1
}
}
if mask&mixmask != 0 {
out[x+line] = out[prevline+x] ^ mix
} else {
out[x+line] = out[prevline+x]
}
count--
x++
}
}
break
case 3: /* Colour */
n := min(count, width-x)
seg := out[x+line : x+line+n]
for i := range seg {
seg[i] = colour2
}
count -= n
x += n
break
case 4: /* Copy */
for count > 0 && x < width {
n := min(count, width-x)
if len(input) < n {
return false
}
copy(out[x+line:x+line+n], input[:n])
input = input[n:]
count -= n
x += n
}
break
case 8: /* Bicolour */
for count > 0 && x < width {
if bicolour {
out[x+line] = colour2
bicolour = false
} else {
out[x+line] = colour1
bicolour = true
count++
}
count--
x++
}
break
case 0xd: /* White */
n := min(count, width-x)
seg := out[x+line : x+line+n]
for i := range seg {
seg[i] = 0xff
}
count -= n
x += n
break
case 0xe: /* Black */
n := min(count, width-x)
clear(out[x+line : x+line+n])
count -= n
x += n
break
default:
fmt.Printf("bitmap opcode 0x%x\n", opcode)
return false
}
}
}
return true
}
// decompress2Pool reuses the intermediate []uint16 buffer across calls to
// decompress2, avoiding a large per-frame allocation.
var decompress2Pool sync.Pool
/* 2 byte bitmap decompress */
+272
View File
@@ -0,0 +1,272 @@
package rle
import (
"fmt"
"unsafe"
)
func decompress2(output *[]uint8, width, height int, input []uint8, size int) bool {
needed := width * height
var out []uint16
if v := decompress2Pool.Get(); v != nil {
out = v.([]uint16)
if cap(out) < needed {
out = make([]uint16, needed)
} else {
out = out[:needed]
}
} else {
out = make([]uint16, needed)
}
defer func() { decompress2Pool.Put(out[:cap(out)]) }()
var (
prevline, line, count int
offset, code int
x int = width
opcode int
lastopcode int = -1
insertmix, bicolour, isfillormix bool
mixmask, mask uint8
colour1, colour2 uint16
mix uint16 = 0xffff
fom_mask uint8
)
for len(input) != 0 {
fom_mask = 0
code = CVAL(&input)
opcode = code >> 4
/* Handle different opcode forms */
switch opcode {
case 0xc, 0xd, 0xe:
opcode -= 6
count = code & 0xf
offset = 16
break
case 0xf:
opcode = code & 0xf
if opcode < 9 {
count = CVAL(&input)
count |= CVAL(&input) << 8
} else {
count = 1
if opcode < 0xb {
count = 8
}
}
offset = 0
break
default:
opcode >>= 1
count = code & 0x1f
offset = 32
break
}
/* Handle strange cases for counts */
if offset != 0 {
isfillormix = ((opcode == 2) || (opcode == 7))
if count == 0 {
if isfillormix {
count = CVAL(&input) + 1
} else {
count = CVAL(&input) + offset
}
} else if isfillormix {
count <<= 3
}
}
/* Read preliminary data */
switch opcode {
case 0: /* Fill */
if (lastopcode == opcode) && !((x == width) && (prevline == 0)) {
insertmix = true
}
break
case 8: /* Bicolour */
CVAL2(&input, &colour1)
CVAL2(&input, &colour2)
break
case 3: /* Colour */
CVAL2(&input, &colour2)
break
case 6: /* SetMix/Mix */
fallthrough
case 7: /* SetMix/FillOrMix */
CVAL2(&input, &mix)
opcode -= 5
break
case 9: /* FillOrMix_1 */
mask = 0x03
opcode = 0x02
fom_mask = 3
break
case 0x0a: /* FillOrMix_2 */
mask = 0x05
opcode = 0x02
fom_mask = 5
break
}
lastopcode = opcode
mixmask = 0
/* Output body */
for count > 0 {
if x >= width {
if height <= 0 {
return false
}
x = 0
height--
prevline = line
line = height * width
}
switch opcode {
case 0: /* Fill */
if insertmix {
if prevline == 0 {
out[x+line] = mix
} else {
out[x+line] = out[prevline+x] ^ mix
}
insertmix = false
count--
x++
}
n := min(count, width-x)
if prevline == 0 {
clear(out[x+line : x+line+n])
} else {
copy(out[x+line:x+line+n], out[prevline+x:prevline+x+n])
}
count -= n
x += n
break
case 1: /* Mix */
n := min(count, width-x)
if prevline == 0 {
seg := out[x+line : x+line+n]
for i := range seg {
seg[i] = mix
}
} else {
src := out[prevline+x : prevline+x+n]
dst := out[x+line : x+line+n]
for i := range dst {
dst[i] = src[i] ^ mix
}
}
count -= n
x += n
break
case 2: /* Fill or Mix */
if prevline == 0 {
for count > 0 && x < width {
mixmask <<= 1
if mixmask == 0 {
mask = fom_mask
if fom_mask == 0 {
mask = uint8(CVAL(&input))
mixmask = 1
}
}
if mask&mixmask != 0 {
out[x+line] = mix
} else {
out[x+line] = 0
}
count--
x++
}
} else {
for count > 0 && x < width {
mixmask = mixmask << 1
if mixmask == 0 {
mask = fom_mask
if fom_mask == 0 {
mask = uint8(CVAL(&input))
mixmask = 1
}
}
if mask&mixmask != 0 {
out[x+line] = out[prevline+x] ^ mix
} else {
out[x+line] = out[prevline+x]
}
count--
x++
}
}
break
case 3: /* Colour */
n := min(count, width-x)
seg := out[x+line : x+line+n]
for i := range seg {
seg[i] = colour2
}
count -= n
x += n
break
case 4: /* Copy */
for count > 0 && x < width {
n := min(count, width-x)
if len(input) < n*2 {
return false
}
copy(out[x+line:x+line+n], unsafe.Slice((*uint16)(unsafe.Pointer(&input[0])), n))
input = input[n*2:]
count -= n
x += n
}
break
case 8: /* Bicolour */
for count > 0 && x < width {
if bicolour {
out[x+line] = colour2
bicolour = false
} else {
out[x+line] = colour1
bicolour = true
count++
}
count--
x++
}
break
case 0xd: /* White */
n2 := min(count, width-x)
seg2 := out[x+line : x+line+n2]
for i := range seg2 {
seg2[i] = 0xffff
}
count -= n2
x += n2
break
case 0xe: /* Black */
n3 := min(count, width-x)
clear(out[x+line : x+line+n3])
count -= n3
x += n3
break
default:
fmt.Printf("bitmap opcode 0x%x\n", opcode)
return false
}
}
}
outBytes := *output
for i, v := range out {
outBytes[i*2] = byte(v >> 8)
outBytes[i*2+1] = byte(v)
}
return true
}
// /* 3 byte bitmap decompress */
+430
View File
@@ -0,0 +1,430 @@
package rle
import "fmt"
func decompress3(output *[]uint8, width, height int, input []uint8, size int) bool {
var (
prevline, line, count int
opcode, offset, code int
x int = width
lastopcode int = -1
insertmix, bicolour, isfillormix bool
mixmask, mask uint8
colour1 = [3]uint8{0, 0, 0}
colour2 = [3]uint8{0, 0, 0}
mix = [3]uint8{0xff, 0xff, 0xff}
fom_mask uint8
)
out := *output
for len(input) != 0 {
fom_mask = 0
code = CVAL(&input)
opcode = code >> 4
/* Handle different opcode forms */
switch opcode {
case 0xc, 0xd, 0xe:
opcode -= 6
count = code & 0xf
offset = 16
break
case 0xf:
opcode = code & 0xf
if opcode < 9 {
count = CVAL(&input)
count |= CVAL(&input) << 8
} else {
count = 1
if opcode < 0xb {
count = 8
}
}
offset = 0
break
default:
opcode >>= 1
count = code & 0x1f
offset = 32
break
}
/* Handle strange cases for counts */
if offset != 0 {
isfillormix = ((opcode == 2) || (opcode == 7))
if count == 0 {
if isfillormix {
count = CVAL(&input) + 1
} else {
count = CVAL(&input) + offset
}
} else if isfillormix {
count <<= 3
}
}
/* Read preliminary data */
switch opcode {
case 0: /* Fill */
if (lastopcode == opcode) && !((x == width) && (prevline == 0)) {
insertmix = true
}
break
case 8: /* Bicolour */
CVAL3(&input, &colour1)
CVAL3(&input, &colour2)
break
case 3: /* Colour */
CVAL3(&input, &colour2)
break
case 6: /* SetMix/Mix */
fallthrough
case 7: /* SetMix/FillOrMix */
CVAL3(&input, &mix)
opcode -= 5
break
case 9: /* FillOrMix_1 */
mask = 0x03
opcode = 0x02
fom_mask = 3
break
case 0x0a: /* FillOrMix_2 */
mask = 0x05
opcode = 0x02
fom_mask = 5
break
}
lastopcode = opcode
mixmask = 0
/* Output body */
for count > 0 {
if x >= width {
if height <= 0 {
return false
}
x = 0
height--
prevline = line
line = height * width * 3
}
switch opcode {
case 0: /* Fill */
if insertmix {
if prevline == 0 {
out[3*x+line] = mix[0]
out[3*x+line+1] = mix[1]
out[3*x+line+2] = mix[2]
} else {
out[3*x+line] = out[prevline+3*x] ^ mix[0]
out[3*x+line+1] = out[prevline+3*x+1] ^ mix[1]
out[3*x+line+2] = out[prevline+3*x+2] ^ mix[2]
}
insertmix = false
count--
x++
}
n := min(count, width-x)
if prevline == 0 {
clear(out[3*x+line : 3*x+line+3*n])
} else {
dstBase := 3*x + line
srcBase := prevline + 3*x
copy(out[dstBase:dstBase+3*n], out[srcBase:srcBase+3*n])
}
count -= n
x += n
break
case 1: /* Mix */
n := min(count, width-x)
dst1 := out[3*x+line : 3*x+line+3*n]
if prevline == 0 {
// Exponential-doubling copy: O(log n) memcpy calls
dst1[0], dst1[1], dst1[2] = mix[0], mix[1], mix[2]
for wrote := 3; wrote < len(dst1); {
wrote += copy(dst1[wrote:], dst1[:wrote])
}
} else {
src1 := out[prevline+3*x : prevline+3*x+3*n]
for i := 0; i+2 < len(dst1); i += 3 {
dst1[i] = src1[i] ^ mix[0]
dst1[i+1] = src1[i+1] ^ mix[1]
dst1[i+2] = src1[i+2] ^ mix[2]
}
}
count -= n
x += n
break
case 2: /* Fill or Mix */
if prevline == 0 {
base := 3*x + line
for count > 0 && x < width {
mixmask = mixmask << 1
if mixmask == 0 {
mask = fom_mask
if fom_mask == 0 {
mask = uint8(CVAL(&input))
mixmask = 1
}
}
if mask&mixmask != 0 {
out[base] = mix[0]
out[base+1] = mix[1]
out[base+2] = mix[2]
} else {
out[base] = 0
out[base+1] = 0
out[base+2] = 0
}
base += 3
count--
x++
}
} else {
base := 3*x + line
prev := prevline + 3*x
for count > 0 && x < width {
mixmask = mixmask << 1
if mixmask == 0 {
mask = fom_mask
if fom_mask == 0 {
mask = uint8(CVAL(&input))
mixmask = 1
}
}
if mask&mixmask != 0 {
out[base] = out[prev] ^ mix[0]
out[base+1] = out[prev+1] ^ mix[1]
out[base+2] = out[prev+2] ^ mix[2]
} else {
out[base] = out[prev]
out[base+1] = out[prev+1]
out[base+2] = out[prev+2]
}
base += 3
prev += 3
count--
x++
}
}
break
case 3: /* Colour */
n := min(count, width-x)
seg3 := out[3*x+line : 3*x+line+3*n]
seg3[0], seg3[1], seg3[2] = colour2[0], colour2[1], colour2[2]
for wrote := 3; wrote < len(seg3); {
wrote += copy(seg3[wrote:], seg3[:wrote])
}
count -= n
x += n
break
case 4: /* Copy */
for count > 0 && x < width {
n := min(count, width-x)
if len(input) < n*3 {
return false
}
copy(out[3*x+line:3*x+line+n*3], input[:n*3])
input = input[n*3:]
count -= n
x += n
}
break
case 8: /* Bicolour */
base8 := 3*x + line
for count > 0 && x < width {
if bicolour {
out[base8] = colour2[0]
out[base8+1] = colour2[1]
out[base8+2] = colour2[2]
bicolour = false
} else {
out[base8] = colour1[0]
out[base8+1] = colour1[1]
out[base8+2] = colour1[2]
bicolour = true
count++
}
base8 += 3
count--
x++
}
break
case 0xd: /* White */
n3 := min(count, width-x)
seg3 := out[3*x+line : 3*x+line+3*n3]
for i := range seg3 {
seg3[i] = 0xff
}
count -= n3
x += n3
break
case 0xe: /* Black */
n2 := min(count, width-x)
clear(out[3*x+line : 3*x+line+3*n2])
count -= n2
x += n2
break
default:
fmt.Printf("bitmap opcode 0x%x\n", opcode)
return false
}
}
}
return true
}
/* decompress a colour plane */
func processPlane(in *[]uint8, width, height int, output *[]uint8, j int) (int, bool) {
var (
indexw int
indexh int
code int
collen int
replen int
color uint8
x uint8
revcode int
lastline int
thisline int
)
ln := len(*in)
out := *output // hoist pointer dereference; writes to out[i] affect the underlying array
readByte := func() (uint8, bool) {
if len(*in) == 0 {
return 0, false
}
v := (*in)[0]
*in = (*in)[1:]
return v, true
}
writePixel := func(pos int, value uint8) bool {
if pos < 0 || pos >= len(out) {
return false
}
out[pos] = value
return true
}
lastline = 0
indexh = 0
i := 0
for indexh < height {
thisline = j + (width * height * 4) - ((indexh + 1) * width * 4)
color = 0
indexw = 0
i = thisline
if lastline == 0 {
for indexw < width {
b, ok := readByte()
if !ok {
return ln - len(*in), false
}
code = int(b)
replen = int(code & 0xf)
collen = int((code >> 4) & 0xf)
revcode = (replen << 4) | collen
if (revcode <= 47) && (revcode >= 16) {
replen = revcode
collen = 0
}
for collen > 0 {
if indexw >= width {
return ln - len(*in), false
}
v, ok := readByte()
if !ok {
return ln - len(*in), false
}
color = v
if !writePixel(i, color) {
return ln - len(*in), false
}
i += 4
indexw++
collen--
}
for replen > 0 {
if indexw >= width {
break
}
if !writePixel(i, color) {
return ln - len(*in), false
}
i += 4
indexw++
replen--
}
}
} else {
// prevOffset is constant per row: indexw*4+lastline == i+(lastline-thisline)
// because i == thisline+indexw*4. Pre-computing it eliminates a multiply
// per pixel in both inner loops.
prevOffset := lastline - thisline
for indexw < width {
b, ok := readByte()
if !ok {
return ln - len(*in), false
}
code = int(b)
replen = int(code & 0xf)
collen = int((code >> 4) & 0xf)
revcode = (replen << 4) | collen
if (revcode <= 47) && (revcode >= 16) {
replen = revcode
collen = 0
}
for collen > 0 {
if indexw >= width {
return ln - len(*in), false
}
v, ok := readByte()
if !ok {
return ln - len(*in), false
}
x = v
if x&1 != 0 {
x = x >> 1
x = x + 1
color = -x
} else {
x = x >> 1
color = x
}
if i+prevOffset < 0 || i+prevOffset >= len(out) {
return ln - len(*in), false
}
x = out[i+prevOffset] + color
if !writePixel(i, x) {
return ln - len(*in), false
}
i += 4
indexw++
collen--
}
for replen > 0 {
if indexw >= width {
break
}
if i+prevOffset < 0 || i+prevOffset >= len(out) {
return ln - len(*in), false
}
x = out[i+prevOffset] + color
if !writePixel(i, x) {
return ln - len(*in), false
}
i += 4
indexw++
replen--
}
}
}
indexh++
lastline = thisline
}
return ln - len(*in), true
}
/* 4 byte bitmap decompress */
+58
View File
@@ -0,0 +1,58 @@
package rle
func decompress4(output *[]uint8, width, height int, input []uint8, size int) bool {
var (
code int
onceBytes, total int
)
code = CVAL(&input)
rle := code&0x10 != 0
noAlpha := code&0x20 != 0
if !rle {
return false
}
total = 1
out := *output
if noAlpha {
// No alpha plane in the stream; fill alpha channel with 0xFF.
for i := 3; i < len(out); i += 4 {
out[i] = 0xFF
}
} else {
var ok bool
onceBytes, ok = processPlane(&input, width, height, output, 3)
if !ok {
return false
}
total += onceBytes
}
var ok bool
onceBytes, ok = processPlane(&input, width, height, output, 2)
if !ok {
return false
}
total += onceBytes
onceBytes, ok = processPlane(&input, width, height, output, 1)
if !ok {
return false
}
total += onceBytes
onceBytes, ok = processPlane(&input, width, height, output, 0)
if !ok {
return false
}
total += onceBytes
return true
}
// DecompressInto decompresses bitmap data into dst, reusing dst if it has
// sufficient capacity (size = width*height*bpp). If dst is nil or too small
// a new slice is allocated. Returns the (re)used output slice.
+30
View File
@@ -0,0 +1,30 @@
package rle
import "fmt"
func DecompressInto(input []uint8, dst []uint8, width, height int, bpp int) []uint8 {
size := width * height * bpp
if cap(dst) >= size {
dst = dst[:size]
} else {
dst = make([]uint8, size)
}
ok := true
switch bpp {
case 1:
ok = decompress1(&dst, width, height, input, size)
case 2:
ok = decompress2(&dst, width, height, input, size)
case 3:
ok = decompress3(&dst, width, height, input, size)
case 4:
ok = decompress4(&dst, width, height, input, size)
default:
fmt.Printf("bpp %d\n", bpp)
ok = false
}
if !ok {
return nil
}
return dst
}
+29
View File
@@ -0,0 +1,29 @@
package rle
import "unsafe"
func CVAL(p *[]uint8) int {
a := int((*p)[0])
*p = (*p)[1:]
return a
}
func CVAL2(p *[]uint8, v *uint16) {
*v = *((*uint16)(unsafe.Pointer(&(*p)[0])))
*p = (*p)[2:]
}
func CVAL3(p *[]uint8, v *[3]uint8) {
(*v)[0] = (*p)[0]
(*v)[1] = (*p)[1]
(*v)[2] = (*p)[2]
*p = (*p)[3:]
}
func REPEAT(f func(), count *int, x *int, width int) {
for *count > 0 && *x < width {
f()
*count--
*x++
}
}
+169
View File
@@ -0,0 +1,169 @@
package rdp
import (
"context"
"fmt"
"io"
"os"
"strings"
"time"
"ezops/pkg/rdp/credssp"
"ezops/pkg/rdp/transport"
)
// Session 保存已建立的 RDP 图形会话状态 // Logan.Li
type Session struct {
Conn io.ReadWriter
UserID uint16
SharedID uint32
Width int
Height int
}
// OpenSession 完成 CredSSP + MCS/GCC 握手并进入可收帧状态 // Logan.Li
func OpenSession(ctx context.Context, cfg Config) (*Session, error) {
port := cfg.Port
if port == 0 {
port = 3389
}
if cfg.MCSMode == "" {
cfg.MCSMode = strings.TrimSpace(os.Getenv("RDP_MCS_MODE"))
}
if cfg.MCSMode == "" {
cfg.MCSMode = "grdp-modern"
}
address := fmt.Sprintf("%s:%d", cfg.Host, port)
strategies := []credssp.Options{
{Version: 6, PubKeyStyle: "binding"},
{Version: 6, PubKeyStyle: "binding", UseCBT: true},
{Version: 6, PubKeyStyle: "binding", ForceMIC: true},
{Version: 2, PubKeyStyle: "raw"},
}
var lastErr error
for _, opts := range strategies {
sess, err := openSessionOnce(ctx, cfg, address, opts)
if err == nil {
return sess, nil
}
lastErr = err
}
if lastErr == nil {
lastErr = fmt.Errorf("RDP 会话建立失败")
}
return nil, lastErr
}
func openSessionOnce(ctx context.Context, cfg Config, address string, opts credssp.Options) (*Session, error) {
tlsConn, _, err := dialCredSSPOnce(ctx, cfg, address, opts)
if err != nil {
return nil, err
}
_ = tlsConn.SetDeadline(time.Now().Add(45 * time.Second))
if err := transport.WriteX224Data(tlsConn, buildMCSConnectInitial(cfg)); err != nil {
_ = tlsConn.Close()
return nil, err
}
payload, err := transport.ReadX224Data(tlsConn)
if err != nil {
_ = tlsConn.Close()
return nil, err
}
if _, err := parseMCSConnectResponse(payload); err != nil {
_ = tlsConn.Close()
return nil, err
}
userID, err := attachMCSUser(tlsConn)
if err != nil {
_ = tlsConn.Close()
return nil, err
}
mcsInfo, _ := parseMCSConnectResponse(payload)
joinIDs := append([]uint16{mcsGlobalChannelID, userID}, mcsInfo.ServerChannelIDs...)
for _, channelID := range joinIDs {
if _, err := joinMCSChannel(tlsConn, userID, channelID); err != nil {
_ = tlsConn.Close()
return nil, err
}
}
_, demand, err := sendClientInfoAndReadDemand(tlsConn, cfg, userID)
if err != nil {
_ = tlsConn.Close()
return nil, err
}
if _, err := sendConfirmActiveAndFinalize(tlsConn, cfg, userID, demand); err != nil {
_ = tlsConn.Close()
return nil, err
}
if err := requestDisplayUpdates(tlsConn, cfg, userID, demand.sharedID); err != nil {
_ = tlsConn.Close()
return nil, err
}
width := cfg.Width
if width <= 0 {
width = 1280
}
height := cfg.Height
if height <= 0 {
height = 800
}
_ = tlsConn.SetDeadline(time.Time{})
return &Session{
Conn: tlsConn,
UserID: userID,
SharedID: demand.sharedID,
Width: width,
Height: height,
}, nil
}
func requestDisplayUpdates(rw io.ReadWriter, cfg Config, userID uint16, sharedID uint32) error {
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)
}
}
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 nil
}
func buildSuppressOutputBody(cfg Config, allow bool) []byte {
width := cfg.Width
if width <= 0 {
width = 1280
}
height := cfg.Height
if height <= 0 {
height = 800
}
out := make([]byte, 0, 12)
if allow {
out = append(out, 1, 0, 0, 0)
} else {
out = append(out, 0, 0, 0, 0)
}
out = append(out, 0, 0, 0, 0, byte(width-1), byte((width-1)>>8), byte(height-1), byte((height-1)>>8))
return out
}
func buildRefreshRectBody(cfg Config) []byte {
width := cfg.Width
if width <= 0 {
width = 1280
}
height := cfg.Height
if height <= 0 {
height = 800
}
out := make([]byte, 0, 10)
out = append(out, 1, 0)
out = append(out, 0, 0, 0, 0, byte(width-1), byte((width-1)>>8), byte(height-1), byte((height-1)>>8))
return out
}
+295
View File
@@ -0,0 +1,295 @@
package rdp
import (
"context"
"fmt"
"io"
"sync"
"time"
"ezops/pkg/rdp/display"
"ezops/pkg/rdp/trace"
"ezops/pkg/rdp/transport"
)
// SessionEngine 是自研完整 RDP 图形会话引擎 // Logan.Li
type SessionEngine struct{}
func (e *SessionEngine) Run(ctx context.Context, cfg Config, sink FrameSink, events <-chan ClientEvent) error {
if cfg.Port == 0 {
cfg.Port = 3389
}
trace.Logf("engine=rdp connect target=%s:%d size=%dx%d security=%s", cfg.Host, cfg.Port, cfg.Width, cfg.Height, cfg.Security)
_ = sink.SendFrame(ServerFrame{
Type: "status",
Message: fmt.Sprintf("正在连接 RDP %s:%d", cfg.Host, cfg.Port),
})
sess, err := OpenSession(ctx, cfg)
if err != nil {
trace.Logf("engine=rdp open failed target=%s:%d err=%v", cfg.Host, cfg.Port, err)
_ = sink.SendFrame(ServerFrame{Type: "error", Message: "RDP 会话建立失败:" + err.Error()})
return err
}
defer func() {
if c, ok := sess.Conn.(io.Closer); ok {
_ = c.Close()
}
}()
_ = sink.SendFrame(ServerFrame{Type: "status", Message: "RDP 图形会话已建立,等待桌面画面..."})
trace.Logf("engine=rdp open ok target=%s:%d", cfg.Host, cfg.Port)
writeMu := &sync.Mutex{}
input := NewInputSenderWithLock(sess.Conn, writeMu)
receiver := NewFastPathReceiver()
readErr := make(chan error, 1)
go func() {
readErr <- e.readLoop(ctx, sess, receiver, sink, writeMu)
}()
go e.inputLoop(ctx, input, sink, events)
for {
select {
case <-ctx.Done():
return ctx.Err()
case err := <-readErr:
if err != nil {
_ = sink.SendFrame(ServerFrame{Type: "error", Message: err.Error()})
}
return err
}
}
}
func (e *SessionEngine) inputLoop(ctx context.Context, input *InputSender, sink FrameSink, events <-chan ClientEvent) {
for {
select {
case <-ctx.Done():
return
case event, ok := <-events:
if !ok {
return
}
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 {
_ = 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 {
framebuffer := display.NewFramebuffer(sess.Width, sess.Height)
ticker := time.NewTicker(16 * time.Millisecond)
defer ticker.Stop()
refreshRequested := false
flushFrame := func() {
frame, ok := framebuffer.FlushRGBA()
if !ok {
return
}
_ = sink.SendFrame(ServerFrame{
Type: "bitmap",
X: frame.X,
Y: frame.Y,
Width: frame.Width,
Height: frame.Height,
RGBA: frame.RGBA,
})
}
sendBitmaps := func(rects []BitmapRect) {
for _, rect := range rects {
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)
}
}()
}
framebuffer.Draw(display.Bitmap{
X: rect.DestLeft,
Y: rect.DestTop,
Width: rect.Width,
Height: rect.Height,
BitsPerPixel: rect.BitsPerPixel,
Data: rect.Data,
})
}
}
type packetResult struct {
packet WirePacket
err error
}
packetCh := make(chan packetResult, 1)
go func() {
for {
pkt, err := ReadWirePacket(sess.Conn)
select {
case packetCh <- packetResult{packet: pkt, err: err}:
case <-ctx.Done():
return
}
if err != nil {
return
}
}
}()
for {
select {
case <-ctx.Done():
flushFrame()
return ctx.Err()
case <-ticker.C:
flushFrame()
case got := <-packetCh:
if got.err != nil {
flushFrame()
return got.err
}
var rects []BitmapRect
var err error
if got.packet.FastPath {
rects, err = receiver.Handle(got.packet.SecFlag, got.packet.Payload)
} else {
rects, err = receiver.HandleSlowPath(got.packet.Payload)
}
if err != nil {
continue
}
if err := e.ackSurfaceFrames(sess, receiver.TakeFrameIDs(), writeMu); err != nil {
trace.Logf("engine=rdp frame ack failed err=%v", err)
}
if len(rects) > 0 {
trace.Logf("engine=rdp bitmap rects=%d fastpath=%t", len(rects), got.packet.FastPath)
sendBitmaps(rects)
}
}
}
}
func isBlackFullScreen(rect BitmapRect, width, height int) bool {
if rect.DestLeft != 0 || rect.DestTop != 0 || rect.Width < width || rect.Height < height {
return false
}
limit := len(rect.Data)
if limit > 256*1024 {
limit = 256 * 1024
}
for i := 0; i < limit; i++ {
if rect.Data[i] > 4 {
return false
}
}
return limit > 0
}
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)
trace.Logf("engine=rdp refresh rect %dx%d", sess.Width, sess.Height)
writeMu.Lock()
err := transport.WriteX224Data(sess.Conn, buildMCSSendDataRequest(sess.UserID, mcsGlobalChannelID, pdu))
writeMu.Unlock()
return err
}
func (e *SessionEngine) ackSurfaceFrames(sess *Session, frameIDs []uint32, writeMu *sync.Mutex) error {
for _, frameID := range frameIDs {
body := []byte{byte(frameID), byte(frameID >> 8), byte(frameID >> 16), byte(frameID >> 24)}
pdu := buildDataPDU(sess.UserID, sess.SharedID, pduType2FrameAcknowledge, body)
trace.Logf("engine=rdp frame ack frameID=%d", frameID)
writeMu.Lock()
err := transport.WriteX224Data(sess.Conn, buildMCSSendDataRequest(sess.UserID, mcsGlobalChannelID, pdu))
writeMu.Unlock()
if err != nil {
return err
}
}
return nil
}
func (e *SessionEngine) handleEvent(input *InputSender, sink FrameSink, event ClientEvent) error {
switch event.Type {
case "mousemove", "mouse_move":
return input.MouseMove(event.X, event.Y)
case "mousedown", "mouse_down":
return input.MouseButton(event.X, event.Y, event.Button, true)
case "mouseup", "mouse_up":
return input.MouseButton(event.X, event.Y, event.Button, false)
case "wheel":
return input.MouseWheel(event.X, event.Y, event.DeltaY)
case "key":
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)
}
return nil
case "auto_exec":
if event.Text == "" {
return nil
}
_ = sink.SendFrame(ServerFrame{Type: "status", Message: "正在远端执行:" + event.Text})
return input.AutoExec(event.Text)
case "resize":
_ = sink.SendFrame(ServerFrame{Type: "status", Message: "分辨率变更请求已记录,重连后生效"})
return nil
default:
return nil
}
}
// ProbeSessionFrames 用于诊断:建立会话后读取若干秒位图帧数量 // Logan.Li
func ProbeSessionFrames(ctx context.Context, cfg Config, seconds int) (int, error) {
sess, err := OpenSession(ctx, cfg)
if err != nil {
return 0, err
}
defer func() {
if c, ok := sess.Conn.(io.Closer); ok {
_ = c.Close()
}
}()
receiver := NewFastPathReceiver()
deadline := time.Now().Add(time.Duration(seconds) * time.Second)
count := 0
for time.Now().Before(deadline) {
if c, ok := sess.Conn.(interface{ SetReadDeadline(time.Time) error }); ok {
_ = c.SetReadDeadline(time.Now().Add(500 * time.Millisecond))
}
pkt, err := ReadWirePacket(sess.Conn)
if err != nil {
if time.Now().Before(deadline) {
continue
}
return count, nil
}
var rects []BitmapRect
if pkt.FastPath {
rects, err = receiver.Handle(pkt.SecFlag, pkt.Payload)
} else {
rects, err = receiver.HandleSlowPath(pkt.Payload)
}
if err == nil {
count += len(rects)
}
}
if c, ok := sess.Conn.(interface{ SetReadDeadline(time.Time) error }); ok {
_ = c.SetReadDeadline(time.Time{})
}
return count, nil
}
+21
View File
@@ -0,0 +1,21 @@
package trace
import (
"log"
"os"
"strings"
)
// Enabled 判断是否开启 RDP 协议级调试日志,默认关闭避免高频帧刷屏 // Logan.Li
func Enabled() bool {
v := strings.ToLower(strings.TrimSpace(os.Getenv("RDP_DEBUG")))
return v == "1" || v == "true" || v == "yes" || v == "debug"
}
// Logf 输出 RDP 调试日志,统一前缀便于 docker logs 过滤 // Logan.Li
func Logf(format string, args ...interface{}) {
if !Enabled() {
return
}
log.Printf("[RDP:TRACE] "+format, args...)
}
+39
View File
@@ -0,0 +1,39 @@
package transport
import (
"crypto/tls"
"fmt"
"net"
"time"
)
// UpgradeTLS 在 X.224 协商后升级为 TLS,RDP 自签证书由堡垒机侧 TOFU 处理 // Logan.Li
func UpgradeTLS(conn net.Conn, serverName string, timeout time.Duration) (*tls.Conn, error) {
_ = conn.SetDeadline(time.Now().Add(timeout))
tlsConn := tls.Client(conn, &tls.Config{
ServerName: serverName,
InsecureSkipVerify: true, // RDP 常见自签证书,后续可落库校验指纹 // Logan.Li
MinVersion: tls.VersionTLS10,
MaxVersion: tls.VersionTLS12,
})
if err := tlsConn.Handshake(); err != nil {
return nil, err
}
_ = tlsConn.SetDeadline(time.Time{})
return tlsConn, nil
}
// TLSCipherSuiteName 返回 TLS cipher suite 名称,旧版本未命名时回退十六进制 // Logan.Li
func TLSCipherSuiteName(id uint16) string {
for _, suite := range tls.CipherSuites() {
if suite.ID == id {
return suite.Name
}
}
for _, suite := range tls.InsecureCipherSuites() {
if suite.ID == id {
return suite.Name
}
}
return fmt.Sprintf("0x%04x", id)
}
+54
View File
@@ -0,0 +1,54 @@
package transport
import (
"encoding/binary"
"fmt"
"io"
)
const (
tpktVersion = 3
tpktHeaderSize = 4
maxTPKTSize = 64 * 1024
)
// WriteTPKT 写入 TPKT 包头与负载,RDP 初始握手基于 TPKT 承载 // Logan.Li
func WriteTPKT(w io.Writer, payload []byte) error {
total := len(payload) + tpktHeaderSize
if total > maxTPKTSize {
return fmt.Errorf("TPKT payload too large: %d", len(payload))
}
header := []byte{tpktVersion, 0, 0, 0}
binary.BigEndian.PutUint16(header[2:4], uint16(total))
if _, err := w.Write(header); err != nil {
return err
}
_, err := w.Write(payload)
return err
}
// ReadTPKT 读取一个完整 TPKT 包并返回负载 // Logan.Li
func ReadTPKT(r io.Reader) ([]byte, error) {
header := make([]byte, tpktHeaderSize)
if _, err := io.ReadFull(r, header); err != nil {
return nil, err
}
if header[0] != tpktVersion {
return nil, fmt.Errorf("invalid TPKT version: %d", header[0])
}
total := int(binary.BigEndian.Uint16(header[2:4]))
if total < tpktHeaderSize || total > maxTPKTSize {
return nil, fmt.Errorf("invalid TPKT length: %d", total)
}
payload := make([]byte, total-tpktHeaderSize)
if _, err := io.ReadFull(r, payload); err != nil {
return nil, err
}
return payload, nil
}
// WriteRaw 直接写入已封装的 TPKT/FastPath 帧,供会话层发送输入事件 // Logan.Li
func WriteRaw(w io.Writer, frame []byte) error {
_, err := w.Write(frame)
return err
}
+119
View File
@@ -0,0 +1,119 @@
package transport
import (
"encoding/binary"
"fmt"
"io"
)
const (
x224TypeConnectionRequest = 0xE0
x224TypeConnectionConfirm = 0xD0
x224TypeData = 0xF0
negTypeRequest = 0x01
negTypeResponse = 0x02
negTypeFailure = 0x03
protocolSSL = 0x00000001
protocolCredSSP = 0x00000002
)
const (
ProtocolStandard uint32 = 0
ProtocolTLS uint32 = protocolSSL
ProtocolCredSSP uint32 = protocolCredSSP
)
// NegotiationResult 是 X.224 阶段的 RDP 安全协议协商结果 // Logan.Li
type NegotiationResult struct {
SelectedProtocol uint32
FailureCode uint32
}
func (r NegotiationResult) ProtocolName() string {
switch r.SelectedProtocol {
case ProtocolCredSSP:
return "CredSSP/NLA"
case ProtocolTLS:
return "TLS"
case ProtocolStandard:
return "Standard RDP"
default:
return fmt.Sprintf("unknown(0x%x)", r.SelectedProtocol)
}
}
// BuildConnectionRequest 构造 X.224 Connection Request,并请求 NLA/TLS 协商 // Logan.Li
func BuildConnectionRequest() []byte {
return BuildConnectionRequestWithCookie("aiops")
}
// BuildConnectionRequestWithCookie 按 MS-RDPBCGR 2.2.1.1.1 构造包含 mstshash 的请求 // Logan.Li
func BuildConnectionRequestWithCookie(username string) []byte {
if username == "" {
username = "aiops"
}
if len(username) > 9 {
username = username[:9]
}
cookie := []byte("Cookie: mstshash=" + username + "\r\n")
neg := make([]byte, 8)
neg[0] = negTypeRequest
neg[1] = 0
binary.LittleEndian.PutUint16(neg[2:4], uint16(len(neg)))
binary.LittleEndian.PutUint32(neg[4:8], protocolSSL|protocolCredSSP)
length := 7 + len(cookie) + len(neg)
pdu := make([]byte, 0, length)
pdu = append(pdu, byte(length-1))
pdu = append(pdu, x224TypeConnectionRequest)
pdu = append(pdu, 0x00, 0x00) // dst-ref // Logan.Li
pdu = append(pdu, 0x00, 0x00) // src-ref // Logan.Li
pdu = append(pdu, 0x00) // class/options // Logan.Li
pdu = append(pdu, cookie...)
pdu = append(pdu, neg...)
return pdu
}
// ParseConnectionConfirm 解析 X.224 Connection Confirm 与 RDP Negotiation Response // Logan.Li
func ParseConnectionConfirm(payload []byte) (NegotiationResult, error) {
if len(payload) < 7 {
return NegotiationResult{}, fmt.Errorf("short X.224 confirm: %d", len(payload))
}
if payload[1] != x224TypeConnectionConfirm {
return NegotiationResult{}, fmt.Errorf("unexpected X.224 pdu type: 0x%x", payload[1])
}
for i := 7; i+8 <= len(payload); i++ {
switch payload[i] {
case negTypeResponse:
return NegotiationResult{SelectedProtocol: binary.LittleEndian.Uint32(payload[i+4 : i+8])}, nil
case negTypeFailure:
return NegotiationResult{FailureCode: binary.LittleEndian.Uint32(payload[i+4 : i+8])}, fmt.Errorf("RDP negotiation failure: 0x%x", binary.LittleEndian.Uint32(payload[i+4:i+8]))
}
}
return NegotiationResult{SelectedProtocol: 0}, nil
}
// WriteX224Data 写入 X.224 Data TPDU,NLA 后的 MCS/GCC 数据仍经由该层承载 // Logan.Li
func WriteX224Data(w io.Writer, payload []byte) error {
data := make([]byte, 0, len(payload)+3)
data = append(data, 0x02, x224TypeData, 0x80)
data = append(data, payload...)
return WriteTPKT(w, data)
}
// ReadX224Data 读取并剥离 X.224 Data TPDU 头,返回上层 MCS 负载 // Logan.Li
func ReadX224Data(r io.Reader) ([]byte, error) {
payload, err := ReadTPKT(r)
if err != nil {
return nil, err
}
if len(payload) < 3 {
return nil, fmt.Errorf("X.224 data too short: %d", len(payload))
}
if payload[1] != x224TypeData {
return nil, fmt.Errorf("unexpected X.224 type: 0x%x", payload[1])
}
return payload[3:], nil
}