10 Commits

Author SHA1 Message Date
yuanyuanxiang
95946e0e6a Release v1.3.3 2026-05-10 20:01:13 +02:00
yuanyuanxiang
ab7a16bec5 Feature: Support building macOS client via "Build-Dialog" 2026-05-10 19:46:48 +02:00
yuanyuanxiang
9acd141cab Fix: Modern Terminal blank under SYSTEM; precise reason in info list 2026-05-10 17:36:46 +02:00
yuanyuanxiang
153cbddcf6 Fix: V2 file transfer broken via FileManager dialog (both directions) 2026-05-10 13:50:04 +02:00
yuanyuanxiang
d46176f4ef Refactor: extract Linux/macOS client shared code into common 2026-05-10 10:15:14 +02:00
yuanyuanxiang
70354e244c Improve: Add adaptive screen algorithm option and set to default
Fix: send Windows client path/username as UTF-8 (consistent with CLIENT_CAP_UTF8), keep client ID stable across upgrade
2026-05-09 23:13:24 +02:00
yuanyuanxiang
a354f1ed86 Improve: Embed Modern Terminal DLL in master's resources
Fix: keep Linux/macOS client alive across server restarts; gate all commands on auth-verified state to neutralize unauthorized servers
2026-05-09 00:43:55 +02:00
yuanyuanxiang
f85cc8b86c Fix: Linux client UTF-8 path/active-window garbled on server 2026-05-08 14:03:45 +02:00
yuanyuanxiang
bc06fd5af5 Feature: Linux/macOS server-identity gate via libsign.a
fix remote-cursor flicker on Windows controller
2026-05-08 12:39:59 +02:00
yuanyuanxiang
731ff7a894 Feature: right-click region screenshot in non-control mode 2026-05-08 09:27:19 +02:00
46 changed files with 1294 additions and 471 deletions

11
.gitignore vendored
View File

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

View File

