主要变更: - 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>
298 lines
6.8 KiB
Go
298 lines
6.8 KiB
Go
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
|
|
}
|