Compare commits
10 Commits
566f5b8d42
...
v1.3.3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
95946e0e6a | ||
|
|
ab7a16bec5 | ||
|
|
9acd141cab | ||
|
|
153cbddcf6 | ||
|
|
d46176f4ef | ||
|
|
70354e244c | ||
|
|
a354f1ed86 | ||
|
|
f85cc8b86c | ||
|
|
bc06fd5af5 | ||
|
|
731ff7a894 |
11
.gitignore
vendored
11
.gitignore
vendored
@@ -74,3 +74,14 @@ test/build/
|
||||
docs/MultiLayerLicense_Design.md
|
||||
docs/MultiLayerLicense_Implementation.md
|
||||
docs/_CodeReference.md
|
||||
linux/CMakeFiles/*
|
||||
Releases/*
|
||||
*.log
|
||||
*.txt
|
||||
linux/Makefile
|
||||
linux/cmake_install.cmake
|
||||
.vs
|
||||
docs/macOS_Support_Design.md
|
||||
settings.local.json
|
||||
*.zip
|
||||
*.lic
|
||||
|
||||
34
ReadMe.md
34
ReadMe.md
@@ -525,6 +525,40 @@ 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,6 +510,40 @@ 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,6 +509,40 @@ 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,6 +43,18 @@ foreach ($pattern in $msBuildPaths) {
|
||||
}
|
||||
}
|
||||
|
||||
# 兜底:默认路径找不到(例如 VS 装在 D 盘)时,用 vswhere 反查。
|
||||
# vswhere.exe 由 VS Installer 维护,固定在 %ProgramFiles(x86)% 下,与 VS 本体盘符无关。
|
||||
if (-not $msBuild) {
|
||||
$vswhere = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe"
|
||||
if (Test-Path $vswhere) {
|
||||
$found = & $vswhere -latest -products * -requires Microsoft.Component.MSBuild `
|
||||
-find "MSBuild\**\Bin\MSBuild.exe" 2>$null |
|
||||
Select-Object -First 1
|
||||
if ($found) { $msBuild = $found }
|
||||
}
|
||||
}
|
||||
|
||||
if (-not $msBuild) {
|
||||
Write-Host "ERROR: MSBuild not found." -ForegroundColor Red
|
||||
Write-Host ""
|
||||
|
||||
@@ -11,6 +11,8 @@
|
||||
|
||||
// ScreenType enum (USING_GDI, USING_DXGI, USING_VIRTUAL) 已移至 common/commands.h
|
||||
|
||||
#define ALGORITHM_NULL "-1"
|
||||
#define ALGORITHM_NUL -1
|
||||
#define ALGORITHM_GRAY 0
|
||||
#define ALGORITHM_DIFF 1
|
||||
#define ALGORITHM_DEFAULT 1
|
||||
|
||||
@@ -281,6 +281,11 @@ public:
|
||||
// 内部:在收到的数据帧分发到 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)
|
||||
{
|
||||
@@ -288,7 +293,6 @@ protected:
|
||||
return recv(m_sClientSocket, buffer, bufSize - 1, 0);
|
||||
}
|
||||
virtual bool ProcessRecvData(CBuffer* m_CompressedBuffer, char* szBuffer, int len, int flag);
|
||||
virtual VOID Disconnect(); // 函数支持 TCP/UDP
|
||||
virtual int SendTo(const char* buf, int len, int flags)
|
||||
{
|
||||
return ::send(m_sClientSocket, buf, len, flags);
|
||||
|
||||
@@ -213,14 +213,18 @@ std::string GetCurrentExeVersion()
|
||||
|
||||
std::string GetCurrentUserNameA()
|
||||
{
|
||||
char username[256];
|
||||
DWORD size = sizeof(username);
|
||||
|
||||
if (GetUserNameA(username, &size)) {
|
||||
return std::string(username);
|
||||
} else {
|
||||
// 用 W 接口取宽字符再转 UTF-8,避免依赖系统 ANSI 代码页(中文账号名在英语系统上
|
||||
// 用 GetUserNameA 取出来是 '?',与 LOGIN_INFOR 的 CLIENT_CAP_UTF8 声明也不一致)。
|
||||
wchar_t wname[256] = {};
|
||||
DWORD wsize = _countof(wname);
|
||||
if (!GetUserNameW(wname, &wsize)) {
|
||||
return "Unknown";
|
||||
}
|
||||
char buf[256 * 3] = {};
|
||||
if (WideCharToMultiByte(CP_UTF8, 0, wname, -1, buf, sizeof(buf), NULL, NULL) <= 0) {
|
||||
return "Unknown";
|
||||
}
|
||||
return std::string(buf);
|
||||
}
|
||||
|
||||
#define XXH_INLINE_ALL
|
||||
@@ -341,9 +345,18 @@ LOGIN_INFOR GetLoginInfo(DWORD dwSpeed, CONNECT_ADDRESS& conn, const std::string
|
||||
LoginInfor.AddReserved(getOSBits()); // 系统位数
|
||||
LoginInfor.AddReserved(GetCPUCores()); // CPU核数
|
||||
LoginInfor.AddReserved(GetMemorySizeGB()); // 系统内存
|
||||
// 路径分两份处理:
|
||||
// - buf (CP_ACP): 保留给 CalcalateIDv2 / 老 CalculateID 用,保证升级后 client ID
|
||||
// 不变(老版客户端用的是 GetModuleFileNameA 的 CP_ACP 字节,
|
||||
// 若改成 UTF-8 同一物理路径会算出不同 ID,丢授权/备注)。
|
||||
// - utf8Path: 发给服务端的 RES_FILE_PATH,与 CLIENT_CAP_UTF8 一致。
|
||||
char buf[_MAX_PATH] = {};
|
||||
GetModuleFileNameA(NULL, buf, sizeof(buf));
|
||||
LoginInfor.AddReserved(buf); // 文件路径
|
||||
GetModuleFileNameA(NULL, buf, sizeof(buf)); // CP_ACP, 留给 ID 计算用
|
||||
wchar_t wbuf[_MAX_PATH] = {};
|
||||
GetModuleFileNameW(NULL, wbuf, _MAX_PATH);
|
||||
char utf8Path[_MAX_PATH * 3] = {}; // UTF-8 最多 3 字节/中文,给足
|
||||
WideCharToMultiByte(CP_UTF8, 0, wbuf, -1, utf8Path, sizeof(utf8Path), NULL, NULL);
|
||||
LoginInfor.AddReserved(utf8Path); // 文件路径 (UTF-8 发给服务端显示)
|
||||
LoginInfor.AddReserved("?"); // test
|
||||
std::string installTime = cfg.GetStr("settings", "install_time");
|
||||
if (installTime.empty()) {
|
||||
@@ -355,7 +368,7 @@ LOGIN_INFOR GetLoginInfo(DWORD dwSpeed, CONNECT_ADDRESS& conn, const std::string
|
||||
LoginInfor.AddReserved(sizeof(void*)==4 ? 32 : 64); // 程序位数
|
||||
std::string masterHash(skCrypt(MASTER_HASH));
|
||||
WIN32_FILE_ATTRIBUTE_DATA fileInfo;
|
||||
GetFileAttributesExA(buf, GetFileExInfoStandard, &fileInfo);
|
||||
GetFileAttributesExW(wbuf, GetFileExInfoStandard, &fileInfo);
|
||||
LoginInfor.AddReserved(str.c_str()); // 授权信息
|
||||
bool isDefault = strlen(conn.szFlag) == 0 || strcmp(conn.szFlag, skCrypt(FLAG_GHOST)) == 0 ||
|
||||
strcmp(conn.szFlag, skCrypt("Happy New Year!")) == 0;
|
||||
|
||||
@@ -137,7 +137,11 @@ CScreenManager::CScreenManager(IOCPClient* ClientObject, int n, void* user, BOOL
|
||||
}
|
||||
}
|
||||
|
||||
BOOL fixedQuality = all || algo == ALGORITHM_H264;
|
||||
int quality = cfg.GetInt("settings", "QualityLevel", QUALITY_GOOD);
|
||||
if (algo != (BYTE)ALGORITHM_NUL)
|
||||
quality = QUALITY_DISABLED;
|
||||
Mprintf("图像传输算法: %d, 多显示器支持是否启用: %d, 屏幕质量等级: %d\n", (int)algo, all, quality);
|
||||
|
||||
m_ScreenSettings.MaxFPS = m_nMaxFPS;
|
||||
m_ScreenSettings.CompressThread = threadNum;
|
||||
m_ScreenSettings.ScreenStrategy = cfg.GetInt("settings", "ScreenStrategy", 0);
|
||||
@@ -146,7 +150,7 @@ CScreenManager::CScreenManager(IOCPClient* ClientObject, int n, void* user, BOOL
|
||||
m_ScreenSettings.FullScreen = cfg.GetInt("settings", "FullScreen", priv);
|
||||
m_ScreenSettings.RemoteCursor = cfg.GetInt("settings", "RemoteCursor", 0);
|
||||
m_ScreenSettings.ScrollDetectInterval = cfg.GetInt("settings", "ScrollDetectInterval", 2); // 默认每2帧
|
||||
m_ScreenSettings.QualityLevel = cfg.GetInt("settings", "QualityLevel", fixedQuality ? QUALITY_GOOD : QUALITY_ADAPTIVE);
|
||||
m_ScreenSettings.QualityLevel = cfg.GetInt("settings", "QualityLevel", quality);
|
||||
m_ScreenSettings.CpuSpeedup = cfg.GetInt("settings", "CpuSpeedup", 0);
|
||||
m_ScreenSettings.AudioEnabled = cfg.GetInt("settings", "AudioEnabled", 0); // 默认禁用音频
|
||||
|
||||
|
||||
@@ -88,7 +88,7 @@ IDR_WAVE WAVE "Res\\msg.wav"
|
||||
//
|
||||
|
||||
VS_VERSION_INFO VERSIONINFO
|
||||
FILEVERSION 1,0,3,2
|
||||
FILEVERSION 1,0,3,3
|
||||
PRODUCTVERSION 1,0,0,1
|
||||
FILEFLAGSMASK 0x3fL
|
||||
#ifdef _DEBUG
|
||||
@@ -106,7 +106,7 @@ BEGIN
|
||||
BEGIN
|
||||
VALUE "CompanyName", "FUCK THE UNIVERSE"
|
||||
VALUE "FileDescription", "A GHOST"
|
||||
VALUE "FileVersion", "1.0.3.2"
|
||||
VALUE "FileVersion", "1.0.3.3"
|
||||
VALUE "InternalName", "ServerDll.dll"
|
||||
VALUE "LegalCopyright", "Copyright (C) 2019-2026"
|
||||
VALUE "OriginalFilename", "ServerDll.dll"
|
||||
|
||||
Binary file not shown.
52
client/sign_shim_unix.cpp
Normal file
52
client/sign_shim_unix.cpp
Normal file
@@ -0,0 +1,52 @@
|
||||
// sign_shim_unix.cpp - Linux/macOS adapter for libsign.a's C interface
|
||||
//
|
||||
// libsign.a 公开 ABI 是 C linkage(避免 std::string 跨编译器/跨 libstdc++
|
||||
// 版本 ABI 风险),但 YAMA 客户端代码(IOCPClient.cpp / KernelManager.cpp /
|
||||
// linux/main.cpp / macos/main.mm)习惯用 std::string 调用 signMessage /
|
||||
// verifyMessage。本文件提供 C++ 适配,让两边契合。
|
||||
//
|
||||
// Windows 不编译这个文件——Windows 直接链接私有 .lib 提供的 std::string 版本。
|
||||
|
||||
#include <string>
|
||||
#include <cstring>
|
||||
|
||||
// libsign.a 提供的 C 接口
|
||||
extern "C" {
|
||||
int signMessage_c(const char* privateKey, int privateKeyLen,
|
||||
const unsigned char* msg, int msgLen,
|
||||
char* outBuf, int outBufSize);
|
||||
int verifyMessage_c(const char* publicKey, int publicKeyLen,
|
||||
const unsigned char* msg, int msgLen,
|
||||
const char* sigHex, int sigLen);
|
||||
int isVerifyCalled_c(void);
|
||||
}
|
||||
|
||||
// 与 YAMA common/commands.h 中 BYTE 一致
|
||||
typedef unsigned char BYTE;
|
||||
|
||||
// ============================================================================
|
||||
// 提供 YAMA 既有声明所期望的 C++ 符号
|
||||
// ============================================================================
|
||||
|
||||
std::string signMessage(const std::string& privateKey, BYTE* msg, int len)
|
||||
{
|
||||
char buf[65] = {};
|
||||
int n = signMessage_c(privateKey.c_str(), (int)privateKey.size(),
|
||||
msg, len,
|
||||
buf, sizeof(buf));
|
||||
if (n != 64) return std::string();
|
||||
return std::string(buf, 64);
|
||||
}
|
||||
|
||||
bool verifyMessage(const std::string& publicKey, BYTE* msg, int len,
|
||||
const std::string& signature)
|
||||
{
|
||||
return verifyMessage_c(publicKey.c_str(), (int)publicKey.size(),
|
||||
msg, len,
|
||||
signature.data(), (int)signature.size()) != 0;
|
||||
}
|
||||
|
||||
int isVerifyCalled()
|
||||
{
|
||||
return isVerifyCalled_c();
|
||||
}
|
||||
@@ -205,11 +205,14 @@ private:
|
||||
// Disable zsh session save/restore (causes errors in PTY)
|
||||
setenv("SHELL_SESSIONS_DISABLE", "1", 1);
|
||||
|
||||
// Try zsh first (macOS default), fallback to bash
|
||||
// Try zsh first (macOS default), fallback to bash. Use -l (login) so
|
||||
// ~/.zprofile is sourced — Homebrew's `brew shellenv` (which puts
|
||||
// /opt/homebrew/bin on PATH) lives there. Without -l the PTY can't
|
||||
// see brew / cmake / node / pyenv / rustup etc.
|
||||
if (access("/bin/zsh", X_OK) == 0) {
|
||||
execl("/bin/zsh", "zsh", "-i", nullptr);
|
||||
execl("/bin/zsh", "zsh", "-l", "-i", nullptr);
|
||||
}
|
||||
execl("/bin/bash", "bash", "-i", nullptr);
|
||||
execl("/bin/bash", "bash", "-l", "-i", nullptr);
|
||||
#else
|
||||
// Linux locale settings (C.UTF-8 is most portable)
|
||||
setenv("LANG", "C.UTF-8", 1);
|
||||
|
||||
103
common/client_auth_state.h
Normal file
103
common/client_auth_state.h
Normal file
@@ -0,0 +1,103 @@
|
||||
// client_auth_state.h
|
||||
// Linux/macOS 客户端服务端身份校验状态 + helper(Layer 1 防护)。
|
||||
//
|
||||
// 行为模型:
|
||||
// - g_loginMsg:startTime + "|" + clientID,启动时填一次,跨重连不变
|
||||
// - g_loginTime:每次新连接重置为当前时刻
|
||||
// - g_settingsVerified:服务端 CMD_MASTERSETTING 通过签名校验后置 true,
|
||||
// 重连时重置为 false
|
||||
//
|
||||
// 客户端是常驻服务——服务端可能频繁重启 / 长期离线 / 临时不可达,这些都不应
|
||||
// 让进程退出。校验失败仅作"本次连接不可信"处理:断开本连接 + 让外层重连。
|
||||
// 功能侧的安全由子连接 auth(TOKEN_CONN_AUTH)兜底——没通过校验的服务端无法
|
||||
// 触发任何 sub-connection 功能。
|
||||
//
|
||||
// 跨线程访问:
|
||||
// - g_settingsVerified 在 DataProcess(IO 线程)写、心跳循环(main 线程)读
|
||||
// - 用 std::atomic<bool> + acquire/release 内存序保证可见性
|
||||
//
|
||||
// C++17 inline 变量保证多翻译单元共享同一实例,无 ODR 冲突。
|
||||
#pragma once
|
||||
|
||||
#include <atomic>
|
||||
#include <cstring>
|
||||
#include <ctime>
|
||||
#include <string>
|
||||
|
||||
#include "common/commands.h"
|
||||
|
||||
// 全局 namespace 中的 verifyMessage:由 client/sign_shim_unix.cpp(Linux/macOS)或
|
||||
// 私有 .lib(Windows)提供。必须在任何 namespace 之外声明,否则会被解析成
|
||||
// ClientAuth::verifyMessage 导致链接失败。
|
||||
extern bool verifyMessage(const std::string& publicKey, BYTE* msg, int len,
|
||||
const std::string& signature);
|
||||
|
||||
namespace ClientAuth {
|
||||
|
||||
// ============== 跨重连保留的状态 ==============
|
||||
inline std::string g_loginMsg;
|
||||
inline time_t g_loginTime = 0;
|
||||
inline std::atomic<bool> g_settingsVerified{false};
|
||||
|
||||
// ============== Helpers ==============
|
||||
|
||||
// 进入新连接前调用:g_loginTime = now,verified = false
|
||||
inline void OnNewConnection()
|
||||
{
|
||||
g_loginTime = time(nullptr);
|
||||
g_settingsVerified.store(false, std::memory_order_release);
|
||||
}
|
||||
|
||||
// DataProcess 开头的 gate:未通过校验前仅放行 CMD_MASTERSETTING(校验本身)。
|
||||
// 其它命令一律静默忽略——既防止未授权服务端 spawn 子连接线程做 DoS,
|
||||
// 也防止它发 COMMAND_BYE 之类把客户端进程关掉。
|
||||
inline bool IsCommandAllowed(unsigned char cmd)
|
||||
{
|
||||
return g_settingsVerified.load(std::memory_order_acquire) || cmd == CMD_MASTERSETTING;
|
||||
}
|
||||
|
||||
// 处理 CMD_MASTERSETTING(payload = szBuffer + 1,payloadLen = ulLength - 1):
|
||||
// 强制要求完整 MasterSettings(包含 Signature 字段);不完整 / 签名失败 → 不更新
|
||||
// g_settingsVerified,让心跳循环 30s 超时自然把本次连接断开重连。
|
||||
//
|
||||
// 返回 true:校验通过(已 store(true)),通过 outReportInterval / outSettingsCopy
|
||||
// 返回 settings 内容供调用方继续应用(更新心跳间隔、密码哈希等)
|
||||
// 返回 false:本次响应异常,调用方应直接 return(不要继续处理)
|
||||
//
|
||||
// 注意:参数采用 unsigned char* 而非 BYTE* 避免依赖 Windows typedef;
|
||||
// BYTE 在 commands.h 已 typedef 为 unsigned char,等价。
|
||||
inline bool HandleMasterSettings(const unsigned char* payload, int payloadLen,
|
||||
MasterSettings* outSettings)
|
||||
{
|
||||
if (payloadLen < (int)sizeof(MasterSettings)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
MasterSettings settings = {};
|
||||
std::memcpy(&settings, payload, sizeof(MasterSettings));
|
||||
|
||||
// 服务端身份校验:用 g_loginMsg (= szStartTime + "|" + clientID) 与 settings.Signature
|
||||
// 验证签名。失败 → 不立即退出,让超时兜底+重连逻辑处理。
|
||||
// 注意 ::verifyMessage 在全局 namespace(见本头部 extern 声明),不能省略 :: 前缀,
|
||||
// 否则会被解析为 ClientAuth::verifyMessage,链接失败。
|
||||
std::string sig((char*)settings.Signature,
|
||||
(char*)settings.Signature + sizeof(settings.Signature));
|
||||
if (!::verifyMessage("", (BYTE*)g_loginMsg.data(), (int)g_loginMsg.length(), sig)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
g_settingsVerified.store(true, std::memory_order_release);
|
||||
if (outSettings) *outSettings = settings;
|
||||
return true;
|
||||
}
|
||||
|
||||
// 心跳循环里检查 30s 超时:登录后 30 秒内必须收到并通过 MasterSettings 校验,
|
||||
// 失败 → 调用方应显式断开本连接让外层重连。永不退出进程。
|
||||
inline bool IsTimedOut()
|
||||
{
|
||||
return !g_settingsVerified.load(std::memory_order_acquire) &&
|
||||
g_loginTime > 0 &&
|
||||
time(nullptr) - g_loginTime > 30;
|
||||
}
|
||||
|
||||
} // namespace ClientAuth
|
||||
@@ -1014,7 +1014,14 @@ typedef struct LOGIN_INFOR {
|
||||
{
|
||||
memset(this, 0, sizeof(LOGIN_INFOR));
|
||||
bToken = TOKEN_LOGIN;
|
||||
sprintf_s(moduleVersion, "%s-%04X", DLL_VERSION, CLIENT_CAP_V2 | CLIENT_CAP_UTF8 | CLIENT_CAP_SCREEN_PREVIEW);
|
||||
// 能力位:声明客户端实际实现了的功能。SCREEN_PREVIEW 只在 Windows 客户端
|
||||
// 实现(依赖 GDI BitBlt + GDI+ JPEG),Linux/macOS 不声明,避免服务端发请求
|
||||
// 后等 4s 超时显示"预览不可用"。
|
||||
unsigned int caps = CLIENT_CAP_V2 | CLIENT_CAP_UTF8;
|
||||
#ifdef _WIN32
|
||||
caps |= CLIENT_CAP_SCREEN_PREVIEW;
|
||||
#endif
|
||||
sprintf_s(moduleVersion, "%s-%04X", DLL_VERSION, caps);
|
||||
}
|
||||
LOGIN_INFOR& Speed(unsigned long speed)
|
||||
{
|
||||
|
||||
99
common/posix_net_helpers.h
Normal file
99
common/posix_net_helpers.h
Normal file
@@ -0,0 +1,99 @@
|
||||
// posix_net_helpers.h
|
||||
// Linux/macOS 客户端共用的网络/Shell 工具:execCmd / httpGet / getPublicIP /
|
||||
// jsonExtract / getGeoLocation。Windows 端已有等价实现,不应包含此头。
|
||||
//
|
||||
// 全部 inline,header-only,避免新增 .cpp / 改 CMakeLists。
|
||||
//
|
||||
// 设计说明:
|
||||
// - httpGet 优先 curl,备选 wget(Linux 默认自带;macOS 默认无 wget,缺失时
|
||||
// wget 命令失败、execCmd 返空——无副作用,等价于"只用 curl")
|
||||
// - getPublicIP 轮询多个公网 IP 查询源,按顺序尝试直到成功
|
||||
// - jsonExtract 仅做最简单的 "key":"value" 提取,不依赖 jsoncpp
|
||||
// - getGeoLocation 通过 ipinfo.io 反查地理位置,与 Windows IPConverter 同源
|
||||
#pragma once
|
||||
|
||||
#include <cstdio>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
#include "common/logger.h"
|
||||
|
||||
namespace PosixNet {
|
||||
|
||||
// 执行 shell 命令,捕获其 stdout 输出(trim 末尾空白后返回)
|
||||
inline std::string execCmd(const std::string& cmd)
|
||||
{
|
||||
std::unique_ptr<FILE, decltype(&pclose)> pipe(popen(cmd.c_str(), "r"), pclose);
|
||||
if (!pipe) return "";
|
||||
char buf[4096];
|
||||
std::string result;
|
||||
while (fgets(buf, sizeof(buf), pipe.get())) {
|
||||
result += buf;
|
||||
}
|
||||
while (!result.empty() && (result.back() == '\n' || result.back() == '\r' || result.back() == ' '))
|
||||
result.pop_back();
|
||||
return result;
|
||||
}
|
||||
|
||||
// HTTP GET 请求:优先 curl,备选 wget
|
||||
inline std::string httpGet(const std::string& url, int timeoutSec = 5)
|
||||
{
|
||||
std::string t = std::to_string(timeoutSec);
|
||||
std::string r = execCmd("curl -s --max-time " + t + " \"" + url + "\" 2>/dev/null");
|
||||
if (!r.empty()) return r;
|
||||
r = execCmd("wget -qO- --timeout=" + t + " \"" + url + "\" 2>/dev/null");
|
||||
return r;
|
||||
}
|
||||
|
||||
// 获取公网 IP(轮询多个查询源,与 Windows 端 IPConverter 一致)
|
||||
inline std::string getPublicIP()
|
||||
{
|
||||
static const char* urls[] = {
|
||||
"https://checkip.amazonaws.com",
|
||||
"https://api.ipify.org",
|
||||
"https://ipinfo.io/ip",
|
||||
"https://icanhazip.com",
|
||||
"https://ifconfig.me/ip",
|
||||
};
|
||||
for (auto& url : urls) {
|
||||
std::string ip = httpGet(url, 3);
|
||||
if (!ip.empty() && ip.find('.') != std::string::npos && ip.size() <= 45) {
|
||||
Mprintf("getPublicIP: %s (from %s)\n", ip.c_str(), url);
|
||||
return ip;
|
||||
}
|
||||
}
|
||||
Mprintf("getPublicIP: all sources failed\n");
|
||||
return "";
|
||||
}
|
||||
|
||||
// 从 JSON 字符串中提取指定 key 的 string 值(简易解析,不依赖 jsoncpp)
|
||||
// 仅支持 "key": "value" 或 "key":"value" 格式
|
||||
inline std::string jsonExtract(const std::string& json, const std::string& key)
|
||||
{
|
||||
std::string needle = "\"" + key + "\"";
|
||||
size_t pos = json.find(needle);
|
||||
if (pos == std::string::npos) return "";
|
||||
pos = json.find(':', pos + needle.size());
|
||||
if (pos == std::string::npos) return "";
|
||||
pos = json.find('"', pos + 1);
|
||||
if (pos == std::string::npos) return "";
|
||||
size_t end = json.find('"', pos + 1);
|
||||
if (end == std::string::npos) return "";
|
||||
return json.substr(pos + 1, end - pos - 1);
|
||||
}
|
||||
|
||||
// 获取 IP 地理位置(ipinfo.io,与 Windows IPConverter 同源)
|
||||
inline std::string getGeoLocation(const std::string& ip)
|
||||
{
|
||||
if (ip.empty()) return "";
|
||||
std::string json = httpGet("https://ipinfo.io/" + ip + "/json", 5);
|
||||
if (json.empty()) return "";
|
||||
std::string country = jsonExtract(json, "country");
|
||||
std::string city = jsonExtract(json, "city");
|
||||
if (city.empty() && country.empty()) return "";
|
||||
if (city.empty()) return country;
|
||||
if (country.empty()) return city;
|
||||
return city + ", " + country;
|
||||
}
|
||||
|
||||
} // namespace PosixNet
|
||||
50
common/rtt_estimator.h
Normal file
50
common/rtt_estimator.h
Normal file
@@ -0,0 +1,50 @@
|
||||
// rtt_estimator.h
|
||||
// 平滑 RTT 估算器(参考 RFC 6298),与 Windows 端 KernelManager 算法一致。
|
||||
// Linux/macOS 客户端共享:每次心跳 ACK 用 update_from_sample(rtt_ms) 喂一次样本。
|
||||
//
|
||||
// 设计要点:
|
||||
// - srtt / rttvar / rto 单位为秒;输入是毫秒
|
||||
// - 异常值(≤0 或 >30s)丢弃,防止统计被一个瞬时坏样本污染
|
||||
// - alpha=1/8, beta=1/4 与 RFC 6298 默认值一致
|
||||
//
|
||||
// C++17 inline 全局变量:g_rttEstimator / g_heartbeatInterval 由本头文件直接定义,
|
||||
// 多翻译单元 include 不会触发 ODR 冲突。
|
||||
#pragma once
|
||||
|
||||
#include <cmath>
|
||||
|
||||
struct RttEstimator {
|
||||
double srtt = 0.0; // 平滑 RTT (秒)
|
||||
double rttvar = 0.0; // RTT 波动 (秒)
|
||||
double rto = 0.0; // 超时时间 (秒)
|
||||
bool initialized = false;
|
||||
|
||||
void update_from_sample(double rtt_ms)
|
||||
{
|
||||
// 过滤异常值:RTT应在合理范围内 (0, 30000] 毫秒
|
||||
if (rtt_ms <= 0 || rtt_ms > 30000)
|
||||
return;
|
||||
|
||||
const double alpha = 1.0 / 8;
|
||||
const double beta = 1.0 / 4;
|
||||
double rtt = rtt_ms / 1000.0;
|
||||
|
||||
if (!initialized) {
|
||||
srtt = rtt;
|
||||
rttvar = rtt / 2.0;
|
||||
rto = srtt + 4.0 * rttvar;
|
||||
initialized = true;
|
||||
} else {
|
||||
rttvar = (1.0 - beta) * rttvar + beta * std::fabs(srtt - rtt);
|
||||
srtt = (1.0 - alpha) * srtt + alpha * rtt;
|
||||
rto = srtt + 4.0 * rttvar;
|
||||
}
|
||||
|
||||
// 限制最小 RTO(RFC 6298 推荐 1 秒)
|
||||
if (rto < 1.0) rto = 1.0;
|
||||
}
|
||||
};
|
||||
|
||||
// 进程级全局:所有翻译单元共享同一份估算器与心跳间隔
|
||||
inline RttEstimator g_rttEstimator;
|
||||
inline int g_heartbeatInterval = 5; // 默认心跳间隔(秒),可被服务端 CMD_MASTERSETTING 更新
|
||||
70
common/sub_conn_thread.h
Normal file
70
common/sub_conn_thread.h
Normal file
@@ -0,0 +1,70 @@
|
||||
// sub_conn_thread.h
|
||||
// Linux/macOS 客户端子连接 worker 线程的统一骨架。
|
||||
//
|
||||
// 各 worker 线程(Shell / ScreenSpy / FileManager / SystemManager 等)共有的步骤:
|
||||
// 1. new IOCPClient(g_bExit, exit_while_disconnect=true)
|
||||
// 2. Enter log
|
||||
// 3. EnableSubConnAuth(true, g_myClientID)(子连接强制 ConnAuth)
|
||||
// 4. ConnectServer(内部会执行 PerformConnAuth;失败返 false)
|
||||
// 5. 创建 platform handler
|
||||
// 6. setManagerCallBack 装回调
|
||||
// 7. 调 onReady(发首包:TOKEN_TERMINAL_START / SendBitmapInfo() 等)
|
||||
// 8. while (running && connected && !g_bExit) Sleep
|
||||
// 9. 清回调防止 dangling
|
||||
// 10. Leave log
|
||||
// 11. catch exceptions
|
||||
//
|
||||
// 平台差异(通过 lambda 注入):
|
||||
// - HandlerT:PTYHandler / ScreenHandler / SystemManager / FileManager
|
||||
// - createHandler 可返回 nullptr 表示初始化失败(如 macOS ScreenHandler 无录屏权限)
|
||||
// - onReady 完成首包发送或额外 setup
|
||||
//
|
||||
// 用法见 linux/main.cpp / macos/main.mm 的 *workingThread 调用点。
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
#include <stdexcept>
|
||||
|
||||
#include "client/IOCPClient.h"
|
||||
#include "common/commands.h"
|
||||
#include "common/logger.h"
|
||||
|
||||
extern State g_bExit;
|
||||
extern uint64_t g_myClientID;
|
||||
extern CONNECT_ADDRESS g_SETTINGS;
|
||||
|
||||
// 子连接 worker 线程通用骨架。
|
||||
//
|
||||
// CreateFn 签名: std::unique_ptr<HandlerT>(IOCPClient*)
|
||||
// 返回 nullptr 表示初始化失败(如权限拒绝),线程会跳过 callback 安装直接 leave。
|
||||
// OnReadyFn 签名: void(IOCPClient*, HandlerT*)
|
||||
// handler 装上 callback 后立即调用,可在此发送首包或做额外 setup。
|
||||
template <class HandlerT, class CreateFn, class OnReadyFn>
|
||||
inline void RunSubConnThread(const char* threadName, CreateFn createHandler, OnReadyFn onReady)
|
||||
{
|
||||
try {
|
||||
std::unique_ptr<IOCPClient> ClientObject(new IOCPClient(g_bExit, true));
|
||||
void* clientAddr = ClientObject.get();
|
||||
Mprintf(">>> Enter %s [%p]\n", threadName, clientAddr);
|
||||
|
||||
// 子连接:开启 auth。Linux/macOS IOCPClient 不带 m_conn,显式传入 g_myClientID。
|
||||
ClientObject->EnableSubConnAuth(true, g_myClientID);
|
||||
|
||||
if (!g_bExit && ClientObject->ConnectServer(g_SETTINGS.ServerIP(), g_SETTINGS.ServerPort())) {
|
||||
std::unique_ptr<HandlerT> handler = createHandler(ClientObject.get());
|
||||
if (handler) {
|
||||
ClientObject->setManagerCallBack(handler.get(),
|
||||
IOCPManager::DataProcess,
|
||||
IOCPManager::ReconnectProcess);
|
||||
onReady(ClientObject.get(), handler.get());
|
||||
while (ClientObject->IsRunning() && ClientObject->IsConnected() && S_CLIENT_NORMAL == g_bExit)
|
||||
Sleep(1000);
|
||||
// 清除回调,防止重连线程访问已销毁的 handler
|
||||
ClientObject->setManagerCallBack(nullptr, nullptr, nullptr);
|
||||
}
|
||||
}
|
||||
Mprintf(">>> Leave %s [%p]\n", threadName, clientAddr);
|
||||
} catch (const std::exception& e) {
|
||||
Mprintf("*** %s exception: %s ***\n", threadName, e.what());
|
||||
}
|
||||
}
|
||||
@@ -28,6 +28,7 @@ set(SOURCES
|
||||
main.cpp
|
||||
../client/Buffer.cpp
|
||||
../client/IOCPClient.cpp
|
||||
../client/sign_shim_unix.cpp
|
||||
)
|
||||
add_executable(ghost ${SOURCES})
|
||||
|
||||
@@ -40,6 +41,14 @@ set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -g")
|
||||
message(STATUS "链接库文件: ${CMAKE_SOURCE_DIR}/lib/libzstd.a")
|
||||
target_link_libraries(ghost PRIVATE "${CMAKE_SOURCE_DIR}/lib/libzstd.a")
|
||||
|
||||
# 链接私有签名库(提供 signMessage / verifyMessage,源码不开源)
|
||||
# 该库由 SimplePlugins 仓库本地构建后放置于 lib/,构建机需先准备好
|
||||
target_link_libraries(ghost PRIVATE "${CMAKE_SOURCE_DIR}/lib/libsign.a")
|
||||
|
||||
# libsign.a 内部使用 OpenSSL HMAC,需要在最终可执行链接 libcrypto
|
||||
find_package(OpenSSL REQUIRED)
|
||||
target_link_libraries(ghost PRIVATE OpenSSL::Crypto)
|
||||
|
||||
# 链接 dl 库(dlopen/dlsym 用于运行时加载 X11)
|
||||
target_link_libraries(ghost PRIVATE dl)
|
||||
|
||||
|
||||
BIN
linux/ghost
BIN
linux/ghost
Binary file not shown.
BIN
linux/lib/libsign.a
Normal file
BIN
linux/lib/libsign.a
Normal file
Binary file not shown.
289
linux/main.cpp
289
linux/main.cpp
@@ -32,6 +32,10 @@
|
||||
#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);
|
||||
@@ -46,6 +50,8 @@ 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)
|
||||
@@ -302,142 +308,55 @@ private:
|
||||
};
|
||||
|
||||
// ============== 心跳保活 & RTT 估算 ==============
|
||||
|
||||
// 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 更新
|
||||
// RttEstimator + g_rttEstimator + g_heartbeatInterval 已抽到 common/rtt_estimator.h
|
||||
|
||||
// PTYHandler moved to common/PTYHandler.h (shared between Linux and macOS)
|
||||
|
||||
void* ShellworkingThread(void* param)
|
||||
void* ShellworkingThread(void* /*param*/)
|
||||
{
|
||||
try {
|
||||
std::unique_ptr<IOCPClient> ClientObject(new IOCPClient(g_bExit, true));
|
||||
void* clientAddr = ClientObject.get();
|
||||
Mprintf(">>> Enter ShellworkingThread [%p]\n", clientAddr);
|
||||
// 子连接:开启 auth。Linux IOCPClient 不带 m_conn,显式传入 g_myClientID。
|
||||
ClientObject->EnableSubConnAuth(true, g_myClientID);
|
||||
if (!g_bExit && ClientObject->ConnectServer(g_SETTINGS.ServerIP(), g_SETTINGS.ServerPort())) {
|
||||
std::unique_ptr<PTYHandler> handler(new PTYHandler(ClientObject.get()));
|
||||
ClientObject->setManagerCallBack(handler.get(), IOCPManager::DataProcess, IOCPManager::ReconnectProcess);
|
||||
RunSubConnThread<PTYHandler>(
|
||||
"ShellworkingThread",
|
||||
[](IOCPClient* c) { return std::unique_ptr<PTYHandler>(new PTYHandler(c)); },
|
||||
[](IOCPClient* c, PTYHandler*) {
|
||||
BYTE bToken = TOKEN_TERMINAL_START;
|
||||
ClientObject->Send2Server((char*)&bToken, 1);
|
||||
Mprintf(">>> ShellworkingThread [%p] Send: TOKEN_TERMINAL_START\n", clientAddr);
|
||||
while (ClientObject->IsRunning() && ClientObject->IsConnected() && S_CLIENT_NORMAL == g_bExit)
|
||||
Sleep(1000);
|
||||
// 清除回调,防止重连线程访问已销毁的 handler
|
||||
ClientObject->setManagerCallBack(nullptr, nullptr, nullptr);
|
||||
}
|
||||
Mprintf(">>> Leave ShellworkingThread [%p]\n", clientAddr);
|
||||
} catch (const std::exception& e) {
|
||||
Mprintf("*** ShellworkingThread exception: %s ***\n", e.what());
|
||||
}
|
||||
c->Send2Server((char*)&bToken, 1);
|
||||
Mprintf(">>> ShellworkingThread [%p] Send: TOKEN_TERMINAL_START\n", c);
|
||||
});
|
||||
return NULL;
|
||||
}
|
||||
|
||||
void* ScreenworkingThread(void* param)
|
||||
void* ScreenworkingThread(void* /*param*/)
|
||||
{
|
||||
try {
|
||||
std::unique_ptr<IOCPClient> ClientObject(new IOCPClient(g_bExit, true));
|
||||
void* clientAddr = ClientObject.get();
|
||||
Mprintf(">>> Enter ScreenworkingThread [%p]\n", clientAddr);
|
||||
// 子连接:开启 auth。Linux IOCPClient 不带 m_conn,显式传入 g_myClientID。
|
||||
ClientObject->EnableSubConnAuth(true, g_myClientID);
|
||||
if (!g_bExit && ClientObject->ConnectServer(g_SETTINGS.ServerIP(), g_SETTINGS.ServerPort())) {
|
||||
std::unique_ptr<ScreenHandler> handler(new ScreenHandler(ClientObject.get()));
|
||||
ClientObject->setManagerCallBack(handler.get(), IOCPManager::DataProcess, IOCPManager::ReconnectProcess);
|
||||
RunSubConnThread<ScreenHandler>(
|
||||
"ScreenworkingThread",
|
||||
[](IOCPClient* c) { return std::unique_ptr<ScreenHandler>(new ScreenHandler(c)); },
|
||||
[](IOCPClient* c, ScreenHandler* h) {
|
||||
// 连接后立即发送完整的 BITMAPINFO 包(与 Windows 端 ScreenManager 流程一致)
|
||||
handler->SendBitmapInfo();
|
||||
Mprintf(">>> ScreenworkingThread [%p] Send: TOKEN_BITMAPINFO\n", clientAddr);
|
||||
while (ClientObject->IsRunning() && ClientObject->IsConnected() && S_CLIENT_NORMAL == g_bExit)
|
||||
Sleep(1000);
|
||||
// 清除回调,防止重连线程访问已销毁的 handler
|
||||
ClientObject->setManagerCallBack(nullptr, nullptr, nullptr);
|
||||
}
|
||||
Mprintf(">>> Leave ScreenworkingThread [%p]\n", clientAddr);
|
||||
} catch (const std::exception& e) {
|
||||
Mprintf("*** ScreenworkingThread exception: %s ***\n", e.what());
|
||||
}
|
||||
h->SendBitmapInfo();
|
||||
Mprintf(">>> ScreenworkingThread [%p] Send: TOKEN_BITMAPINFO\n", c);
|
||||
});
|
||||
return NULL;
|
||||
}
|
||||
|
||||
void* SystemManagerThread(void* param)
|
||||
void* SystemManagerThread(void* /*param*/)
|
||||
{
|
||||
try {
|
||||
std::unique_ptr<IOCPClient> ClientObject(new IOCPClient(g_bExit, true));
|
||||
void* clientAddr = ClientObject.get();
|
||||
Mprintf(">>> Enter SystemManagerThread [%p]\n", clientAddr);
|
||||
// 子连接:开启 auth。Linux IOCPClient 不带 m_conn,显式传入 g_myClientID。
|
||||
ClientObject->EnableSubConnAuth(true, g_myClientID);
|
||||
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());
|
||||
}
|
||||
RunSubConnThread<SystemManager>(
|
||||
"SystemManagerThread",
|
||||
[](IOCPClient* c) { return std::unique_ptr<SystemManager>(new SystemManager(c)); },
|
||||
[](IOCPClient* c, SystemManager*) {
|
||||
Mprintf(">>> SystemManagerThread [%p] Send: TOKEN_PSLIST\n", c);
|
||||
});
|
||||
return NULL;
|
||||
}
|
||||
|
||||
void* FileManagerThread(void* param)
|
||||
void* FileManagerThread(void* /*param*/)
|
||||
{
|
||||
try {
|
||||
std::unique_ptr<IOCPClient> ClientObject(new IOCPClient(g_bExit, true));
|
||||
void* clientAddr = ClientObject.get();
|
||||
Mprintf(">>> Enter FileManagerThread [%p]\n", clientAddr);
|
||||
// 子连接:开启 auth。Linux IOCPClient 不带 m_conn,显式传入 g_myClientID。
|
||||
ClientObject->EnableSubConnAuth(true, g_myClientID);
|
||||
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());
|
||||
}
|
||||
RunSubConnThread<FileManager>(
|
||||
"FileManagerThread",
|
||||
[](IOCPClient* c) { return std::unique_ptr<FileManager>(new FileManager(c)); },
|
||||
[](IOCPClient* c, FileManager*) {
|
||||
Mprintf(">>> FileManagerThread [%p] Send: TOKEN_DRIVE_LIST\n", c);
|
||||
});
|
||||
return NULL;
|
||||
}
|
||||
|
||||
@@ -446,6 +365,12 @@ int DataProcess(void* user, PBYTE szBuffer, ULONG ulLength)
|
||||
if (szBuffer == nullptr || ulLength == 0)
|
||||
return TRUE;
|
||||
|
||||
// 服务端身份未通过校验前,仅放行 CMD_MASTERSETTING(校验本身)。详见
|
||||
// common/client_auth_state.h ClientAuth::IsCommandAllowed 的注释。
|
||||
if (!ClientAuth::IsCommandAllowed(szBuffer[0])) {
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
if (szBuffer[0] == COMMAND_BYE) {
|
||||
Mprintf("*** [%p] Received Bye-Bye command ***\n", user);
|
||||
g_bExit = S_CLIENT_EXIT;
|
||||
@@ -467,18 +392,23 @@ int DataProcess(void* user, PBYTE szBuffer, ULONG ulLength)
|
||||
uint64_t now = GetUnixMs();
|
||||
double rtt_ms = (double)(now - ack->Time);
|
||||
g_rttEstimator.update_from_sample(rtt_ms);
|
||||
// 心跳节奏太密日志会刷屏;最多 60s 一行
|
||||
static time_t lastAckLog = 0;
|
||||
time_t now_s = time(nullptr);
|
||||
if (now_s - lastAckLog >= 60) {
|
||||
lastAckLog = now_s;
|
||||
Mprintf("** [%p] Heartbeat ACK: RTT=%.1fms, SRTT=%.1fms ***\n",
|
||||
user, rtt_ms, g_rttEstimator.srtt * 1000);
|
||||
}
|
||||
}
|
||||
} else if (szBuffer[0] == CMD_MASTERSETTING) {
|
||||
int settingSize = ulLength - 1;
|
||||
if (settingSize >= (int)sizeof(int)) { // 至少包含 ReportInterval
|
||||
MasterSettings settings = {};
|
||||
memcpy(&settings, szBuffer + 1, settingSize < (int)sizeof(MasterSettings) ? settingSize : sizeof(MasterSettings));
|
||||
MasterSettings settings;
|
||||
if (!ClientAuth::HandleMasterSettings(szBuffer + 1, (int)ulLength - 1, &settings)) {
|
||||
return TRUE; // 包不全或签名失败:让 30s 超时兜底重连
|
||||
}
|
||||
if (settings.ReportInterval > 0)
|
||||
g_heartbeatInterval = settings.ReportInterval;
|
||||
Mprintf("** [%p] MasterSettings: ReportInterval=%ds ***\n", user, g_heartbeatInterval);
|
||||
}
|
||||
} else if (szBuffer[0] == COMMAND_NEXT) {
|
||||
Mprintf("** [%p] Received 'NEXT' command ***\n", user);
|
||||
} else if (szBuffer[0] == COMMAND_C2C_TEXT) {
|
||||
@@ -774,87 +704,14 @@ std::string getScreenResolution()
|
||||
return "0:0*0";
|
||||
}
|
||||
|
||||
// 执行命令并返回输出
|
||||
static std::string execCmd(const std::string& cmd)
|
||||
{
|
||||
std::unique_ptr<FILE, decltype(&pclose)> pipe(popen(cmd.c_str(), "r"), pclose);
|
||||
if (!pipe) return "";
|
||||
char buf[4096];
|
||||
std::string result;
|
||||
while (fgets(buf, sizeof(buf), pipe.get())) {
|
||||
result += buf;
|
||||
}
|
||||
// 去除尾部空白
|
||||
while (!result.empty() && (result.back() == '\n' || result.back() == '\r' || result.back() == ' '))
|
||||
result.pop_back();
|
||||
return result;
|
||||
}
|
||||
|
||||
// HTTP GET 请求(优先 curl,备选 wget)
|
||||
static std::string httpGet(const std::string& url, int timeoutSec = 5)
|
||||
{
|
||||
std::string t = std::to_string(timeoutSec);
|
||||
// 优先使用 curl
|
||||
std::string r = execCmd("curl -s --max-time " + t + " \"" + url + "\" 2>/dev/null");
|
||||
if (!r.empty()) return r;
|
||||
// 备选 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;
|
||||
}
|
||||
// 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); }
|
||||
|
||||
// ============== 守护进程 ==============
|
||||
|
||||
@@ -1090,6 +947,9 @@ int main(int argc, char* argv[])
|
||||
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;
|
||||
|
||||
@@ -1102,6 +962,8 @@ int main(int argc, char* argv[])
|
||||
continue;
|
||||
}
|
||||
|
||||
// 进入新连接,重置服务端身份校验状态
|
||||
ClientAuth::OnNewConnection();
|
||||
ClientObject->SendLoginInfo(logInfo.Speed(clock() - c));
|
||||
|
||||
// 心跳保活循环:定时发送心跳包,服务端回复后动态更新 RTT
|
||||
@@ -1131,8 +993,19 @@ int main(int argc, char* argv[])
|
||||
if (!ClientObject->IsRunning() || !ClientObject->IsConnected() || g_bExit != S_CLIENT_NORMAL)
|
||||
break;
|
||||
|
||||
// 30 秒内未通过 MasterSettings 校验 → 断开本连接让外层重连,
|
||||
// 永不退出进程(详见 ClientAuth::IsTimedOut 注释)。
|
||||
if (ClientAuth::IsTimedOut()) {
|
||||
ClientObject->Disconnect(); // 关闭 socket,防止重连时 fd 泄漏
|
||||
break;
|
||||
}
|
||||
|
||||
// 构造并发送心跳包(与 Windows 端 KernelManager::SendHeartbeat 格式一致)
|
||||
std::string activity = utf8ToGbk(activityChecker.Check());
|
||||
// ActiveWnd 直接发 UTF-8——与 LOGIN_INFOR.moduleVersion 中声明的
|
||||
// CLIENT_CAP_UTF8 一致;服务端按 cap 位用 CP_UTF8 解码。早期为兼容
|
||||
// MBCS 老服务端做过 utf8ToGbk 转换,但现在新版 Linux 客户端经
|
||||
// libsign 网关只能连新版服务端,无需再转。
|
||||
std::string activity = activityChecker.Check();
|
||||
|
||||
Heartbeat hb;
|
||||
hb.Time = GetUnixMs();
|
||||
@@ -1143,10 +1016,16 @@ 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Logger::getInstance().stop();
|
||||
removePidFile();
|
||||
|
||||
@@ -19,6 +19,7 @@ set(SOURCES
|
||||
main.mm
|
||||
../client/Buffer.cpp
|
||||
../client/IOCPClient.cpp
|
||||
../client/sign_shim_unix.cpp
|
||||
ScreenHandler.mm
|
||||
InputHandler.mm
|
||||
SystemManager.mm
|
||||
@@ -62,6 +63,11 @@ 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
Normal file
BIN
macos/ghost
Normal file
Binary file not shown.
@@ -3,18 +3,32 @@
|
||||
# 用法: ./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_SRC"
|
||||
echo "错误: 找不到 ghost 二进制"
|
||||
echo " 尝试过: $SCRIPT_DIR/ghost"
|
||||
echo " 尝试过: $SCRIPT_DIR/build/bin/ghost"
|
||||
echo ""
|
||||
echo "请先编译: ./build.sh"
|
||||
echo "或将 ghost 二进制放到脚本同目录"
|
||||
echo "或指定路径: $0 <ghost可执行文件路径>"
|
||||
exit 1
|
||||
fi
|
||||
@@ -25,17 +39,17 @@ echo ""
|
||||
set -e
|
||||
|
||||
# 1. 停止旧进程
|
||||
echo "[1/6] 停止旧进程..."
|
||||
echo "[1/7] 停止旧进程..."
|
||||
pkill -9 -f "$APP_BIN" 2>/dev/null || true
|
||||
|
||||
# 2. 重置系统权限(关键步骤!避免权限缓存导致空白桌面)
|
||||
echo "[2/6] 重置系统权限..."
|
||||
echo "[2/7] 重置系统权限..."
|
||||
echo " (这会清除屏幕录制和辅助功能的旧授权,需要重新授权)"
|
||||
tccutil reset ScreenCapture 2>/dev/null || true
|
||||
tccutil reset Accessibility 2>/dev/null || true
|
||||
|
||||
# 3. 创建应用程序包
|
||||
echo "[3/6] 创建应用程序..."
|
||||
echo "[3/7] 创建应用程序..."
|
||||
sudo rm -rf "$APP_DIR"
|
||||
sudo mkdir -p "$APP_DIR/Contents/MacOS"
|
||||
sudo mkdir -p "$APP_DIR/Contents/Resources"
|
||||
@@ -65,11 +79,15 @@ sudo tee "$APP_DIR/Contents/Info.plist" > /dev/null << 'EOF'
|
||||
EOF
|
||||
|
||||
# 4. 清除隔离属性
|
||||
echo "[4/6] 清除隔离属性..."
|
||||
echo "[4/7] 清除隔离属性..."
|
||||
sudo xattr -cr "$APP_DIR"
|
||||
|
||||
# 5. 签名应用
|
||||
echo "[5/6] 签名应用..."
|
||||
# 5. 签名应用(ad-hoc 重签)
|
||||
# 必须步骤:Apple Silicon 上未签 / 签名失效的 binary 会被 AMFI 直接 SIGKILL。
|
||||
# 常见破坏签名的场景:服务端 BuildDlg 在 Windows 端 patch 了 binary 里的服务器
|
||||
# 地址 → 那一页的 SHA-256 hash 跟原签名块对不上 → AMFI 拒绝运行。
|
||||
# --force 替换旧签名,--deep 覆盖 bundle 内所有可执行项,--sign - 是 ad-hoc。
|
||||
echo "[5/7] 签名应用 (ad-hoc, 修复 binary 修改后的签名失效)..."
|
||||
sudo codesign --force --deep --sign - "$APP_DIR"
|
||||
|
||||
# 6. 添加到登录项(开机自启)
|
||||
|
||||
BIN
macos/lib/libsign.a
Normal file
BIN
macos/lib/libsign.a
Normal file
Binary file not shown.
213
macos/main.mm
213
macos/main.mm
@@ -19,6 +19,10 @@
|
||||
#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"
|
||||
@@ -36,6 +40,8 @@ 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 };
|
||||
|
||||
@@ -438,49 +444,12 @@ static bool hasCameraDevice()
|
||||
// ============== Public IP ==============
|
||||
|
||||
// Execute command and return output
|
||||
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 "";
|
||||
}
|
||||
// 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(); }
|
||||
|
||||
// ============== Install Time (persistent storage) ==============
|
||||
|
||||
@@ -626,6 +595,9 @@ static void fillLoginInfo(LOGIN_INFOR& info)
|
||||
}
|
||||
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);
|
||||
}
|
||||
@@ -670,120 +642,51 @@ static void daemonize()
|
||||
}
|
||||
|
||||
// ============== Main Entry Point ==============
|
||||
// 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; // 心跳间隔(秒),默认 5 秒,后续可由服务端动态调整
|
||||
|
||||
void* ShellworkingThread(void* param)
|
||||
void* ShellworkingThread(void* /*param*/)
|
||||
{
|
||||
try {
|
||||
std::unique_ptr<IOCPClient> ClientObject(new IOCPClient(g_bExit, true));
|
||||
void* clientAddr = ClientObject.get();
|
||||
NSLog(@">>> Enter ShellworkingThread [%p]", clientAddr);
|
||||
// 子连接:开启 auth。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<PTYHandler> handler(new PTYHandler(ClientObject.get()));
|
||||
ClientObject->setManagerCallBack(handler.get(), IOCPManager::DataProcess, IOCPManager::ReconnectProcess);
|
||||
RunSubConnThread<PTYHandler>(
|
||||
"ShellworkingThread",
|
||||
[](IOCPClient* c) { return std::unique_ptr<PTYHandler>(new PTYHandler(c)); },
|
||||
[](IOCPClient* c, PTYHandler*) {
|
||||
BYTE bToken = TOKEN_TERMINAL_START;
|
||||
ClientObject->Send2Server((char*)&bToken, 1);
|
||||
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());
|
||||
}
|
||||
c->Send2Server((char*)&bToken, 1);
|
||||
Mprintf(">>> ShellworkingThread [%p] Send: TOKEN_TERMINAL_START\n", c);
|
||||
});
|
||||
return NULL;
|
||||
}
|
||||
|
||||
void* ScreenworkingThread(void* param)
|
||||
void* ScreenworkingThread(void* /*param*/)
|
||||
{
|
||||
try {
|
||||
std::unique_ptr<IOCPClient> ClientObject(new IOCPClient(g_bExit, true));
|
||||
void* clientAddr = ClientObject.get();
|
||||
Mprintf(">>> Enter ScreenworkingThread [%p]\n", clientAddr);
|
||||
// 子连接:开启 auth。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<ScreenHandler> handler(new ScreenHandler(ClientObject.get()));
|
||||
if (!handler->init()) {
|
||||
RunSubConnThread<ScreenHandler>(
|
||||
"ScreenworkingThread",
|
||||
[](IOCPClient* c) -> std::unique_ptr<ScreenHandler> {
|
||||
// macOS ScreenHandler 需要先 init() 申请录屏权限/抓屏 stream,失败 → 返 nullptr
|
||||
// 让骨架直接 leave,跳过 callback 安装
|
||||
auto h = std::unique_ptr<ScreenHandler>(new ScreenHandler(c));
|
||||
if (!h->init()) {
|
||||
Mprintf("*** ScreenHandler initialization failed (no permission?) ***\n");
|
||||
return NULL;
|
||||
return nullptr;
|
||||
}
|
||||
ClientObject->setManagerCallBack(handler.get(), IOCPManager::DataProcess, IOCPManager::ReconnectProcess);
|
||||
return h;
|
||||
},
|
||||
[](IOCPClient* c, ScreenHandler* h) {
|
||||
// 连接后立即发送完整的 BITMAPINFO 包(与 Windows 端 ScreenManager 流程一致)
|
||||
handler->sendBitmapInfo();
|
||||
Mprintf(">>> ScreenworkingThread [%p] Send: TOKEN_BITMAPINFO\n", clientAddr);
|
||||
while (ClientObject->IsRunning() && ClientObject->IsConnected() && S_CLIENT_NORMAL == g_bExit)
|
||||
Sleep(1000);
|
||||
// 清除回调,防止重连线程访问已销毁的 handler
|
||||
ClientObject->setManagerCallBack(nullptr, nullptr, nullptr);
|
||||
}
|
||||
Mprintf(">>> Leave ScreenworkingThread [%p]\n", clientAddr);
|
||||
} catch (const std::exception& e) {
|
||||
Mprintf("*** ScreenworkingThread exception: %s ***\n", e.what());
|
||||
}
|
||||
h->sendBitmapInfo();
|
||||
Mprintf(">>> ScreenworkingThread [%p] Send: TOKEN_BITMAPINFO\n", c);
|
||||
});
|
||||
return NULL;
|
||||
}
|
||||
|
||||
void* FileManagerworkingThread(void* param)
|
||||
void* FileManagerworkingThread(void* /*param*/)
|
||||
{
|
||||
try {
|
||||
std::unique_ptr<IOCPClient> ClientObject(new IOCPClient(g_bExit, true));
|
||||
void* clientAddr = ClientObject.get();
|
||||
Mprintf(">>> Enter FileManagerworkingThread [%p]\n", clientAddr);
|
||||
// 子连接:开启 auth。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<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());
|
||||
}
|
||||
RunSubConnThread<FileManager>(
|
||||
"FileManagerworkingThread",
|
||||
[](IOCPClient* c) { return std::unique_ptr<FileManager>(new FileManager(c)); },
|
||||
[](IOCPClient* c, FileManager*) {
|
||||
Mprintf(">>> FileManagerworkingThread [%p] initialized\n", c);
|
||||
});
|
||||
return NULL;
|
||||
}
|
||||
|
||||
@@ -792,6 +695,12 @@ int DataProcess(void* user, PBYTE szBuffer, ULONG ulLength)
|
||||
if (szBuffer == nullptr || ulLength == 0)
|
||||
return TRUE;
|
||||
|
||||
// 服务端身份未通过校验前,仅放行 CMD_MASTERSETTING(校验本身)。详见
|
||||
// common/client_auth_state.h ClientAuth::IsCommandAllowed 的注释。
|
||||
if (!ClientAuth::IsCommandAllowed(szBuffer[0])) {
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
if (szBuffer[0] == COMMAND_BYE) {
|
||||
Mprintf("*** [%p] Received Bye-Bye command ***\n", user);
|
||||
g_bExit = S_CLIENT_EXIT;
|
||||
@@ -848,14 +757,13 @@ int DataProcess(void* user, PBYTE szBuffer, ULONG ulLength)
|
||||
}
|
||||
}
|
||||
} else if (szBuffer[0] == CMD_MASTERSETTING) {
|
||||
int settingSize = ulLength - 1;
|
||||
if (settingSize >= (int)sizeof(int)) { // 至少包含 ReportInterval
|
||||
MasterSettings settings = {};
|
||||
memcpy(&settings, szBuffer + 1, settingSize < (int)sizeof(MasterSettings) ? settingSize : sizeof(MasterSettings));
|
||||
MasterSettings settings;
|
||||
if (!ClientAuth::HandleMasterSettings(szBuffer + 1, (int)ulLength - 1, &settings)) {
|
||||
return TRUE; // 包不全或签名失败:让 30s 超时兜底重连
|
||||
}
|
||||
if (settings.ReportInterval > 0)
|
||||
g_heartbeatInterval = settings.ReportInterval;
|
||||
Mprintf("** [%p] MasterSettings: ReportInterval=%ds ***\n", user, g_heartbeatInterval);
|
||||
}
|
||||
} else if (szBuffer[0] == COMMAND_NEXT) {
|
||||
Mprintf("** [%p] Received 'NEXT' command ***\n", user);
|
||||
} else if (szBuffer[0] == CMD_SET_GROUP) {
|
||||
@@ -981,6 +889,8 @@ int main(int argc, const char* argv[])
|
||||
continue;
|
||||
}
|
||||
|
||||
// 进入新连接,重置服务端身份校验状态
|
||||
ClientAuth::OnNewConnection();
|
||||
ClientObject->SendLoginInfo(logInfo.Speed(clock() - c));
|
||||
|
||||
// 心跳保活循环:定时发送心跳包,服务端回复后动态更新 RTT
|
||||
@@ -1002,6 +912,13 @@ 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/3] 停止进程..."
|
||||
echo "[1/4] 停止进程..."
|
||||
pkill -9 -f "$APP_DIR" 2>/dev/null || true
|
||||
|
||||
# 2. 删除文件
|
||||
echo "[2/3] 删除文件..."
|
||||
echo "[2/4] 删除文件..."
|
||||
sudo rm -rf "$APP_DIR"
|
||||
rm -rf ~/.config/ghost 2>/dev/null || true
|
||||
rm -f /tmp/ghost.log 2>/dev/null || true
|
||||
|
||||
Binary file not shown.
@@ -178,15 +178,25 @@ bool SupportsFileTransferV2(context* ctx) {
|
||||
}
|
||||
|
||||
// 获取客户端协议字符串编码:优先看自身能力位,若是子连接(CAPABILITIES 为空)
|
||||
// 则通过 peer IP 查主连接。找不到则默认 CP936。
|
||||
// 则通过 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 && mainCtx->SupportsUtf8()) return CP_UTF8;
|
||||
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;
|
||||
}
|
||||
@@ -3892,7 +3902,23 @@ void CMy2015RemoteDlg::OnOnlineUpdate()
|
||||
return;
|
||||
DWORD dwFileSize = 0;
|
||||
BOOL is64bit = "64" == ContextObject->GetAdditionalData(RES_PROGRAM_BITS);
|
||||
std::filesystem::path path = ContextObject->GetAdditionalData(RES_FILE_PATH).GetString();
|
||||
// 客户端 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::string stem = path.stem().string();
|
||||
std::string dirName = path.parent_path().filename().string();
|
||||
const char* resName = dlg.m_nSelected
|
||||
@@ -4036,7 +4062,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", "").c_str();
|
||||
CString algo = THIS_CFG.GetStr("settings", "ScreenCompress", ALGORITHM_NULL).c_str();
|
||||
BYTE bToken[32] = { COMMAND_SCREEN_SPY, n, algo.IsEmpty() ? ALGORITHM_RGB565 : atoi(algo.GetString()), all};
|
||||
SendSelectedCommand(bToken, sizeof(bToken), screenParamModifier, bToken);
|
||||
}
|
||||
@@ -5592,12 +5618,23 @@ VOID CMy2015RemoteDlg::MessageHandle(CONTEXT_OBJECT* ContextObject)
|
||||
break;
|
||||
}
|
||||
case TOKEN_TERMINAL_START: { // Linux PTY 终端 (WebView2 + xterm.js)
|
||||
// 检查 WebView2 和 DLL,都满足则使用现代终端,否则退化到经典终端
|
||||
if (IsWebView2Available() && LoadTerminalModule()) {
|
||||
// 三个前置条件,缺任何一个都回退到经典终端,并把原因贴到信息列表。
|
||||
// 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) {
|
||||
g_2015RemoteDlg->SendMessage(WM_OPENTERMINALDIALOG, 0, (LPARAM)ContextObject);
|
||||
} else {
|
||||
g_2015RemoteDlg->PostMessageA(WM_SHOWMESSAGE,
|
||||
(WPARAM)new CharMsg("To use Modern Terminal - WebView2 and TerminalModule.dll are required"), NULL);
|
||||
(WPARAM)new CharMsg(fallbackReason), NULL);
|
||||
g_2015RemoteDlg->SendMessage(WM_OPENSHELLDIALOG, 0, (LPARAM)ContextObject);
|
||||
}
|
||||
break;
|
||||
@@ -7550,6 +7587,28 @@ 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"},
|
||||
|
||||
@@ -104,6 +104,7 @@
|
||||
<OpenMPSupport>false</OpenMPSupport>
|
||||
<DisableSpecificWarnings>4018;4244;4267;4819;4838</DisableSpecificWarnings>
|
||||
<LanguageStandard>stdcpp17</LanguageStandard>
|
||||
<AdditionalOptions>/source-charset:utf-8 /execution-charset:.936 %(AdditionalOptions)</AdditionalOptions>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<SubSystem>Windows</SubSystem>
|
||||
@@ -138,6 +139,7 @@
|
||||
<OpenMPSupport>false</OpenMPSupport>
|
||||
<DisableSpecificWarnings>4018;4244;4267;4819;4838</DisableSpecificWarnings>
|
||||
<LanguageStandard>stdcpp17</LanguageStandard>
|
||||
<AdditionalOptions>/source-charset:utf-8 /execution-charset:.936 %(AdditionalOptions)</AdditionalOptions>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<SubSystem>Windows</SubSystem>
|
||||
@@ -172,6 +174,7 @@
|
||||
<OpenMPSupport>false</OpenMPSupport>
|
||||
<DisableSpecificWarnings>4018;4244;4267;4819;4838</DisableSpecificWarnings>
|
||||
<LanguageStandard>stdcpp17</LanguageStandard>
|
||||
<AdditionalOptions>/source-charset:utf-8 /execution-charset:.936 %(AdditionalOptions)</AdditionalOptions>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<SubSystem>Windows</SubSystem>
|
||||
@@ -208,6 +211,7 @@
|
||||
<OpenMPSupport>false</OpenMPSupport>
|
||||
<DisableSpecificWarnings>4018;4244;4267;4819;4838</DisableSpecificWarnings>
|
||||
<LanguageStandard>stdcpp17</LanguageStandard>
|
||||
<AdditionalOptions>/source-charset:utf-8 /execution-charset:.936 %(AdditionalOptions)</AdditionalOptions>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<SubSystem>Windows</SubSystem>
|
||||
@@ -231,6 +235,7 @@
|
||||
</ItemDefinitionGroup>
|
||||
<ItemGroup>
|
||||
<None Include="..\..\linux\ghost" />
|
||||
<None Include="..\..\macos\ghost" />
|
||||
<None Include="..\..\Release\ghost.exe" />
|
||||
<None Include="..\..\Release\SCLoader.exe" />
|
||||
<None Include="..\..\Release\ServerDll.dll" />
|
||||
@@ -241,6 +246,8 @@
|
||||
<None Include="..\..\x64\Release\ServerDll.dll" />
|
||||
<None Include="..\..\x64\Release\TestRun.exe" />
|
||||
<None Include="..\..\x64\Release\TinyRun.dll" />
|
||||
<None Include="lang\en_US.ini" />
|
||||
<None Include="lang\zh_TW.ini" />
|
||||
<None Include="res\1.cur" />
|
||||
<None Include="res\2.cur" />
|
||||
<None Include="res\2015Remote.ico" />
|
||||
@@ -250,6 +257,7 @@
|
||||
<None Include="res\3rd\rcedit.exe" />
|
||||
<None Include="res\3rd\SCLoader_32.exe" />
|
||||
<None Include="res\3rd\SCLoader_64.exe" />
|
||||
<None Include="res\3rd\TerminalModule_x64.dll" />
|
||||
<None Include="res\3rd\upx.exe" />
|
||||
<None Include="res\4.cur" />
|
||||
<None Include="res\arrow.cur" />
|
||||
|
||||
@@ -325,6 +325,10 @@
|
||||
<None Include="res\3rd\rcedit.exe" />
|
||||
<None Include="res\3rd\SCLoader_32.exe" />
|
||||
<None Include="res\3rd\SCLoader_64.exe" />
|
||||
<None Include="lang\en_US.ini" />
|
||||
<None Include="lang\zh_TW.ini" />
|
||||
<None Include="res\3rd\TerminalModule_x64.dll" />
|
||||
<None Include="..\..\macos\ghost" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Text Include="..\..\ReadMe.md" />
|
||||
|
||||
@@ -28,6 +28,7 @@ enum Index {
|
||||
IndexGhostMsc,
|
||||
IndexTestRunMsc,
|
||||
IndexLinuxGhost,
|
||||
IndexMacGhost,
|
||||
OTHER_ITEM
|
||||
};
|
||||
|
||||
@@ -417,7 +418,7 @@ void CBuildDlg::OnBnClickedOk()
|
||||
MessageBoxL("Shellcode 只能向64位电脑注入,注入器也只能是64位!", "提示", MB_ICONWARNING);
|
||||
return;
|
||||
}
|
||||
if (index == IndexLinuxGhost) {
|
||||
if (index == IndexLinuxGhost || index == IndexMacGhost) {
|
||||
m_ComboCompress.SetCurSel(CLIENT_COMPRESS_NONE);
|
||||
m_SliderClientSize.SetPos(0);
|
||||
}
|
||||
@@ -477,6 +478,11 @@ 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;
|
||||
@@ -699,7 +705,18 @@ void CBuildDlg::OnBnClickedOk()
|
||||
std::vector<char> padding(size, time(0)%256);
|
||||
WriteBinaryToFile(strSeverFile.GetString(), padding.data(), size, -1);
|
||||
}
|
||||
MessageBoxL(_TR("生成成功! 文件位于:") + "\r\n" + strSeverFile + tip, "提示", MB_ICONINFORMATION);
|
||||
CString successMsg = _TR("生成成功! 文件位于:") + "\r\n" + strSeverFile + tip;
|
||||
// macOS binary 被 patch 后签名失效,AMFI 会 SIGKILL。提醒走 install.sh
|
||||
// (内部会 ad-hoc 重签) 或手动 codesign。
|
||||
if (typ == CLIENT_TYPE_MACOS) {
|
||||
successMsg += "\r\n\r\n";
|
||||
successMsg += _TR("提示: macOS 端 binary 已被修改导致签名失效,直接运行会被系统强杀。");
|
||||
successMsg += "\r\n";
|
||||
successMsg += _TR("推荐: 拷贝到 macOS 后运行 install.sh 安装 (脚本会自动重签)。");
|
||||
successMsg += "\r\n";
|
||||
successMsg += _TR("或手动重签:") + " codesign --force --sign - ghost";
|
||||
}
|
||||
MessageBoxL(successMsg, "提示", MB_ICONINFORMATION);
|
||||
}
|
||||
SAFE_DELETE_ARRAY(szBuffer);
|
||||
if (index == IndexTestRun_DLL) return;
|
||||
@@ -763,6 +780,7 @@ BOOL CBuildDlg::OnInitDialog()
|
||||
m_ComboExe.InsertStringL(IndexGhostMsc, "ghost.exe - Windows 服务");
|
||||
m_ComboExe.InsertStringL(IndexTestRunMsc, "TestRun - Windows 服务");
|
||||
m_ComboExe.InsertStringL(IndexLinuxGhost, "ghost - Linux x64");
|
||||
m_ComboExe.InsertStringL(IndexMacGhost, "ghost - Apple MacOS");
|
||||
m_ComboExe.InsertStringL(OTHER_ITEM, CString("选择文件"));
|
||||
m_ComboExe.SetCurSel(IndexTestRun_MemDLL);
|
||||
|
||||
@@ -864,9 +882,34 @@ CString CBuildDlg::GetFilePath(CString type, CString filter, BOOL isOpen)
|
||||
return "";
|
||||
}
|
||||
|
||||
// 选 Linux / macOS 客户端时禁用对它们不适用的 Windows-only 选项:
|
||||
// - 架构 (m_ComboBits):Linux/macOS binary 是固定架构的预编译资源
|
||||
// - 加壳 (m_ComboCompress):UPX / ShellCode AES 等都是 Windows PE 概念
|
||||
// - 高级 group:安装目录 / 程序名称 / 载荷类型 / 增肥 / 下载服务,全是 Windows 安装/伪装相关
|
||||
void CBuildDlg::EnableWindowsOnlyControls(BOOL enable)
|
||||
{
|
||||
static const int ids[] = {
|
||||
// 架构
|
||||
IDC_COMBO_BITS, IDC_STATIC_BUILD_ARCH,
|
||||
// 加壳
|
||||
IDC_COMBO_COMPRESS, IDC_STATIC_BUILD_PACK,
|
||||
// 高级 group + 内部所有控件
|
||||
IDC_STATIC_BUILD_ADVANCED,
|
||||
IDC_STATIC_PAYLOAD, IDC_STATIC_PAYLOAD2, IDC_STATIC_PAYLOAD3,
|
||||
IDC_STATIC_BUILD_PADDING, IDC_STATIC_DOWNLOAD,
|
||||
IDC_EDIT_INSTALL_DIR, IDC_EDIT_INSTALL_NAME,
|
||||
IDC_COMBO_PAYLOAD, IDC_SLIDER_CLIENT_SIZE,
|
||||
IDC_CHECK_FILESERVER, IDC_EDIT_DOWNLOAD_URL,
|
||||
};
|
||||
for (int id : ids) {
|
||||
if (CWnd* p = GetDlgItem(id)) p->EnableWindow(enable);
|
||||
}
|
||||
}
|
||||
|
||||
void CBuildDlg::OnCbnSelchangeComboExe()
|
||||
{
|
||||
auto n = m_ComboExe.GetCurSel();
|
||||
EnableWindowsOnlyControls(!(n == IndexLinuxGhost || n == IndexMacGhost));
|
||||
if (n == OTHER_ITEM) {
|
||||
CString name = GetFilePath(_T("dll"), _T("All Files (*.*)|*.*|DLL Files (*.dll)|*.dll|EXE Files (*.exe)|*.exe|"));
|
||||
if (!name.IsEmpty()) {
|
||||
|
||||
@@ -104,4 +104,7 @@ public:
|
||||
CString m_sDownloadUrl;
|
||||
afx_msg void OnBnClickedCheckFileserver();
|
||||
afx_msg void OnCbnSelchangeComboPayload();
|
||||
|
||||
// 选 Linux / macOS 客户端时禁用 Windows-only 选项(架构 / 加壳 / 高级 group)
|
||||
void EnableWindowsOnlyControls(BOOL enable);
|
||||
};
|
||||
|
||||
@@ -8,8 +8,13 @@
|
||||
#include "InputDlg.h"
|
||||
#include "ZstdArchive.h"
|
||||
#include "2015RemoteDlg.h"
|
||||
#include "CDlgFileSend.h"
|
||||
#include <Shlobj.h>
|
||||
|
||||
// V2 接收用:定义在 CPasswordDlg.cpp,按本仓约定就地前置声明
|
||||
std::string GetPwdHash();
|
||||
std::string GetHMAC(int offset);
|
||||
|
||||
#ifdef _DEBUG
|
||||
#define new DEBUG_NEW
|
||||
#undef THIS_FILE
|
||||
@@ -176,6 +181,8 @@ BEGIN_MESSAGE_MAP(CFileManagerDlg, CDialog)
|
||||
ON_MESSAGE(WM_MY_MESSAGE, OnMyMessage)
|
||||
ON_MESSAGE(WM_LOCAL_SEARCH_DONE, OnLocalSearchDone)
|
||||
ON_MESSAGE(WM_LOCAL_SEARCH_PROGRESS, OnLocalSearchProgress)
|
||||
ON_MESSAGE(WM_RECVFILEV2_CHUNK, &CFileManagerDlg::OnRecvFileV2Chunk)
|
||||
ON_MESSAGE(WM_RECVFILEV2_COMPLETE, &CFileManagerDlg::OnRecvFileV2Complete)
|
||||
//}}AFX_MSG_MAP
|
||||
ON_COMMAND(ID_FILEMANGER_COMPRESS, &CFileManagerDlg::OnFilemangerCompress)
|
||||
ON_COMMAND(ID_FILEMANGER_UNCOMPRESS, &CFileManagerDlg::OnFilemangerUncompress)
|
||||
@@ -994,6 +1001,28 @@ 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;
|
||||
@@ -2682,10 +2711,87 @@ void CFileManagerDlg::OnLocalStop()
|
||||
|
||||
void CFileManagerDlg::PostNcDestroy()
|
||||
{
|
||||
// TODO: Add your specialized code here and/or call the base class
|
||||
// 清理 V2 接收进度框:注意 CDialogBase::PostNcDestroy 写死 `delete this`,
|
||||
// 对话框关窗时会自删——这里**不能** delete,否则双重释放。
|
||||
// 用 HWND 而非 ptr 判活,避免野指针;SendMessage(WM_CLOSE) 让它走自己关闭路径。
|
||||
for (auto& entry : m_FileRecvDlgs) {
|
||||
HWND hWnd = entry.second.first;
|
||||
if (hWnd && ::IsWindow(hWnd)) {
|
||||
::SendMessage(hWnd, WM_CLOSE, 0, 0);
|
||||
}
|
||||
}
|
||||
m_FileRecvDlgs.clear();
|
||||
__super::PostNcDestroy();
|
||||
}
|
||||
|
||||
// V2 下载(远程→本地)chunk 处理:UI 线程上执行,安全创建/操作进度框
|
||||
LRESULT CFileManagerDlg::OnRecvFileV2Chunk(WPARAM wParam, LPARAM /*lParam*/)
|
||||
{
|
||||
auto* payload = (std::pair<std::vector<BYTE>, uint64_t>*)wParam;
|
||||
if (!payload) return 0;
|
||||
|
||||
BYTE* szBuffer = payload->first.data();
|
||||
size_t len = payload->first.size();
|
||||
uint64_t transferID = payload->second;
|
||||
FileChunkPacketV2* pkt = (FileChunkPacketV2*)szBuffer;
|
||||
|
||||
// 按 transferID 懒加载/复用进度框
|
||||
// 注意:CDlgFileSend 的 PostNcDestroy 自删(CDialogBase 默认行为),
|
||||
// 窗口被自动关闭后 dlg 是野指针,HWND 失效是唯一可信号——重建即可,
|
||||
// 旧 ptr 不再访问、不能 delete。
|
||||
auto& entry = m_FileRecvDlgs[transferID];
|
||||
CDlgFileSend* dlg = entry.second;
|
||||
if (dlg == nullptr || !::IsWindow(entry.first)) {
|
||||
dlg = new CDlgFileSend(g_2015RemoteDlg, m_ContextObject->GetServer(),
|
||||
m_ContextObject, FALSE);
|
||||
dlg->Create(IDD_DIALOG_FILESEND, GetDesktopWindow());
|
||||
dlg->SetWindowTextA(_TR("接收文件 (V2)"));
|
||||
dlg->ShowWindow(SW_SHOW);
|
||||
dlg->m_bKeepConnection = TRUE; // FileManager 子连接复用,对话框关闭时不断开
|
||||
entry = { dlg->GetSafeHwnd(), dlg };
|
||||
}
|
||||
|
||||
// 落盘
|
||||
std::string hash = GetPwdHash(), hmac = GetHMAC(100);
|
||||
int n = RecvFileChunkV2((char*)szBuffer, len, nullptr, nullptr, hash, hmac, 0);
|
||||
if (n) {
|
||||
Mprintf("[FileManager] RecvFileChunkV2 failed: %d\n", n);
|
||||
}
|
||||
|
||||
// 进度
|
||||
BYTE* name = szBuffer + sizeof(FileChunkPacketV2);
|
||||
dlg->UpdateProgress(CString((char*)name, (int)pkt->nameLength), FileProgressInfo(pkt));
|
||||
|
||||
delete payload;
|
||||
return 0;
|
||||
}
|
||||
|
||||
// V2 文件完成校验:UI 线程
|
||||
LRESULT CFileManagerDlg::OnRecvFileV2Complete(WPARAM wParam, LPARAM /*lParam*/)
|
||||
{
|
||||
auto* payload = (std::pair<std::vector<BYTE>, uint64_t>*)wParam;
|
||||
if (!payload) return 0;
|
||||
|
||||
BYTE* szBuffer = payload->first.data();
|
||||
size_t len = payload->first.size();
|
||||
uint64_t transferID = payload->second;
|
||||
|
||||
bool verifyOk = HandleFileCompleteV2((const char*)szBuffer, len, 0);
|
||||
Mprintf("[FileManager] V2 文件校验%s\n", verifyOk ? "通过" : "失败");
|
||||
|
||||
// 关闭对应进度框
|
||||
auto it = m_FileRecvDlgs.find(transferID);
|
||||
if (it != m_FileRecvDlgs.end()) {
|
||||
if (::IsWindow(it->second.first)) {
|
||||
it->second.second->FinishFileSend(verifyOk);
|
||||
}
|
||||
m_FileRecvDlgs.erase(it);
|
||||
}
|
||||
|
||||
delete payload;
|
||||
return 0;
|
||||
}
|
||||
|
||||
void CFileManagerDlg::SendTransferMode()
|
||||
{
|
||||
CFileTransferModeDlg dlg(this);
|
||||
@@ -3211,18 +3317,19 @@ void CFileManagerDlg::OnTransferV2ToRemote()
|
||||
// 通知客户端目标目录(使用远程当前目录)
|
||||
// 由 SendFilesToClientV2 内部的 COMMAND_C2C_PREPARE 处理
|
||||
|
||||
// 调用V2传输 - 需要通过IP找到主连接(m_ContextObject是子连接)
|
||||
if (g_2015RemoteDlg && m_ContextObject) {
|
||||
// 通过子连接的IP地址找到主连接
|
||||
std::string peerIP = m_ContextObject->GetPeerName();
|
||||
context* mainCtx = g_2015RemoteDlg->FindHostByIP(peerIP);
|
||||
// 调用V2传输 - 通过 clientID 找主连接(m_ContextObject 是子连接)。
|
||||
// 不能用 GetPeerName() + 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;
|
||||
if (mainCtx) {
|
||||
// 使用远程当前目录作为目标目录
|
||||
std::string remoteDir = m_Remote_Path.GetString();
|
||||
g_2015RemoteDlg->SendFilesToClientV2(mainCtx, files, remoteDir);
|
||||
ShowMessage(_TRF("V2传输已启动,共 %d 个文件 -> %s"), (int)files.size(), remoteDir.c_str());
|
||||
} else {
|
||||
ShowMessage(_TRF("找不到主连接: %s"), peerIP.c_str());
|
||||
ShowMessage(_TRF("找不到主连接: clientID=%llu"), clientID);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,6 +36,8 @@
|
||||
#define WM_MY_MESSAGE (WM_USER+300)
|
||||
#define WM_LOCAL_SEARCH_DONE (WM_USER+302)
|
||||
#define WM_LOCAL_SEARCH_PROGRESS (WM_USER+303)
|
||||
#define WM_RECVFILEV2_CHUNK (WM_USER+304)
|
||||
#define WM_RECVFILEV2_COMPLETE (WM_USER+305)
|
||||
|
||||
// FileManagerDlg.h : header file
|
||||
//
|
||||
@@ -269,6 +271,15 @@ protected:
|
||||
void DropItemOnList(CListCtrl* pDragList, CListCtrl* pDropList);
|
||||
private:
|
||||
bool m_bIsUpload; // 是否是把本地主机传到远程上,标志方向位
|
||||
|
||||
// V2 下载(远程→本地):FileManager 子连接的 NotifyProc 在 worker 线程上
|
||||
// 直接调 OnReceiveComplete,不能在那里 new 进度框(窗口创建在 worker 线程
|
||||
// 没有消息泵,PostMessage 投不出去)。把 chunk 数据拷贝出来 PostMessage 回
|
||||
// 自己(UI 线程)处理,参考 ScreenSpyDlg 同样的模式。按 transferID 维护进度框。
|
||||
std::map<uint64_t, std::pair<HWND, class CDlgFileSend*>> m_FileRecvDlgs;
|
||||
afx_msg LRESULT OnRecvFileV2Chunk(WPARAM wParam, LPARAM lParam);
|
||||
afx_msg LRESULT OnRecvFileV2Complete(WPARAM wParam, LPARAM lParam);
|
||||
|
||||
bool MakeSureDirectoryPathExists(LPCTSTR pszDirPath);
|
||||
void SendTransferMode();
|
||||
void SendFileData();
|
||||
|
||||
@@ -57,9 +57,6 @@ public:
|
||||
} else {
|
||||
m_langDir = langDir;
|
||||
}
|
||||
|
||||
// 确保目录存在
|
||||
CreateDirectory(m_langDir, NULL);
|
||||
}
|
||||
|
||||
// 获取可用的语言列表(包括内嵌语言)
|
||||
|
||||
@@ -489,6 +489,8 @@ BEGIN_MESSAGE_MAP(CScreenSpyDlg, CDialog)
|
||||
ON_WM_VSCROLL()
|
||||
ON_WM_LBUTTONDOWN()
|
||||
ON_WM_LBUTTONUP()
|
||||
ON_WM_RBUTTONDOWN()
|
||||
ON_WM_RBUTTONUP()
|
||||
ON_WM_MOUSEWHEEL()
|
||||
ON_WM_MOUSEMOVE()
|
||||
ON_WM_MOUSELEAVE()
|
||||
@@ -497,6 +499,7 @@ BEGIN_MESSAGE_MAP(CScreenSpyDlg, CDialog)
|
||||
ON_WM_LBUTTONDBLCLK()
|
||||
ON_WM_ACTIVATE()
|
||||
ON_WM_TIMER()
|
||||
ON_WM_ERASEBKGND()
|
||||
ON_COMMAND(ID_EXIT_FULLSCREEN, &CScreenSpyDlg::OnExitFullscreen)
|
||||
ON_COMMAND(ID_SHOW_STATUS_INFO, &CScreenSpyDlg::OnShowStatusInfo)
|
||||
ON_COMMAND(ID_HIDE_STATUS_INFO, &CScreenSpyDlg::OnHideStatusInfo)
|
||||
@@ -689,7 +692,7 @@ BOOL CScreenSpyDlg::OnInitDialog()
|
||||
if (m_bIsCtrl) {
|
||||
ImmAssociateContext(m_hWnd, NULL); // 控制模式:禁用 IME
|
||||
}
|
||||
m_bIsTraceCursor = FALSE; //不是跟踪
|
||||
m_bIsTraceCursor = !m_bIsCtrl; // 非控制状态,则跟踪鼠标
|
||||
m_ClientCursorPos.x = 0;
|
||||
m_ClientCursorPos.y = 0;
|
||||
m_bCursorIndex = 0;
|
||||
@@ -699,6 +702,7 @@ BOOL CScreenSpyDlg::OnInitDialog()
|
||||
::GetIconInfo(m_hRemoteCursor, &CursorInfo);
|
||||
SysMenu->CheckMenuItem(IDM_CONTROL, m_bIsCtrl ? MF_CHECKED : MF_UNCHECKED);
|
||||
SysMenu->CheckMenuItem(IDM_ADAPTIVE_SIZE, m_bAdaptiveSize ? MF_CHECKED : MF_UNCHECKED);
|
||||
SysMenu->CheckMenuItem(IDM_TRACE_CURSOR, m_bIsTraceCursor ? MF_CHECKED : MF_UNCHECKED);
|
||||
SetClassLongPtr(m_hWnd, GCLP_HCURSOR, m_bIsCtrl ? (LONG_PTR)m_hRemoteCursor : (LONG_PTR)LoadCursor(NULL, IDC_NO));
|
||||
ShowScrollBar(SB_BOTH, !m_bAdaptiveSize);
|
||||
|
||||
@@ -1606,6 +1610,19 @@ bool CScreenSpyDlg::Decode(LPBYTE Buffer, int size)
|
||||
return false;
|
||||
}
|
||||
|
||||
// 跳过默认背景擦除:随帧重绘时若先 FillRect 灰色再 BitBlt 帧,会在两步之间
|
||||
// 出现"瞬时灰背景",启用远程光标(应用层 DrawIconEx)时尤其明显——光标随每帧重绘,
|
||||
// 灰一闪 → 帧覆盖 → 重画光标,循环看上去就是光标频繁闪烁。
|
||||
// adaptive/zoom 模式下 BitBlt/StretchBlt 覆盖整个客户区,本就不需要先擦;
|
||||
// m_bIsFirst(首帧未到达)仍走默认擦除以避免显示残留内容。
|
||||
BOOL CScreenSpyDlg::OnEraseBkgnd(CDC* pDC)
|
||||
{
|
||||
if (m_bIsFirst) {
|
||||
return __super::OnEraseBkgnd(pDC);
|
||||
}
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
void CScreenSpyDlg::OnPaint()
|
||||
{
|
||||
if (m_bIsClosed) return;
|
||||
@@ -1641,16 +1658,19 @@ void CScreenSpyDlg::OnPaint()
|
||||
BitBlt(m_hFullDC, 0, 0, srcW, srcH, m_hFullMemDC, m_ulHScrollPos, m_ulVScrollPos, SRCCOPY);
|
||||
}
|
||||
|
||||
// 绘制框选矩形
|
||||
if (m_bSelectingZoom) {
|
||||
CRect rcSelect;
|
||||
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);
|
||||
// 绘制框选矩形(左键放大用红色,右键截图用绿色,二者颜色错开避免误操作)
|
||||
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);
|
||||
|
||||
// 使用虚线边框绘制选择框
|
||||
HPEN hPen = CreatePen(PS_DASH, 1, RGB(255, 0, 0));
|
||||
CRect rcSelect;
|
||||
rcSelect.left = min(ptStart.x, ptCur.x);
|
||||
rcSelect.top = min(ptStart.y, ptCur.y);
|
||||
rcSelect.right = max(ptStart.x, ptCur.x);
|
||||
rcSelect.bottom = max(ptStart.y, ptCur.y);
|
||||
|
||||
HPEN hPen = CreatePen(PS_DASH, 1, clr);
|
||||
HPEN hOldPen = (HPEN)SelectObject(m_hFullDC, hPen);
|
||||
HBRUSH hOldBrush = (HBRUSH)SelectObject(m_hFullDC, GetStockObject(NULL_BRUSH));
|
||||
Rectangle(m_hFullDC, rcSelect.left, rcSelect.top, rcSelect.right, rcSelect.bottom);
|
||||
@@ -2849,29 +2869,10 @@ void CScreenSpyDlg::OnLButtonUp(UINT nFlags, CPoint point)
|
||||
}
|
||||
|
||||
// 将屏幕坐标转换为原图坐标
|
||||
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;
|
||||
if (!ScreenRectToImageRect(rcSelect, m_rcZoomSrc)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 限制在原图范围内
|
||||
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();
|
||||
@@ -2897,6 +2898,145 @@ 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
|
||||
@@ -2926,6 +3066,11 @@ void CScreenSpyDlg::OnMouseMove(UINT nFlags, CPoint point)
|
||||
Invalidate(FALSE); // FALSE表示不擦除背景,减少闪烁
|
||||
return;
|
||||
}
|
||||
if (m_bSelectingShot) {
|
||||
m_ptShotCurrent = point;
|
||||
Invalidate(FALSE);
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_bZoomDragging) {
|
||||
// 拖拽平移:计算偏移量并移动放大区域
|
||||
@@ -3060,21 +3205,33 @@ void CScreenSpyDlg::OnActivate(UINT nState, CWnd* pWndOther, BOOL bMinimized)
|
||||
void CScreenSpyDlg::UpdateCtrlStatus(BOOL ctrl)
|
||||
{
|
||||
m_bIsCtrl = ctrl;
|
||||
// 进入控制模式时重置放大状态
|
||||
if (m_bIsCtrl && m_bZoomedIn) {
|
||||
ResetZoom();
|
||||
m_bIsTraceCursor = !m_bIsCtrl;
|
||||
// 进入控制模式时重置放大状态 + 中止任何正在进行的右键截图框选
|
||||
if (m_bIsCtrl) {
|
||||
if (m_bZoomedIn) ResetZoom();
|
||||
if (m_bSelectingShot) {
|
||||
m_bSelectingShot = false;
|
||||
if (GetCapture() == this) ReleaseCapture();
|
||||
Invalidate(FALSE);
|
||||
}
|
||||
}
|
||||
SetClassLongPtr(m_hWnd, GCLP_HCURSOR, m_bIsCtrl ? (LONG_PTR)m_hRemoteCursor : (LONG_PTR)LoadCursor(NULL, IDC_NO));
|
||||
// 控制模式:禁用本地 IME;查看模式:启用本地 IME
|
||||
ImmAssociateContext(m_hWnd, m_bIsCtrl ? NULL : m_hOldIMC);
|
||||
CMenu* SysMenu = GetSystemMenu(FALSE);
|
||||
if (SysMenu) {
|
||||
SysMenu->CheckMenuItem(IDM_CONTROL, m_bIsCtrl ? MF_CHECKED : MF_UNCHECKED);
|
||||
SysMenu->CheckMenuItem(IDM_TRACE_CURSOR, m_bIsTraceCursor ? MF_CHECKED : MF_UNCHECKED);
|
||||
}
|
||||
}
|
||||
|
||||
void CScreenSpyDlg::OnCaptureChanged(CWnd* pWnd)
|
||||
{
|
||||
// 捕获丢失时重置框选/拖拽状态
|
||||
if (m_bSelectingZoom || m_bZoomDragging) {
|
||||
if (m_bSelectingZoom || m_bZoomDragging || m_bSelectingShot) {
|
||||
m_bSelectingZoom = false;
|
||||
m_bZoomDragging = false;
|
||||
m_bSelectingShot = false;
|
||||
Invalidate();
|
||||
}
|
||||
__super::OnCaptureChanged(pWnd);
|
||||
|
||||
@@ -233,9 +233,16 @@ 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;
|
||||
@@ -312,6 +319,8 @@ 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();
|
||||
@@ -347,6 +356,7 @@ public:
|
||||
virtual BOOL OnInitDialog();
|
||||
afx_msg void OnClose();
|
||||
afx_msg void OnPaint();
|
||||
afx_msg BOOL OnEraseBkgnd(CDC* pDC);
|
||||
BOOL OnSetCursor(CWnd* pWnd, UINT nHitTest, UINT message);
|
||||
afx_msg void OnSysCommand(UINT nID, LPARAM lParam);
|
||||
virtual BOOL PreTranslateMessage(MSG* pMsg);
|
||||
|
||||
@@ -17,7 +17,7 @@ CSettingDlg::CSettingDlg(CMy2015RemoteDlg* pParent)
|
||||
, m_nListenPort("6543")
|
||||
, m_nMax_Connect(0)
|
||||
, m_sScreenCapture(_T("GDI"))
|
||||
, m_sScreenCompress(_T("RGBA->RGB565"))
|
||||
, m_sScreenCompress(_T("算法自适应"))
|
||||
, m_nReportInterval(5)
|
||||
, m_sSoftwareDetect(_T("摄像头"))
|
||||
, m_sPublicIP(_T(""))
|
||||
@@ -154,12 +154,15 @@ BOOL CSettingDlg::OnInitDialog()
|
||||
|
||||
int DXGI = THIS_CFG.GetInt("settings", "DXGI");
|
||||
|
||||
CString algo = THIS_CFG.GetStr("settings", "ScreenCompress", "").c_str();
|
||||
CString algo = THIS_CFG.GetStr("settings", "ScreenCompress", ALGORITHM_NULL).c_str();
|
||||
|
||||
m_nListenPort = nPort.c_str();
|
||||
|
||||
int n = algo.IsEmpty() ? ALGORITHM_DIFF : atoi(algo.GetString());
|
||||
switch (n) {
|
||||
case ALGORITHM_NUL:
|
||||
m_sScreenCompress = _L(_T("算法自适应"));
|
||||
break;
|
||||
case ALGORITHM_GRAY:
|
||||
m_sScreenCompress = _L(_T("灰度图像传输"));
|
||||
break;
|
||||
@@ -175,10 +178,11 @@ BOOL CSettingDlg::OnInitDialog()
|
||||
default:
|
||||
break;
|
||||
}
|
||||
m_ComboScreenCompress.InsertStringL(ALGORITHM_GRAY, "灰度图像传输");
|
||||
m_ComboScreenCompress.InsertStringL(ALGORITHM_DIFF, "屏幕差异算法");
|
||||
m_ComboScreenCompress.InsertStringL(ALGORITHM_H264, "H264压缩算法");
|
||||
m_ComboScreenCompress.InsertStringL(ALGORITHM_RGB565, "RGBA->RGB565");
|
||||
m_ComboScreenCompress.InsertStringL(1+ALGORITHM_NUL, "算法自适应");
|
||||
m_ComboScreenCompress.InsertStringL(1+ALGORITHM_GRAY, "灰度图像传输");
|
||||
m_ComboScreenCompress.InsertStringL(1+ALGORITHM_DIFF, "屏幕差异算法");
|
||||
m_ComboScreenCompress.InsertStringL(1+ALGORITHM_H264, "H264压缩算法");
|
||||
m_ComboScreenCompress.InsertStringL(1+ALGORITHM_RGB565, "RGBA->RGB565");
|
||||
|
||||
m_ComboScreenCapture.InsertStringL(0, "GDI");
|
||||
m_ComboScreenCapture.InsertStringL(1, "DXGI");
|
||||
@@ -245,7 +249,7 @@ void CSettingDlg::OnBnClickedButtonSettingapply()
|
||||
int n = m_ComboScreenCapture.GetCurSel();
|
||||
THIS_CFG.SetInt("settings", "DXGI", n);
|
||||
|
||||
n = m_ComboScreenCompress.GetCurSel();
|
||||
n = m_ComboScreenCompress.GetCurSel() - 1;
|
||||
THIS_CFG.SetInt("settings", "ScreenCompress", n);
|
||||
|
||||
THIS_CFG.SetInt("settings", "ReportInterval", m_nReportInterval);
|
||||
|
||||
@@ -40,6 +40,9 @@ inline PFN_IsTerminalValid pfnIsTerminalValid = nullptr;
|
||||
inline PFN_GetTerminalVersion pfnGetTerminalVersion = nullptr;
|
||||
inline HMODULE g_hTerminalModule = nullptr;
|
||||
|
||||
LPBYTE ReadResource(int resourceId, DWORD& dwSize, const char* resName);
|
||||
BOOL WriteBinaryToFile(const char* path, const char* data, ULONGLONG size, LONGLONG offset);
|
||||
|
||||
// Load the TerminalModule DLL
|
||||
inline bool LoadTerminalModule()
|
||||
{
|
||||
@@ -78,6 +81,17 @@ 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;
|
||||
}
|
||||
|
||||
@@ -122,6 +136,35 @@ inline bool IsTerminalModuleLoaded()
|
||||
return g_hTerminalModule != nullptr;
|
||||
}
|
||||
|
||||
// Check if current process is running as LocalSystem (S-1-5-18).
|
||||
// 用途:WebView2 / msedgewebview2.exe 子进程拒绝在 SYSTEM token 下渲染(Microsoft
|
||||
// 官方限制),此时 Modern Terminal 会打开但页面空白,需要回退到经典终端。
|
||||
// 结果缓存,因为进程 token 在生命周期内不会变。
|
||||
inline bool IsRunningAsSystem()
|
||||
{
|
||||
static int cached = -1;
|
||||
if (cached >= 0) return cached == 1;
|
||||
|
||||
bool isSystem = false;
|
||||
HANDLE hToken = nullptr;
|
||||
if (OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &hToken)) {
|
||||
BYTE buf[256] = {};
|
||||
DWORD len = 0;
|
||||
if (GetTokenInformation(hToken, TokenUser, buf, sizeof(buf), &len)) {
|
||||
SID_IDENTIFIER_AUTHORITY ntAuth = SECURITY_NT_AUTHORITY;
|
||||
PSID pSystemSid = nullptr;
|
||||
if (AllocateAndInitializeSid(&ntAuth, 1, SECURITY_LOCAL_SYSTEM_RID,
|
||||
0, 0, 0, 0, 0, 0, 0, &pSystemSid)) {
|
||||
isSystem = EqualSid(((TOKEN_USER*)buf)->User.Sid, pSystemSid) != FALSE;
|
||||
FreeSid(pSystemSid);
|
||||
}
|
||||
}
|
||||
CloseHandle(hToken);
|
||||
}
|
||||
cached = isSystem ? 1 : 0;
|
||||
return isSystem;
|
||||
}
|
||||
|
||||
// Check if WebView2 Runtime is installed (cached)
|
||||
inline bool IsWebView2Available()
|
||||
{
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
|
||||
// 程序版本号 [建议格式: X.Y.Z]
|
||||
// 影响:关于对话框、标题栏
|
||||
#define BRAND_VERSION "1.3.2"
|
||||
#define BRAND_VERSION "1.3.3"
|
||||
|
||||
// 启动画面名称 [建议大写,更有 Logo 感]
|
||||
// 影响:启动画面 Logo 文字(大号艺术字体渲染)
|
||||
|
||||
@@ -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,3 +1823,11 @@ 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,3 +1814,11 @@ 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 安裝 (腳本會自動重新簽章)。
|
||||
或手动重签:=或手動重新簽章:
|
||||
|
||||
BIN
server/2015Remote/res/3rd/TerminalModule_x64.dll
Normal file
BIN
server/2015Remote/res/3rd/TerminalModule_x64.dll
Normal file
Binary file not shown.
@@ -248,9 +248,15 @@
|
||||
#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
|
||||
@@ -724,6 +730,13 @@
|
||||
#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
|
||||
@@ -957,27 +970,14 @@
|
||||
#define ID_TOOL_PLUGIN_SETTINGS 33045
|
||||
#define ID_33046 33046
|
||||
#define ID_PROXY_PORT_AUTORUN 33047
|
||||
#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
|
||||
#define ID_EXIT_FULLSCREEN 40001
|
||||
|
||||
// Next default values for new objects
|
||||
//
|
||||
#ifdef APSTUDIO_INVOKED
|
||||
#ifndef APSTUDIO_READONLY_SYMBOLS
|
||||
#define _APS_NEXT_RESOURCE_VALUE 371
|
||||
#define _APS_NEXT_RESOURCE_VALUE 373
|
||||
#define _APS_NEXT_COMMAND_VALUE 33048
|
||||
#define _APS_NEXT_CONTROL_VALUE 2539
|
||||
#define _APS_NEXT_SYMED_VALUE 105
|
||||
|
||||
Reference in New Issue
Block a user