@@ -525,6 +525,40 @@ cd macos
## 更新日志
### v1.3.3 (2026.5.10)
**Linux/macOS 客户端深化 & 双层认证安全 & 跨平台共享代码重构**
**新功能:**
- **服务端身份校验Layer 1**Linux/macOS 客户端 HMAC-SHA256 校验服务端身份,未授权服务端无法触发任何子连接
- **子连接认证Layer 2TOKEN_CONN_AUTH**:所有子连接首包签名 + clientID 钉死,解决 NAT/127.0.0.1 路由错位
- **Linux 客户端**H.264 硬件编码(动态加载 libx264、XFixes 光标类型检测、UTF-8 协议能力位
- **macOS 客户端**:文件管理器、远程终端(共享 PTYHandler、剪贴板同步、守护进程模式 (-d)、电源管理、屏幕锁定/空闲检测、CGDisplayStream 推送模式优化
- **主控**屏幕预览缩略图、区域截图、远程桌面缩放、Web 用户按组过滤、嵌入式现代终端、自适应屏幕算法、外部资源覆盖、分组持久化、Build Dialog 支持生成 macOS 客户端
**重构:**
- Linux/macOS 客户端共享代码抽到 `common/`rtt_estimator / client_auth_state / posix_net_helpers / sub_conn_thread减少 ~300 行重复
**改进:**
- 现代终端 SYSTEM 兼容:自动回退到经典终端,信息列表给出精确原因
- `build.ps1` 增加 `vswhere` 兜底VS 装非默认盘也能找到)
- 强制 `/source-charset:utf-8 /execution-charset:.936` 解决英语 Windows 编译中文乱码
- macOS `install.sh` 源 binary 优先级优化(命令行 → 同目录 → build/bin适配分发场景
**Bug 修复:**
- V2 文件传输在文件管理器对话框双向均损坏(上传 IP 路由错乱 + 下载 chunk 未分发)
- 现代终端在 SYSTEM 权限下空白WebView2 不支持 LocalSystem
- Linux 客户端 UTF-8 路径/活动窗口在服务端乱码
- 日志列表表头点击错排序到主机列表
- LVM_SETUNICODEFORMAT 后表头排序失效(补充 HDN_ITEMCLICKW 映射)
- 服务+代理 Release 模式托盘图标不显示
- macOS/Linux 客户端分组变更后未重发 LOGIN_INFOR
- 文件对话框 map 野指针崩溃
- 重连时未清回调导致访问已销毁 handler 崩溃
- MFC 与 Web 远程桌面会话未完全独立
- macOS 锁屏状态远程桌面启动时未唤醒显示器
- MFC 远程桌面触控板双指滚动失效
### v1.3.2 (2026.5.1)
**macOS 客户端 & Web 远程桌面增强**

View File

@@ -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**

View File

@@ -509,6 +509,40 @@ cd macos
## 更新日誌
### v1.3.3 (2026.5.10)
**Linux/macOS 用戶端深化 & 雙層認證安全 & 跨平台共享程式碼重構**
**新功能:**
- **服務端身分校驗Layer 1**Linux/macOS 用戶端 HMAC-SHA256 校驗服務端身分,未授權服務端無法觸發任何子連線
- **子連線認證Layer 2TOKEN_CONN_AUTH**:所有子連線首包簽章 + clientID 鎖定,解決 NAT/127.0.0.1 路由錯位
- **Linux 用戶端**H.264 硬體編碼(動態載入 libx264、XFixes 游標類型偵測、UTF-8 協議能力位
- **macOS 用戶端**:檔案管理員、遠端終端機(共享 PTYHandler、剪貼簿同步、守護程序模式 (-d)、電源管理、螢幕鎖定/閒置偵測、CGDisplayStream 推送模式最佳化
- **主控**螢幕預覽縮圖、區域截圖、遠端桌面縮放、Web 使用者依群組過濾、嵌入式現代終端、自適應螢幕演算法、外部資源覆蓋、群組持久化、Build Dialog 支援產生 macOS 用戶端
**重構:**
- Linux/macOS 用戶端共享程式碼抽到 `common/`rtt_estimator / client_auth_state / posix_net_helpers / sub_conn_thread減少約 300 行重複
**改進:**
- 現代終端 SYSTEM 相容:自動回退到經典終端,資訊列表給出精確原因
- `build.ps1` 新增 `vswhere` 兜底VS 裝在非預設磁碟也能找到)
- 強制 `/source-charset:utf-8 /execution-charset:.936` 解決英語 Windows 編譯中文亂碼
- macOS `install.sh` 來源 binary 優先順序最佳化(命令列 → 同目錄 → build/bin適配分發場景
**Bug 修復:**
- V2 檔案傳輸在檔案管理器對話方塊雙向均損壞(上傳 IP 路由錯亂 + 下載 chunk 未分發)
- 現代終端在 SYSTEM 權限下空白WebView2 不支援 LocalSystem
- Linux 用戶端 UTF-8 路徑/作用視窗在服務端亂碼
- 日誌列表表頭點擊錯誤排序到主機列表
- LVM_SETUNICODEFORMAT 後表頭排序失效(補充 HDN_ITEMCLICKW 對應)
- 服務+代理 Release 模式系統匣圖示不顯示
- macOS/Linux 用戶端群組變更後未重發 LOGIN_INFOR
- 檔案對話方塊 map 中野指標導致崩潰
- 重連時未清回呼導致存取已銷毀 handler 崩潰
- MFC 與 Web 遠端桌面工作階段未完全獨立
- macOS 鎖屏狀態遠端桌面啟動時未喚醒顯示器
- MFC 遠端桌面觸控板雙指捲動失效
### v1.3.2 (2026.5.1)
**macOS 用戶端 & Web 遠端桌面增強**

View File

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

View File

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

View File

@@ -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);

View File

@@ -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;

View File

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

View File

@@ -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
View File

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

View File

@@ -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
View File

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

View File

@@ -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+ JPEGLinux/macOS 不声明,避免服务端发请求
// 后等 4s 超时显示"预览不可用"。
unsigned int caps = CLIENT_CAP_V2 | CLIENT_CAP_UTF8;
#ifdef _WIN32
caps |= CLIENT_CAP_SCREEN_PREVIEW;
#endif
sprintf_s(moduleVersion, "%s-%04X", DLL_VERSION, caps);
}
LOGIN_INFOR& Speed(unsigned long speed)
{

View File

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

50
common/rtt_estimator.h Normal file
View File

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

70
common/sub_conn_thread.h Normal file
View File

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

View File

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

Binary file not shown.

BIN
linux/lib/libsign.a Normal file

Binary file not shown.

View File

@@ -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); // 分组变更后需要重
// 客户端 IDV2 文件传输需要)
uint64_t g_myClientID = 0;
// 服务端身份校验全局状态已抽到 common/client_auth_state.hnamespace ClientAuth
// ============== UTF-8 → GBK 编码转换(服务端为 Windows GBK 环境) ==============
static std::string utf8ToGbk(const std::string& utf8)
@@ -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;
}
// 限制最小 RTORFC 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;
// 备选 wgetUbuntu 默认自带)
r = execCmd("wget -qO- --timeout=" + t + " \"" + url + "\" 2>/dev/null");
return r;
}
// 获取公网 IP轮询多个查询源与 Windows 端一致)
std::string getPublicIP()
{
static const char* urls[] = {
"https://checkip.amazonaws.com",
"https://api.ipify.org",
"https://ipinfo.io/ip",
"https://icanhazip.com",
"https://ifconfig.me/ip",
};
for (auto& url : urls) {
std::string ip = httpGet(url, 3);
// 简单校验:非空且看起来像 IP含有点号长度合理
if (!ip.empty() && ip.find('.') != std::string::npos && ip.size() <= 45) {
Mprintf("getPublicIP: %s (from %s)\n", ip.c_str(), url);
return ip;
}
}
Mprintf("getPublicIP: all sources failed\n");
return "";
}
// 从 JSON 字符串中提取指定 key 的值(简易解析,不依赖 jsoncpp
// 支持格式: "key": "value" 或 "key":"value"
static std::string jsonExtract(const std::string& json, const std::string& key)
{
std::string needle = "\"" + key + "\"";
size_t pos = json.find(needle);
if (pos == std::string::npos) return "";
pos = json.find(':', pos + needle.size());
if (pos == std::string::npos) return "";
pos = json.find('"', pos + 1);
if (pos == std::string::npos) return "";
size_t end = json.find('"', pos + 1);
if (end == std::string::npos) return "";
return json.substr(pos + 1, end - pos - 1);
}
// 获取 IP 地理位置(通过 ipinfo.io与 Windows 端一致)
std::string getGeoLocation(const std::string& ip)
{
if (ip.empty()) return "";
std::string json = httpGet("https://ipinfo.io/" + ip + "/json", 5);
if (json.empty()) return "";
std::string country = jsonExtract(json, "country");
std::string city = jsonExtract(json, "city");
if (city.empty() && country.empty()) return "";
if (city.empty()) return country;
if (country.empty()) return city;
return city + ", " + country;
}
// execCmd / httpGet / getPublicIP / jsonExtract / getGeoLocation 已抽到
// common/posix_net_helpers.hnamespace PosixNet。下面保留同名 wrapper避免
// 改动调用点。Linux 历史调用风格保留:自由函数无 namespace。
static inline std::string execCmd(const std::string& cmd) { return PosixNet::execCmd(cmd); }
static inline std::string httpGet(const std::string& url, int timeoutSec = 5) { return PosixNet::httpGet(url, timeoutSec); }
static inline std::string jsonExtract(const std::string& json, const std::string& key) { return PosixNet::jsonExtract(json, key); }
inline std::string getPublicIP() { return PosixNet::getPublicIP(); }
inline std::string getGeoLocation(const std::string& ip){ return PosixNet::getGeoLocation(ip); }
// ============== 守护进程 ==============
@@ -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();

