Feature(Go): Mouse/keyboard input + user management with users.json (Phase 5 + 7)
This commit is contained in:
@@ -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])
|
||||
|
||||
Reference in New Issue
Block a user