Fix(Go): stable device list ordering + RDP-reset handler

Fix UTF-8 login text decode + stale screen sub-conn retirement
This commit is contained in:
yuanyuanxiang
2026-05-19 16:28:01 +02:00
parent 5af017bf09
commit d757c33bcb
3 changed files with 167 additions and 87 deletions

View File

@@ -109,6 +109,7 @@ const (
CommandNext byte = 30 // COMMAND_NEXT - "control-side dialog is open, you may stream"
CommandShell byte = 40 // COMMAND_SHELL - ask device to open a shell sub-connection
CommandTerminalRsize byte = 81 // CMD_TERMINAL_RESIZE - [cmd:1][cols:2 LE][rows:2 LE]
CmdRestoreConsole byte = 82 // CMD_RESTORE_CONSOLE - RDP session "归位": switch back to the console session and restart capture
CommandBye byte = 204 // COMMAND_BYE - disconnect
CommandHeartbeat byte = 216 // CMD_HEARTBEAT_ACK
@@ -382,7 +383,22 @@ type LoginInfo struct {
Reserved string // Contains additional info separated by |
}
// ParseLoginInfo parses LOGIN_INFOR from data
// ParseLoginInfo parses LOGIN_INFOR from data.
//
// Encoding: text fields are GBK on legacy Windows clients and UTF-8 on modern
// clients that set CLIENT_CAP_UTF8 (always on for LNX / MAC). Picking the
// wrong codec mangles non-ASCII characters — e.g. a German location string
// "Nürnberg" sent as UTF-8 (4E C3 BC 72 ...) and force-decoded as GBK turns
// into mojibake. The heartbeat path already honors this via DecodeClientString
// (see cmd/main.go handleHeartbeat); ParseLoginInfo previously did not, so
// every login string from a UTF-8 client was being misread.
//
// To get encoding right we have a chicken-and-egg problem: capability lives
// in ModuleVersion (offset 164) and clientType lives in Reserved field 0
// (offset 476) — but Reserved itself needs that information to decode. Both
// "discriminator" values are pure ASCII (hex digits, "Windows"/"LNX"/"MAC"),
// so we can extract them with a UTF-8 read and then re-decode the actual
// user-text fields with the correct codec.
func ParseLoginInfo(data []byte) (*LoginInfo, error) {
if len(data) < 100 { // Minimum size check
return nil, ErrInvalidData
@@ -392,64 +408,61 @@ func ParseLoginInfo(data []byte) (*LoginInfo, error) {
Token: data[0],
}
// Parse OS version info (offset 1, 156 bytes)
// The C++ client fills this with a readable string like "Windows 10" via getSystemName()
if len(data) >= OffsetOsVerInfoEx+156 {
info.OsVerInfo = parseOsVersionInfo(data[OffsetOsVerInfoEx : OffsetOsVerInfoEx+156])
}
// Parse CPU MHz (offset 160, 4 bytes)
// CPU MHz, WebCam, Speed — fixed-width binary, encoding-independent.
if len(data) >= OffsetCPUMHz+4 {
info.CPUMHz = binary.LittleEndian.Uint32(data[OffsetCPUMHz:])
}
// Parse module version (offset 164, 24 bytes)
// This contains date string like "Dec 19 2025"
if len(data) >= OffsetModuleVersion+24 {
info.ModuleVersion = GbkToUTF8(data[OffsetModuleVersion : OffsetModuleVersion+24])
}
// Parse PC name (offset 188, 240 bytes)
if len(data) >= OffsetPCName+240 {
info.PCName = GbkToUTF8(data[OffsetPCName : OffsetPCName+240])
}
// Parse Master ID (offset 428, 20 bytes)
if len(data) >= OffsetMasterID+20 {
info.MasterID = GbkToUTF8(data[OffsetMasterID : OffsetMasterID+20])
}
// Parse WebCam exist (offset 448, 4 bytes)
if len(data) >= OffsetWebCamExist+4 {
info.WebCamExist = binary.LittleEndian.Uint32(data[OffsetWebCamExist:]) != 0
}
// Parse Speed (offset 452, 4 bytes)
if len(data) >= OffsetSpeed+4 {
info.Speed = binary.LittleEndian.Uint32(data[OffsetSpeed:])
}
// Parse Start time (offset 456, 20 bytes)
if len(data) >= OffsetStartTime+20 {
info.StartTime = GbkToUTF8(data[OffsetStartTime : OffsetStartTime+20])
// ModuleVersion is "version-capabilityHex" — pure ASCII (e.g. "Dec 19
// 2025-0006"). Safe to read as UTF-8 regardless of client codec.
if len(data) >= OffsetModuleVersion+24 {
info.ModuleVersion = Utf8CleanString(data[OffsetModuleVersion : OffsetModuleVersion+24])
}
_, capability, _ := strings.Cut(info.ModuleVersion, "-")
// Peek at Reserved field 0 (RES_CLIENT_TYPE: "Windows" / "LNX" / "MAC")
// — pure ASCII, so we can read raw bytes without knowing the codec.
// LNX / MAC clients are implicitly UTF-8 even when capability is absent.
clientType := ""
if len(data) > OffsetReserved {
raw := data[OffsetReserved:min(OffsetReserved+512, len(data))]
if nul := bytes.IndexByte(raw, 0); nul >= 0 {
raw = raw[:nul]
}
head, _, _ := bytes.Cut(raw, []byte("|"))
clientType = string(head)
}
// Parse Reserved (offset 476, 512 bytes) - contains additional info
// Now decode every user-text field with the client's actual codec.
decode := func(b []byte) string { return DecodeClientString(b, capability, clientType) }
if len(data) >= OffsetOsVerInfoEx+156 {
info.OsVerInfo = decode(data[OffsetOsVerInfoEx : OffsetOsVerInfoEx+156])
}
if len(data) >= OffsetPCName+240 {
info.PCName = decode(data[OffsetPCName : OffsetPCName+240])
}
if len(data) >= OffsetMasterID+20 {
info.MasterID = decode(data[OffsetMasterID : OffsetMasterID+20])
}
if len(data) >= OffsetStartTime+20 {
info.StartTime = decode(data[OffsetStartTime : OffsetStartTime+20])
}
if len(data) >= OffsetReserved+512 {
info.Reserved = GbkToUTF8(data[OffsetReserved : OffsetReserved+512])
info.Reserved = decode(data[OffsetReserved : OffsetReserved+512])
} else if len(data) > OffsetReserved {
info.Reserved = GbkToUTF8(data[OffsetReserved:])
info.Reserved = decode(data[OffsetReserved:])
}
return info, nil
}
// parseOsVersionInfo parses the OS version info field
// The C++ client fills this with a readable string like "Windows 10" via getSystemName()
func parseOsVersionInfo(data []byte) string {
return GbkToUTF8(data)
}
// ParseReserved parses the reserved field into a slice of strings
func (info *LoginInfo) ParseReserved() []string {
if info.Reserved == "" {