View File

@@ -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 CommonCryptoHMAC-SHA256CCHmac 在 libSystem
# 中已被 Cocoa/CoreFoundation 等链接自动引入,故此处无需额外 framework
"${CMAKE_SOURCE_DIR}/lib/libsign.a"
)
# Compiler flags

BIN
macos/ghost Normal file

Binary file not shown.

View File

@@ -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

Binary file not shown.

View File

@@ -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.hnamespace 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.hnamespace PosixNet
// 这里保留同名 wrapper 避免改动调用点。Linux 端额外的 jsonExtract / getGeoLocation
// macOS 暂未使用,需要时直接用 PosixNet:: 命名空间访问。
static inline std::string execCmd(const std::string& cmd) { return PosixNet::execCmd(cmd); }
static inline std::string httpGet(const std::string& url, int timeoutSec = 5) { return PosixNet::httpGet(url, timeoutSec); }
static inline std::string getPublicIP() { return PosixNet::getPublicIP(); }
// ============== Install Time (persistent storage) ==============
@@ -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)
void* ShellworkingThread(void* /*param*/)
{
// 过滤异常值RTT应在合理范围内 (0, 30000] 毫秒
if (rtt_ms <= 0 || rtt_ms > 30000)
return;
const double alpha = 1.0 / 8;
const double beta = 1.0 / 4;
double rtt = rtt_ms / 1000.0;
if (!initialized) {
srtt = rtt;
rttvar = rtt / 2.0;
rto = srtt + 4.0 * rttvar;
initialized = true;
} else {
rttvar = (1.0 - beta) * rttvar + beta * std::fabs(srtt - rtt);
srtt = (1.0 - alpha) * srtt + alpha * rtt;
rto = srtt + 4.0 * rttvar;
}
// 限制最小 RTORFC 6298 推荐 1 秒)
if (rto < 1.0) rto = 1.0;
}
};
RttEstimator g_rttEstimator;
int g_heartbeatInterval = 5; // 心跳间隔(秒),默认 5 秒,后续可由服务端动态调整
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();

