Feature(Go): Mouse/keyboard input + user management with users.json (Phase 5 + 7)

This commit is contained in:
yuanyuanxiang
2026-05-18 13:53:44 +02:00
parent f013512c06
commit 98a914f963
7 changed files with 764 additions and 21 deletions

View File

@@ -6,6 +6,7 @@ import (
"crypto/sha256"
"encoding/binary"
"encoding/hex"
"strconv"
"strings"
"golang.org/x/text/encoding/simplifiedchinese"
@@ -36,6 +37,21 @@ func GbkToUTF8(data []byte) string {
return cleanString(buf.String())
}
// Utf8CleanString trims at the first NUL and strips non-printables — the
// UTF-8 counterpart of GbkToUTF8 for clients that have the CLIENT_CAP_UTF8
// capability bit. Decoding as GBK in that case would mangle multi-byte
// sequences (the C++ comment at WebService.cpp:1530 calls out this exact
// "double-encoding" footgun).
func Utf8CleanString(data []byte) string {
if idx := bytes.IndexByte(data, 0); idx >= 0 {
data = data[:idx]
}
if len(data) == 0 {
return ""
}
return cleanString(string(data))
}
// cleanString removes non-printable characters except common whitespace
func cleanString(s string) string {
var result strings.Builder
@@ -47,14 +63,52 @@ func cleanString(s string) string {
return strings.TrimSpace(result.String())
}
// Client capability bitmask values, matching common/commands.h CLIENT_CAP_*.
// Reported in the hex tail of LOGIN_INFOR.moduleVersion (after the '-').
const (
ClientCapV2 uint32 = 0x0001 // CLIENT_CAP_V2 — V2 file transfer
ClientCapUTF8 uint32 = 0x0002 // CLIENT_CAP_UTF8 — UTF-8 protocol strings (activeWindow, key-log titles, ...)
ClientCapScreenPreview uint32 = 0x0004 // CLIENT_CAP_SCREEN_PREVIEW
)
// SupportsCap returns true when the client's reported capability hex string
// has the given bit set. An empty / unparseable string means "no caps" and
// matches the legacy GBK-Windows convention.
func SupportsCap(capability string, bit uint32) bool {
if capability == "" {
return false
}
caps, err := strconv.ParseUint(strings.TrimSpace(capability), 16, 32)
if err != nil {
return false
}
return uint32(caps)&bit != 0
}
// DecodeClientString decodes a fixed-length, NUL-padded buffer the client
// sent as part of a binary protocol field (typically ActiveWnd). If the
// client signals UTF-8 capability or is known to ship UTF-8 by default
// (Linux / macOS), the bytes are treated as UTF-8; otherwise they're
// decoded from GBK (CP936 — the legacy Windows default).
//
// clientType comes from LOGIN_INFOR reserved field 0 (RES_CLIENT_TYPE) and
// capability from the hex tail of moduleVersion. Both can be empty.
func DecodeClientString(data []byte, capability, clientType string) string {
if SupportsCap(capability, ClientCapUTF8) || clientType == "LNX" || clientType == "MAC" {
return Utf8CleanString(data)
}
return GbkToUTF8(data)
}
// 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
CommandActived byte = 0 // COMMAND_ACTIVED
CommandScreenSpy byte = 16 // COMMAND_SCREEN_SPY - start screen capture
CommandScreenControl byte = 20 // COMMAND_SCREEN_CONTROL - mouse/keyboard input (MSG64 batches)
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
@@ -87,11 +141,11 @@ const (
// aborts the process. Struct layout matches MasterSettings in
// common/commands.h (pragma pack 4, total 1000 bytes).
const (
CmdMasterSetting byte = 215
MasterSettingsSize = 1000
CmdMasterSetting byte = 215
MasterSettingsSize = 1000
MasterSettingsOffReportInterval = 0 // int32, seconds
MasterSettingsOffSignature = 508 // Signature[64]
MasterSettingsSignatureLen = 64
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.
@@ -116,6 +170,101 @@ const (
AlgorithmH264 byte = 2 // ALGORITHM_H264 — H264 encoding (the algorithm web uses)
)
// Windows message constants used inside MSG64.message. The client dispatches
// on these values verbatim (CScreenManager::ProcessCommand at
// client/ScreenManager.cpp:1617), so these MUST stay bit-identical to the
// WinUser.h definitions even though this Go server is cross-platform.
const (
WMKeyDown uint64 = 0x0100
WMKeyUp uint64 = 0x0101
WMSysKeyDown uint64 = 0x0104
WMSysKeyUp uint64 = 0x0105
WMMouseMove uint64 = 0x0200
WMLButtonDown uint64 = 0x0201
WMLButtonUp uint64 = 0x0202
WMLButtonDblClk uint64 = 0x0203
WMRButtonDown uint64 = 0x0204
WMRButtonUp uint64 = 0x0205
WMRButtonDblClk uint64 = 0x0206
WMMButtonDown uint64 = 0x0207
WMMButtonUp uint64 = 0x0208
WMMouseWheel uint64 = 0x020A
)
// Virtual-key codes referenced from the input mapping. Same numeric values
// as the Win32 VK_* constants.
const (
VKLWin = 0x5B // VK_LWIN — filtered: never forwarded
VKRWin = 0x5C // VK_RWIN — filtered: never forwarded
VKPrior = 0x21 // VK_PRIOR (Page Up) — extended-key range start
VKDown = 0x28 // VK_DOWN — extended-key range end
VKInsert = 0x2D
VKDelete = 0x2E
VKNumLock = 0x90
VKRControl = 0xA3
VKRMenu = 0xA5
VKApps = 0x5D
)
// MK_* wParam bitflags for mouse-button messages.
const (
MKLButton uint64 = 0x0001
MKRButton uint64 = 0x0002
MKMButton uint64 = 0x0010
)
// MSG64 is the 48-byte fixed layout the client expects inside a
// COMMAND_SCREEN_CONTROL packet (common/commands.h class MSG64).
//
// [hwnd:8][message:8][wParam:8][lParam:8][time:8][pt.x:4][pt.y:4]
//
// All uint64 fields are little-endian; pt is two int32 LE. The client's
// ProcessCommand validates `ulLength % 48 == 0` and treats each 48-byte
// block as one MSG64.
const Msg64Size = 48
// BuildScreenControlPacket encodes one COMMAND_SCREEN_CONTROL packet
// carrying a single MSG64 record. The cmd byte is prepended.
//
// Wire layout:
//
// [CMD:1][hwnd:8 LE][message:8 LE][wParam:8 LE][lParam:8 LE][time:8 LE][pt.x:4 LE][pt.y:4 LE]
//
// time is filled with a monotonic-ish ms value (ms since Unix epoch trimmed
// to 32 bits) so the client's GetTickCount() comparisons stay reasonable.
func BuildScreenControlPacket(message, wParam, lParam uint64, ptX, ptY int32, timeMs uint32) []byte {
buf := make([]byte, 1+Msg64Size)
buf[0] = CommandScreenControl
// hwnd left zero — the client recomputes hWnd via WindowFromPoint.
binary.LittleEndian.PutUint64(buf[1+8:1+16], message)
binary.LittleEndian.PutUint64(buf[1+16:1+24], wParam)
binary.LittleEndian.PutUint64(buf[1+24:1+32], lParam)
binary.LittleEndian.PutUint64(buf[1+32:1+40], uint64(timeMs))
binary.LittleEndian.PutUint32(buf[1+40:1+44], uint32(ptX))
binary.LittleEndian.PutUint32(buf[1+44:1+48], uint32(ptY))
return buf
}
// MakeLParam packs x into the low word and y into the high word — the
// Windows MAKELPARAM macro the client expects in mouse-message lParams.
func MakeLParam(x, y int32) uint64 {
return uint64(uint32(x)&0xFFFF) | (uint64(uint32(y)&0xFFFF) << 16)
}
// IsExtendedKey returns true when the given Win32 VK code should set the
// extended-key bit (bit 24) in a keyboard lParam. Matches the C++
// HandleKey logic (server/2015Remote/WebService.cpp:944).
func IsExtendedKey(vk int) bool {
if vk >= VKPrior && vk <= VKDown {
return true
}
switch vk {
case VKInsert, VKDelete, VKNumLock, VKRControl, VKRMenu, VKApps:
return true
}
return false
}
// 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 ("?").
@@ -125,6 +274,7 @@ const (
ResFieldInstallTime = 6 // RES_INSTALL_TIME
ResFieldClientLoc = 10 // RES_CLIENT_LOC — geo string
ResFieldClientPubIP = 11 // RES_CLIENT_PUBIP — public IP
ResFieldResolution = 15 // RES_RESOLUTION — client-formatted screen geometry: "N:W*H"
ResFieldClientID = 16 // RES_CLIENT_ID — uint64 decimal, matches TOKEN_BITMAPINFO clientID
)
@@ -173,8 +323,8 @@ 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])
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])