Compare commits
5 Commits
70354e244c
...
v1.3.3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
95946e0e6a | ||
|
|
ab7a16bec5 | ||
|
|
9acd141cab | ||
|
|
153cbddcf6 | ||
|
|
d46176f4ef |
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 ""
|
||||
|
||||
@@ -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.
@@ -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
|
||||
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());
|
||||
}
|
||||
}
|
||||
BIN
linux/ghost
BIN
linux/ghost
Binary file not shown.
292
linux/main.cpp
292
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,14 +50,7 @@ static std::atomic<bool> g_needResendLogin(false); // 分组变更后需要重
|
||||
// 客户端 ID(V2 文件传输需要)
|
||||
uint64_t g_myClientID = 0;
|
||||
|
||||
// 服务端身份校验:登录消息(签名输入),登录时间,是否已通过校验
|
||||
// 客户端是常驻服务——服务端可能频繁重启 / 长期离线 / 临时不可达,这些都不应让进程退出。
|
||||
// 校验失败仅作"本次连接不可信"处理:断开本连接 + 让外层重连。
|
||||
// g_settingsVerified 在 DataProcess(IO 线程)写、心跳循环(main 线程)和 DataProcess 自身读,
|
||||
// 跨线程访问 → 用 atomic 保证可见性。
|
||||
std::string g_loginMsg;
|
||||
time_t g_loginTime = 0;
|
||||
std::atomic<bool> g_settingsVerified{false};
|
||||
// 服务端身份校验全局状态已抽到 common/client_auth_state.h(namespace ClientAuth)
|
||||
|
||||
// ============== UTF-8 → GBK 编码转换(服务端为 Windows GBK 环境) ==============
|
||||
|
||||
@@ -311,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;
|
||||
}
|
||||
|
||||
@@ -455,11 +365,9 @@ int DataProcess(void* user, PBYTE szBuffer, ULONG ulLength)
|
||||
if (szBuffer == nullptr || ulLength == 0)
|
||||
return TRUE;
|
||||
|
||||
// 服务端身份未通过校验前,仅放行 CMD_MASTERSETTING(校验本身)。
|
||||
// 其它命令一律静默忽略——既防止未授权服务端 spawn 子连接线程做 DoS,
|
||||
// 也防止它发 COMMAND_BYE 之类把客户端进程关掉。
|
||||
if (!g_settingsVerified.load(std::memory_order_acquire) &&
|
||||
szBuffer[0] != CMD_MASTERSETTING) {
|
||||
// 服务端身份未通过校验前,仅放行 CMD_MASTERSETTING(校验本身)。详见
|
||||
// common/client_auth_state.h ClientAuth::IsCommandAllowed 的注释。
|
||||
if (!ClientAuth::IsCommandAllowed(szBuffer[0])) {
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
@@ -494,25 +402,10 @@ int DataProcess(void* user, PBYTE szBuffer, ULONG ulLength)
|
||||
}
|
||||
}
|
||||
} else if (szBuffer[0] == CMD_MASTERSETTING) {
|
||||
int settingSize = ulLength - 1;
|
||||
// 强制要求完整 MasterSettings(包含 Signature 字段)。包不完整 → 视为本次响应异常,
|
||||
// 不更新 g_settingsVerified,让心跳循环的 30s 超时自然把本次连接断开重连。
|
||||
if (settingSize < (int)sizeof(MasterSettings)) {
|
||||
return TRUE;
|
||||
MasterSettings settings;
|
||||
if (!ClientAuth::HandleMasterSettings(szBuffer + 1, (int)ulLength - 1, &settings)) {
|
||||
return TRUE; // 包不全或签名失败:让 30s 超时兜底重连
|
||||
}
|
||||
MasterSettings settings = {};
|
||||
memcpy(&settings, szBuffer + 1, sizeof(MasterSettings));
|
||||
|
||||
// 服务端身份校验:用 g_loginMsg (= szStartTime + "|" + clientID) 与 settings.Signature
|
||||
// 验证签名。失败 → 不立即退出,让超时兜底+重连逻辑处理(避免合法服务端临时
|
||||
// 抖动导致进程退出)
|
||||
extern bool verifyMessage(const std::string& publicKey, BYTE* msg, int len, const std::string& signature);
|
||||
std::string sig((char*)settings.Signature, (char*)settings.Signature + sizeof(settings.Signature));
|
||||
if (!verifyMessage("", (BYTE*)g_loginMsg.data(), (int)g_loginMsg.length(), sig)) {
|
||||
return TRUE; // 同上,不立即退出
|
||||
}
|
||||
g_settingsVerified.store(true, std::memory_order_release);
|
||||
|
||||
if (settings.ReportInterval > 0)
|
||||
g_heartbeatInterval = settings.ReportInterval;
|
||||
Mprintf("** [%p] MasterSettings: ReportInterval=%ds ***\n", user, g_heartbeatInterval);
|
||||
@@ -811,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); }
|
||||
|
||||
// ============== 守护进程 ==============
|
||||
|
||||
@@ -1128,7 +948,7 @@ int main(int argc, char* argv[])
|
||||
logInfo.AddReserved(getFileSize(exePath).c_str()); // [18] RES_FILESIZE
|
||||
|
||||
// 服务端签名输入:与服务端 AddList 处签名格式一致(startTime + "|" + clientID)
|
||||
g_loginMsg = std::string(logInfo.szStartTime) + "|" + std::to_string(g_myClientID);
|
||||
ClientAuth::g_loginMsg = std::string(logInfo.szStartTime) + "|" + std::to_string(g_myClientID);
|
||||
|
||||
// 初始化用户活动检测器(用于心跳包中的 ActiveWnd 字段)
|
||||
ActivityChecker activityChecker;
|
||||
@@ -1143,8 +963,7 @@ int main(int argc, char* argv[])
|
||||
}
|
||||
|
||||
// 进入新连接,重置服务端身份校验状态
|
||||
g_loginTime = time(nullptr);
|
||||
g_settingsVerified.store(false, std::memory_order_release);
|
||||
ClientAuth::OnNewConnection();
|
||||
ClientObject->SendLoginInfo(logInfo.Speed(clock() - c));
|
||||
|
||||
// 心跳保活循环:定时发送心跳包,服务端回复后动态更新 RTT
|
||||
@@ -1174,10 +993,9 @@ int main(int argc, char* argv[])
|
||||
if (!ClientObject->IsRunning() || !ClientObject->IsConnected() || g_bExit != S_CLIENT_NORMAL)
|
||||
break;
|
||||
|
||||
// 登录后 30 秒内必须收到并通过 MasterSettings 校验。失败 → 显式断开本连接
|
||||
// 让外层重连。永不退出进程(客户端是常驻服务,服务端不可达不应该让其自杀)。
|
||||
if (!g_settingsVerified.load(std::memory_order_acquire) && g_loginTime > 0 &&
|
||||
time(nullptr) - g_loginTime > 30) {
|
||||
// 30 秒内未通过 MasterSettings 校验 → 断开本连接让外层重连,
|
||||
// 永不退出进程(详见 ClientAuth::IsTimedOut 注释)。
|
||||
if (ClientAuth::IsTimedOut()) {
|
||||
ClientObject->Disconnect(); // 关闭 socket,防止重连时 fd 泄漏
|
||||
break;
|
||||
}
|
||||
|
||||
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. 添加到登录项(开机自启)
|
||||
|
||||
230
macos/main.mm
230
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,13 +40,7 @@ static std::atomic<bool> g_needResendLogin(false); // 分组变更后需要重
|
||||
// Client ID (calculated from system info, used by ScreenHandler)
|
||||
uint64_t g_myClientID = 0;
|
||||
|
||||
// 服务端身份校验:登录消息(签名输入),登录时间,是否已通过校验
|
||||
// 客户端是常驻服务——服务端可能频繁重启 / 长期离线 / 临时不可达,这些都不应让进程退出。
|
||||
// 校验失败仅作"本次连接不可信"处理:断开本连接 + 让外层重连。
|
||||
// g_settingsVerified 跨线程访问 → 用 atomic 保证可见性。
|
||||
std::string g_loginMsg;
|
||||
time_t g_loginTime = 0;
|
||||
std::atomic<bool> g_settingsVerified{false};
|
||||
// 服务端身份校验全局状态已抽到 common/client_auth_state.h(namespace ClientAuth)
|
||||
|
||||
// 远程地址:当前为写死状态,如需调试,请按实际情况修改
|
||||
CONNECT_ADDRESS g_SETTINGS = { FLAG_GHOST, "91.99.165.207", "443", CLIENT_TYPE_MACOS };
|
||||
@@ -446,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) ==============
|
||||
|
||||
@@ -635,7 +596,7 @@ static void fillLoginInfo(LOGIN_INFOR& info)
|
||||
info.AddReserved(std::to_string(g_myClientID).c_str());
|
||||
|
||||
// 服务端签名输入:与服务端 AddList 处签名格式一致(startTime + "|" + clientID)
|
||||
g_loginMsg = std::string(info.szStartTime) + "|" + std::to_string(g_myClientID);
|
||||
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);
|
||||
@@ -681,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;
|
||||
}
|
||||
|
||||
@@ -803,11 +695,9 @@ int DataProcess(void* user, PBYTE szBuffer, ULONG ulLength)
|
||||
if (szBuffer == nullptr || ulLength == 0)
|
||||
return TRUE;
|
||||
|
||||
// 服务端身份未通过校验前,仅放行 CMD_MASTERSETTING(校验本身)。
|
||||
// 其它命令一律静默忽略——既防止未授权服务端 spawn 子连接线程做 DoS,
|
||||
// 也防止它发 COMMAND_BYE 之类把客户端进程关掉。
|
||||
if (!g_settingsVerified.load(std::memory_order_acquire) &&
|
||||
szBuffer[0] != CMD_MASTERSETTING) {
|
||||
// 服务端身份未通过校验前,仅放行 CMD_MASTERSETTING(校验本身)。详见
|
||||
// common/client_auth_state.h ClientAuth::IsCommandAllowed 的注释。
|
||||
if (!ClientAuth::IsCommandAllowed(szBuffer[0])) {
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
@@ -867,22 +757,10 @@ int DataProcess(void* user, PBYTE szBuffer, ULONG ulLength)
|
||||
}
|
||||
}
|
||||
} else if (szBuffer[0] == CMD_MASTERSETTING) {
|
||||
int settingSize = ulLength - 1;
|
||||
// 包不完整 → 不立即退出,让心跳循环 30s 超时断开重连
|
||||
if (settingSize < (int)sizeof(MasterSettings)) {
|
||||
return TRUE;
|
||||
MasterSettings settings;
|
||||
if (!ClientAuth::HandleMasterSettings(szBuffer + 1, (int)ulLength - 1, &settings)) {
|
||||
return TRUE; // 包不全或签名失败:让 30s 超时兜底重连
|
||||
}
|
||||
MasterSettings settings = {};
|
||||
memcpy(&settings, szBuffer + 1, sizeof(MasterSettings));
|
||||
|
||||
// 签名校验失败 → 同上,让超时兜底+重连处理
|
||||
extern bool verifyMessage(const std::string& publicKey, BYTE* msg, int len, const std::string& signature);
|
||||
std::string sig((char*)settings.Signature, (char*)settings.Signature + sizeof(settings.Signature));
|
||||
if (!verifyMessage("", (BYTE*)g_loginMsg.data(), (int)g_loginMsg.length(), sig)) {
|
||||
return TRUE;
|
||||
}
|
||||
g_settingsVerified.store(true, std::memory_order_release);
|
||||
|
||||
if (settings.ReportInterval > 0)
|
||||
g_heartbeatInterval = settings.ReportInterval;
|
||||
Mprintf("** [%p] MasterSettings: ReportInterval=%ds ***\n", user, g_heartbeatInterval);
|
||||
@@ -1012,8 +890,7 @@ int main(int argc, const char* argv[])
|
||||
}
|
||||
|
||||
// 进入新连接,重置服务端身份校验状态
|
||||
g_loginTime = time(nullptr);
|
||||
g_settingsVerified.store(false, std::memory_order_release);
|
||||
ClientAuth::OnNewConnection();
|
||||
ClientObject->SendLoginInfo(logInfo.Speed(clock() - c));
|
||||
|
||||
// 心跳保活循环:定时发送心跳包,服务端回复后动态更新 RTT
|
||||
@@ -1035,10 +912,9 @@ int main(int argc, const char* argv[])
|
||||
if (!ClientObject->IsRunning() || !ClientObject->IsConnected() || g_bExit != S_CLIENT_NORMAL)
|
||||
break;
|
||||
|
||||
// 登录后 30 秒内必须收到并通过 MasterSettings 校验。失败 → 显式断开本连接
|
||||
// 让外层重连。永不退出进程(客户端是常驻服务,服务端不可达不应该让其自杀)。
|
||||
if (!g_settingsVerified.load(std::memory_order_acquire) && g_loginTime > 0 &&
|
||||
time(nullptr) - g_loginTime > 30) {
|
||||
// 30 秒内未通过 MasterSettings 校验 → 断开本连接让外层重连,
|
||||
// 永不退出进程(详见 ClientAuth::IsTimedOut 注释)。
|
||||
if (ClientAuth::IsTimedOut()) {
|
||||
ClientObject->Disconnect();
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -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.
@@ -5618,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);
|
||||
g_2015RemoteDlg->PostMessageA(WM_SHOWMESSAGE,
|
||||
(WPARAM)new CharMsg(fallbackReason), NULL);
|
||||
g_2015RemoteDlg->SendMessage(WM_OPENSHELLDIALOG, 0, (LPARAM)ContextObject);
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -328,6 +328,7 @@
|
||||
<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();
|
||||
|
||||
@@ -136,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 文字(大号艺术字体渲染)
|
||||
|
||||
@@ -1824,3 +1824,10 @@ IOCP
|
||||
无法识别远程主机=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:
|
||||
|
||||
@@ -1815,3 +1815,10 @@ IOCP
|
||||
无法识别远程主机=无法识别远程主机
|
||||
没有远程历史记录=没有远程历史记录
|
||||
算法自适应=算法自适应
|
||||
; Build Dialog - Traditional Chinese Translation
|
||||
; Format: Simplified Chinese=Traditional Chinese
|
||||
; 用途: 生成 macOS 客户端成功提示新增的 3 条文案
|
||||
|
||||
提示: macOS 端 binary 已被修改导致签名失效,直接运行会被系统强杀。=提示: macOS 端 binary 已被修改導致簽章失效,直接執行會被系統強制終止。
|
||||
推荐: 拷贝到 macOS 后运行 install.sh 安装 (脚本会自动重签)。=推薦: 複製到 macOS 後執行 install.sh 安裝 (腳本會自動重新簽章)。
|
||||
或手动重签:=或手動重新簽章:
|
||||
|
||||
@@ -249,9 +249,10 @@
|
||||
#define IDB_BITMAP_CANCELSHARE 369
|
||||
#define IDD_DIALOG_PLUGIN_SETTINGS 370
|
||||
#define IDD_DIALOG_TRIGGER_SETTINGS 371
|
||||
#define IDR_BINARY7 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
|
||||
@@ -976,7 +977,7 @@
|
||||
//
|
||||
#ifdef APSTUDIO_INVOKED
|
||||
#ifndef APSTUDIO_READONLY_SYMBOLS
|
||||
#define _APS_NEXT_RESOURCE_VALUE 372
|
||||
#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