View File

@@ -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.

View File

@@ -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"},

View File

@@ -104,6 +104,7 @@
<OpenMPSupport>false</OpenMPSupport>
<DisableSpecificWarnings>4018;4244;4267;4819;4838</DisableSpecificWarnings>
<LanguageStandard>stdcpp17</LanguageStandard>
<AdditionalOptions>/source-charset:utf-8 /execution-charset:.936 %(AdditionalOptions)</AdditionalOptions>
</ClCompile>
<Link>
<SubSystem>Windows</SubSystem>
@@ -138,6 +139,7 @@
<OpenMPSupport>false</OpenMPSupport>
<DisableSpecificWarnings>4018;4244;4267;4819;4838</DisableSpecificWarnings>
<LanguageStandard>stdcpp17</LanguageStandard>
<AdditionalOptions>/source-charset:utf-8 /execution-charset:.936 %(AdditionalOptions)</AdditionalOptions>
</ClCompile>
<Link>
<SubSystem>Windows</SubSystem>
@@ -172,6 +174,7 @@
<OpenMPSupport>false</OpenMPSupport>
<DisableSpecificWarnings>4018;4244;4267;4819;4838</DisableSpecificWarnings>
<LanguageStandard>stdcpp17</LanguageStandard>
<AdditionalOptions>/source-charset:utf-8 /execution-charset:.936 %(AdditionalOptions)</AdditionalOptions>
</ClCompile>
<Link>
<SubSystem>Windows</SubSystem>
@@ -208,6 +211,7 @@
<OpenMPSupport>false</OpenMPSupport>
<DisableSpecificWarnings>4018;4244;4267;4819;4838</DisableSpecificWarnings>
<LanguageStandard>stdcpp17</LanguageStandard>
<AdditionalOptions>/source-charset:utf-8 /execution-charset:.936 %(AdditionalOptions)</AdditionalOptions>
</ClCompile>
<Link>
<SubSystem>Windows</SubSystem>
@@ -231,6 +235,7 @@
</ItemDefinitionGroup>
<ItemGroup>
<None Include="..\..\linux\ghost" />
<None Include="..\..\macos\ghost" />
<None Include="..\..\Release\ghost.exe" />
<None Include="..\..\Release\SCLoader.exe" />
<None Include="..\..\Release\ServerDll.dll" />
@@ -241,6 +246,8 @@
<None Include="..\..\x64\Release\ServerDll.dll" />
<None Include="..\..\x64\Release\TestRun.exe" />
<None Include="..\..\x64\Release\TinyRun.dll" />
<None Include="lang\en_US.ini" />
<None Include="lang\zh_TW.ini" />
<None Include="res\1.cur" />
<None Include="res\2.cur" />
<None Include="res\2015Remote.ico" />
@@ -250,6 +257,7 @@
<None Include="res\3rd\rcedit.exe" />
<None Include="res\3rd\SCLoader_32.exe" />
<None Include="res\3rd\SCLoader_64.exe" />
<None Include="res\3rd\TerminalModule_x64.dll" />
<None Include="res\3rd\upx.exe" />
<None Include="res\4.cur" />
<None Include="res\arrow.cur" />

View File

@@ -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" />

View File

@@ -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()) {

View File

@@ -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);
};

View File

