Files
SimpleRemoter/server/go/protocol/commands.go

318 lines
11 KiB
Go

package protocol
import (
"bytes"
"crypto/hmac"
"crypto/sha256"
"encoding/binary"
"encoding/hex"
"strings"
"golang.org/x/text/encoding/simplifiedchinese"
"golang.org/x/text/transform"
)
// GbkToUTF8 converts GBK encoded bytes to UTF-8 string. The input is treated
// as a null-terminated GBK buffer (typical for Windows clients); content
// after the first NUL byte is discarded. Non-printable characters are
// stripped from the result.
func GbkToUTF8(data []byte) string {
// Find the first null byte and truncate there
if idx := bytes.IndexByte(data, 0); idx >= 0 {
data = data[:idx]
}
if len(data) == 0 {
return ""
}
// Try to decode as GBK
reader := transform.NewReader(bytes.NewReader(data), simplifiedchinese.GBK.NewDecoder())
buf := new(bytes.Buffer)
_, err := buf.ReadFrom(reader)
if err != nil {
// If GBK decoding fails, try treating as UTF-8 or ASCII
return cleanString(string(data))
}
return cleanString(buf.String())
}
// cleanString removes non-printable characters except common whitespace
func cleanString(s string) string {
var result strings.Builder
for _, r := range s {
if r >= 32 || r == '\t' || r == '\n' || r == '\r' {
result.WriteRune(r)
}
}
return strings.TrimSpace(result.String())
}
// Command tokens - matching the C++ definitions (common/commands.h).
const (
// Server -> Client commands
CommandActived byte = 0 // COMMAND_ACTIVED
CommandScreenSpy byte = 16 // COMMAND_SCREEN_SPY - start screen capture
CommandNext byte = 30 // COMMAND_NEXT - "control-side dialog is open, you may stream"
CommandBye byte = 204 // COMMAND_BYE - disconnect
CommandHeartbeat byte = 216 // CMD_HEARTBEAT_ACK
// Client -> Server tokens
TokenAuth byte = 100 // TOKEN_AUTH - authorization required
TokenHeartbeat byte = 101 // TOKEN_HEARTBEAT
TokenLogin byte = 102 // TOKEN_LOGIN - login packet
TokenBitmapInfo byte = 115 // TOKEN_BITMAPINFO - screen sub-connection header
TokenFirstScreen byte = 116 // TOKEN_FIRSTSCREEN - raw BGRA baseline frame (NOT H264)
TokenNextScreen byte = 117 // TOKEN_NEXTSCREEN - non-keyframe H264 (P-frame)
TokenKeyframe byte = 134 // TOKEN_KEYFRAME - H264 IDR (sent on GOP boundary)
TokenConnAuth byte = 246 // TOKEN_CONN_AUTH - sub-connection identity handshake
CmdCursorImage byte = 93 // CMD_CURSOR_IMAGE - custom cursor bitmap (Phase 5+ feature)
)
// Sub-connection authentication (matches common/commands.h ConnAuth* structs).
// Each newly-opened sub-conn first sends a 512-byte ConnAuthPacket, then waits
// for a 256-byte ConnAuthAck before any further command is meaningful.
const (
ConnAuthPacketSize = 512
ConnAuthAckSize = 256
// ConnAuthAck field offsets within the 256-byte buffer.
ConnAuthAckOffStatus = 1 // uint8
ConnAuthAckOffServerTime = 2 // uint64 LE
// Status codes.
ConnAuthStatusOK byte = 0
)
// CMD_MASTERSETTING is the server's reply to a fresh client login. The
// client uses the Signature field to prove this server has the shared
// secret; without a valid signature the client's private FileUpload init
// aborts the process. Struct layout matches MasterSettings in
// common/commands.h (pragma pack 4, total 1000 bytes).
const (
CmdMasterSetting byte = 215
MasterSettingsSize = 1000
MasterSettingsOffReportInterval = 0 // int32, seconds
MasterSettingsOffSignature = 508 // Signature[64]
MasterSettingsSignatureLen = 64
// DefaultReportIntervalSec matches the C++ default. Sending 0 makes the
// client disable its active-window heartbeat field, breaking RTT /
// ActiveWindow live updates on the web UI.
DefaultReportIntervalSec = 5
)
// SignMessage computes HMAC-SHA256(key, msg) and returns the 64-char
// lowercase hex digest. Used to sign CMD_MASTERSETTING replies so the
// client can verify the response came from a legitimate server.
//
// The key is a deployment-time shared secret loaded from the
// YAMA_SIGN_PASSWORD env var so the binary doesn't carry the literal in
// cleartext; provision out-of-band and never commit it.
func SignMessage(password string, msg []byte) string {
mac := hmac.New(sha256.New, []byte(password))
mac.Write(msg)
return hex.EncodeToString(mac.Sum(nil))
}
// Screen-spy parameters that match the C++ ScreenSpy implementation.
const (
AlgorithmH264 byte = 2 // ALGORITHM_H264 — H264 encoding (the algorithm web uses)
)
// Reserved-field indices we care about (see common/commands.h RES_* enum).
// LOGIN_INFOR.szReserved is a '|'-separated list; clients fill known slots
// even when leaving others blank ("?").
const (
ResFieldClientType = 0 // RES_CLIENT_TYPE — client kind (Windows / macOS / ...)
ResFieldFilePath = 4 // RES_FILE_PATH — install path
ResFieldInstallTime = 6 // RES_INSTALL_TIME
ResFieldClientLoc = 10 // RES_CLIENT_LOC — geo string
ResFieldClientPubIP = 11 // RES_CLIENT_PUBIP — public IP
ResFieldClientID = 16 // RES_CLIENT_ID — uint64 decimal, matches TOKEN_BITMAPINFO clientID
)
// ScreenFrameHeaderLen is the size of the small per-frame header prepended by
// the device on every TOKEN_NEXTSCREEN buffer, before the H.264 NAL payload.
// Layout (excluding the leading TOKEN_* byte):
//
// [algorithm:1][cursorPos:8 (int32 x, int32 y)][cursorIdx:1] = 10 bytes
//
// (The C++ side counts the token byte into its ulHeadLength=11; we keep the
// constant strictly post-token so the call site reads `skip := 1 + headerLen`
// without confusion.) SCREENYSPY_IMPROVE adds a 4-byte frameID after the
// cursor index, which is the production-off setting per common/commands.h.
const ScreenFrameHeaderLen = 1 + 8 + 1
// IsH264Keyframe scans an Annex-B H.264 bitstream for a NAL unit indicating
// a keyframe boundary — IDR (type 5), SPS (7) or PPS (8). Returns true on
// the first hit. Matches the detection used by the C++ ScreenSpy broadcast
// path so frame-type bytes stay consistent across server implementations.
func IsH264Keyframe(data []byte) bool {
n := len(data)
for i := 0; i+4 < n; i++ {
var nalOffset int
switch {
case data[i] == 0 && data[i+1] == 0 && data[i+2] == 0 && data[i+3] == 1:
nalOffset = i + 4
case data[i] == 0 && data[i+1] == 0 && data[i+2] == 1:
nalOffset = i + 3
default:
continue
}
if nalOffset >= n {
continue
}
nalType := data[nalOffset] & 0x1F
if nalType == 5 || nalType == 7 || nalType == 8 {
return true
}
}
return false
}
// LOGIN_INFOR structure size and offsets (matching C++ struct with default alignment)
// Note: C++ struct uses default alignment (4-byte for uint32/int)
const (
LoginInfoSize = 980 // Total size of LOGIN_INFOR struct (with alignment padding)
// Field offsets (with alignment padding)
OffsetToken = 0 // 1 byte (unsigned char)
OffsetOsVerInfoEx = 1 // 156 bytes (char[156])
// 3 bytes padding here to align dwCPUMHz to 4-byte boundary
OffsetCPUMHz = 160 // 4 bytes (unsigned int) - aligned to 4
OffsetModuleVersion = 164 // 24 bytes (char[24])
OffsetPCName = 188 // 240 bytes (char[240])
OffsetMasterID = 428 // 20 bytes (char[20])
OffsetWebCamExist = 448 // 4 bytes (int) - aligned to 4
OffsetSpeed = 452 // 4 bytes (unsigned int)
OffsetStartTime = 456 // 20 bytes (char[20])
OffsetReserved = 476 // 512 bytes (char[512])
)
// LoginInfo represents client login information
type LoginInfo struct {
Token byte
OsVerInfo string // OS version info
CPUMHz uint32
ModuleVersion string
PCName string // Computer name
MasterID string
WebCamExist bool
Speed uint32
StartTime string
Reserved string // Contains additional info separated by |
}
// ParseLoginInfo parses LOGIN_INFOR from data
func ParseLoginInfo(data []byte) (*LoginInfo, error) {
if len(data) < 100 { // Minimum size check
return nil, ErrInvalidData
}
info := &LoginInfo{
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)
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])
}
// Parse Reserved (offset 476, 512 bytes) - contains additional info
if len(data) >= OffsetReserved+512 {
info.Reserved = GbkToUTF8(data[OffsetReserved : OffsetReserved+512])
} else if len(data) > OffsetReserved {
info.Reserved = GbkToUTF8(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 == "" {
return nil
}
return strings.Split(info.Reserved, "|")
}
// GetReservedField returns a specific field from reserved data by index
// Fields: ClientType(0), SystemBits(1), CPU(2), Memory(3), FilePath(4),
// Reserved(5), InstallTime(6), InstallInfo(7), ProgramBits(8), ExpiredDate(9),
// ClientLoc(10), ClientPubIP(11), ExeVersion(12), Username(13), IsAdmin(14)
func (info *LoginInfo) GetReservedField(index int) string {
fields := info.ParseReserved()
if index >= 0 && index < len(fields) {
return fields[index]
}
return ""
}
// Validation structure for TOKEN_AUTH
type Validation struct {
From string // Start date (20 bytes)
To string // End date (20 bytes)
Admin string // Admin address (100 bytes)
Port uint16 // Admin port (2 bytes)
MaxDepth uint16 // Max generation depth (2 bytes), 0=cannot generate sub-master
Checksum string // HMAC checksum field (16 bytes)
}
// BuildValidation creates a validation response
func BuildValidation(days float64, admin string, port int, maxDepth uint16) []byte {
// This would build the validation structure
// For now, return a simple structure
data := make([]byte, 160) // Size of Validation struct
data[0] = TokenAuth
// Fill in fields...
// From: 20 bytes (offset 0)
// To: 20 bytes (offset 20)
// Admin: 100 bytes (offset 40)
// Port: 2 bytes (offset 140)
// MaxDepth: 2 bytes (offset 142)
// Checksum: 16 bytes (offset 144)
return data
}