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:
@@ -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 == "" {
|
||||
|
||||
Reference in New Issue
Block a user