@@ -8,8 +8,13 @@
#include "InputDlg.h"
#include "ZstdArchive.h"
#include "2015RemoteDlg.h"
#include "CDlgFileSend.h"
#include <Shlobj.h>
// V2 接收用:定义在 CPasswordDlg.cpp按本仓约定就地前置声明
std::string GetPwdHash();
std::string GetHMAC(int offset);
#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
@@ -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() + FindHostByIPNAT/frpc/反代场景下子连接的 socket peer
// 常是 127.0.0.1 或内网地址,跟主连接登录时存的 RES_CLIENT_PUBIP 对不上,
// 会找到错误的 ctx 或返回 NULL剪贴板 V2 走 FindHost(clientID) 没此问题)。
if (g_2015RemoteDlg) {
uint64_t clientID = GetClientID();
context* mainCtx = clientID ? g_2015RemoteDlg->FindHost(clientID) : nullptr;
if (mainCtx) {
// 使用远程当前目录作为目标目录
std::string remoteDir = m_Remote_Path.GetString();
g_2015RemoteDlg->SendFilesToClientV2(mainCtx, files, remoteDir);
ShowMessage(_TRF("V2传输已启动共 %d 个文件 -> %s"), (int)files.size(), remoteDir.c_str());
} else {
ShowMessage(_TRF("找不到主连接: %s"), peerIP.c_str());
ShowMessage(_TRF("找不到主连接: clientID=%llu"), clientID);
}
}
}

View File

@@ -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();

View File

@@ -57,9 +57,6 @@ public:
} else {
m_langDir = langDir;
}
// 确保目录存在
CreateDirectory(m_langDir, NULL);
}
// 获取可用的语言列表(包括内嵌语言)

View File

@@ -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] 区域裁出来,写成独立 BMP24bpp 或 32bpp 由源图决定)
void CScreenSpyDlg::SaveRegionScreenshot(const CRect& rcImage)
{
if (!m_BitmapInfor_Full || !m_BitmapData_Full) return;
if (rcImage.Width() <= 0 || rcImage.Height() <= 0) return;
auto path = GetScreenShotPath(this, m_IPAddress, _TR("位图文件(*.bmp)|*.bmp|"), "bmp");
if (path.empty()) return;
// 源 DIB 是 BGR 24bpp 或 BGRA 32bppbottom-upbiHeight > 0
const BITMAPINFOHEADER& srcHdr = m_BitmapInfor_Full->bmiHeader;
int bpp = srcHdr.biBitCount;
if (bpp != 24 && bpp != 32) return; // 仅支持当前实际使用的两种位深
int srcW = srcHdr.biWidth;
int srcH = srcHdr.biHeight;
int srcStride = ((srcW * bpp + 31) / 32) * 4;
int dstW = rcImage.Width();
int dstH = rcImage.Height();
int dstStride = ((dstW * bpp + 31) / 32) * 4;
int dstSize = dstStride * dstH;
std::vector<BYTE> dstPixels(dstSize, 0);
const BYTE* srcBase = (const BYTE*)m_BitmapData_Full;
// bottom-up原图第 y 行(从顶起算)位于 srcBase + (srcH - 1 - y) * srcStride
int byteX = rcImage.left * (bpp / 8);
int copyBytes = dstW * (bpp / 8);
for (int y = 0; y < dstH; ++y) {
int srcRowFromTop = rcImage.top + y;
int srcRowOffset = (srcH - 1 - srcRowFromTop) * srcStride + byteX;
int dstRowOffset = (dstH - 1 - y) * dstStride;
memcpy(&dstPixels[dstRowOffset], &srcBase[srcRowOffset], copyBytes);
}
// 拼装 BITMAPINFO裁剪后只需要 BITMAPINFOHEADER24/32bpp 不需要调色板)
BITMAPINFO dstBmi = {};
dstBmi.bmiHeader = srcHdr;
dstBmi.bmiHeader.biWidth = dstW;
dstBmi.bmiHeader.biHeight = dstH;
dstBmi.bmiHeader.biSizeImage = dstSize;
dstBmi.bmiHeader.biCompression = BI_RGB;
if (WriteBitmap(&dstBmi, dstPixels.data(), path)) {
m_strSaveNotice = path;
m_nSaveNoticeTime = GetTickCount64();
}
}
BOOL CScreenSpyDlg::OnMouseWheel(UINT nFlags, short zDelta, CPoint pt)
{
// 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);

View File

@@ -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);

View File

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

View File

@@ -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()
{

View File

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

View File

@@ -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:

View File

@@ -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 安裝 (腳本會自動重新簽章)。
或手动重签:=或手動重新簽章:

Binary file not shown.

View File

@@ -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