2 Commits

Author SHA1 Message Date
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
11 changed files with 569 additions and 432 deletions

View File

@@ -205,11 +205,14 @@ private:
// Disable zsh session save/restore (causes errors in PTY) // Disable zsh session save/restore (causes errors in PTY)
setenv("SHELL_SESSIONS_DISABLE", "1", 1); 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) { 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 #else
// Linux locale settings (C.UTF-8 is most portable) // Linux locale settings (C.UTF-8 is most portable)
setenv("LANG", "C.UTF-8", 1); 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

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

@@ -32,6 +32,10 @@
#include "common/logger.h" #include "common/logger.h"
#define XXH_INLINE_ALL #define XXH_INLINE_ALL
#include "common/xxhash.h" #include "common/xxhash.h"
#include "common/rtt_estimator.h"
#include "common/client_auth_state.h"
#include "common/posix_net_helpers.h"
#include "common/sub_conn_thread.h"
#include "LinuxConfig.h" #include "LinuxConfig.h"
int DataProcess(void* user, PBYTE szBuffer, ULONG ulLength); int DataProcess(void* user, PBYTE szBuffer, ULONG ulLength);
@@ -46,14 +50,7 @@ static std::atomic<bool> g_needResendLogin(false); // 分组变更后需要重
// 客户端 IDV2 文件传输需要) // 客户端 IDV2 文件传输需要)
uint64_t g_myClientID = 0; uint64_t g_myClientID = 0;
// 服务端身份校验:登录消息(签名输入),登录时间,是否已通过校验 // 服务端身份校验全局状态已抽到 common/client_auth_state.hnamespace ClientAuth
// 客户端是常驻服务——服务端可能频繁重启 / 长期离线 / 临时不可达,这些都不应让进程退出。
// 校验失败仅作"本次连接不可信"处理:断开本连接 + 让外层重连。
// g_settingsVerified 在 DataProcessIO 线程写、心跳循环main 线程)和 DataProcess 自身读,
// 跨线程访问 → 用 atomic 保证可见性。
std::string g_loginMsg;
time_t g_loginTime = 0;
std::atomic<bool> g_settingsVerified{false};
// ============== UTF-8 → GBK 编码转换(服务端为 Windows GBK 环境) ============== // ============== UTF-8 → GBK 编码转换(服务端为 Windows GBK 环境) ==============
@@ -311,142 +308,55 @@ private:
}; };
// ============== 心跳保活 & RTT 估算 ============== // ============== 心跳保活 & RTT 估算 ==============
// RttEstimator + g_rttEstimator + g_heartbeatInterval 已抽到 common/rtt_estimator.h
// RTT 估算器(参考 RFC 6298 算法,与 Windows 端 KernelManager 一致)
struct RttEstimator {
double srtt = 0.0; // 平滑 RTT (秒)
double rttvar = 0.0; // RTT 波动 (秒)
double rto = 0.0; // 超时时间 (秒)
bool initialized = false;
void update_from_sample(double rtt_ms)
{
// 过滤异常值RTT应在合理范围内 (0, 30000] 毫秒
if (rtt_ms <= 0 || rtt_ms > 30000)
return;
const double alpha = 1.0 / 8;
const double beta = 1.0 / 4;
double rtt = rtt_ms / 1000.0;
if (!initialized) {
srtt = rtt;
rttvar = rtt / 2.0;
rto = srtt + 4.0 * rttvar;
initialized = true;
} else {
rttvar = (1.0 - beta) * rttvar + beta * std::fabs(srtt - rtt);
srtt = (1.0 - alpha) * srtt + alpha * rtt;
rto = srtt + 4.0 * rttvar;
}
// 限制最小 RTORFC 6298 推荐 1 秒)
if (rto < 1.0) rto = 1.0;
}
};
RttEstimator g_rttEstimator;
int g_heartbeatInterval = 5; // 默认心跳间隔(秒),可被服务端 CMD_MASTERSETTING 更新
// PTYHandler moved to common/PTYHandler.h (shared between Linux and macOS) // PTYHandler moved to common/PTYHandler.h (shared between Linux and macOS)
void* ShellworkingThread(void* param) void* ShellworkingThread(void* /*param*/)
{ {
try { RunSubConnThread<PTYHandler>(
std::unique_ptr<IOCPClient> ClientObject(new IOCPClient(g_bExit, true)); "ShellworkingThread",
void* clientAddr = ClientObject.get(); [](IOCPClient* c) { return std::unique_ptr<PTYHandler>(new PTYHandler(c)); },
Mprintf(">>> Enter ShellworkingThread [%p]\n", clientAddr); [](IOCPClient* c, PTYHandler*) {
// 子连接:开启 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);
BYTE bToken = TOKEN_TERMINAL_START; BYTE bToken = TOKEN_TERMINAL_START;
ClientObject->Send2Server((char*)&bToken, 1); c->Send2Server((char*)&bToken, 1);
Mprintf(">>> ShellworkingThread [%p] Send: TOKEN_TERMINAL_START\n", clientAddr); Mprintf(">>> ShellworkingThread [%p] Send: TOKEN_TERMINAL_START\n", c);
while (ClientObject->IsRunning() && ClientObject->IsConnected() && S_CLIENT_NORMAL == g_bExit) });
Sleep(1000);
// 清除回调,防止重连线程访问已销毁的 handler
ClientObject->setManagerCallBack(nullptr, nullptr, nullptr);
}
Mprintf(">>> Leave ShellworkingThread [%p]\n", clientAddr);
} catch (const std::exception& e) {
Mprintf("*** ShellworkingThread exception: %s ***\n", e.what());
}
return NULL; return NULL;
} }
void* ScreenworkingThread(void* param) void* ScreenworkingThread(void* /*param*/)
{ {
try { RunSubConnThread<ScreenHandler>(
std::unique_ptr<IOCPClient> ClientObject(new IOCPClient(g_bExit, true)); "ScreenworkingThread",
void* clientAddr = ClientObject.get(); [](IOCPClient* c) { return std::unique_ptr<ScreenHandler>(new ScreenHandler(c)); },
Mprintf(">>> Enter ScreenworkingThread [%p]\n", clientAddr); [](IOCPClient* c, ScreenHandler* h) {
// 子连接:开启 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);
// 连接后立即发送完整的 BITMAPINFO 包(与 Windows 端 ScreenManager 流程一致) // 连接后立即发送完整的 BITMAPINFO 包(与 Windows 端 ScreenManager 流程一致)
handler->SendBitmapInfo(); h->SendBitmapInfo();
Mprintf(">>> ScreenworkingThread [%p] Send: TOKEN_BITMAPINFO\n", clientAddr); Mprintf(">>> ScreenworkingThread [%p] Send: TOKEN_BITMAPINFO\n", c);
while (ClientObject->IsRunning() && ClientObject->IsConnected() && S_CLIENT_NORMAL == g_bExit) });
Sleep(1000);
// 清除回调,防止重连线程访问已销毁的 handler
ClientObject->setManagerCallBack(nullptr, nullptr, nullptr);
}
Mprintf(">>> Leave ScreenworkingThread [%p]\n", clientAddr);
} catch (const std::exception& e) {
Mprintf("*** ScreenworkingThread exception: %s ***\n", e.what());
}
return NULL; return NULL;
} }
void* SystemManagerThread(void* param) void* SystemManagerThread(void* /*param*/)
{ {
try { RunSubConnThread<SystemManager>(
std::unique_ptr<IOCPClient> ClientObject(new IOCPClient(g_bExit, true)); "SystemManagerThread",
void* clientAddr = ClientObject.get(); [](IOCPClient* c) { return std::unique_ptr<SystemManager>(new SystemManager(c)); },
Mprintf(">>> Enter SystemManagerThread [%p]\n", clientAddr); [](IOCPClient* c, SystemManager*) {
// 子连接:开启 auth。Linux IOCPClient 不带 m_conn显式传入 g_myClientID。 Mprintf(">>> SystemManagerThread [%p] Send: TOKEN_PSLIST\n", c);
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());
}
return NULL; return NULL;
} }
void* FileManagerThread(void* param) void* FileManagerThread(void* /*param*/)
{ {
try { RunSubConnThread<FileManager>(
std::unique_ptr<IOCPClient> ClientObject(new IOCPClient(g_bExit, true)); "FileManagerThread",
void* clientAddr = ClientObject.get(); [](IOCPClient* c) { return std::unique_ptr<FileManager>(new FileManager(c)); },
Mprintf(">>> Enter FileManagerThread [%p]\n", clientAddr); [](IOCPClient* c, FileManager*) {
// 子连接:开启 auth。Linux IOCPClient 不带 m_conn显式传入 g_myClientID。 Mprintf(">>> FileManagerThread [%p] Send: TOKEN_DRIVE_LIST\n", c);
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());
}
return NULL; return NULL;
} }
@@ -455,11 +365,9 @@ int DataProcess(void* user, PBYTE szBuffer, ULONG ulLength)
if (szBuffer == nullptr || ulLength == 0) if (szBuffer == nullptr || ulLength == 0)
return TRUE; return TRUE;
// 服务端身份未通过校验前,仅放行 CMD_MASTERSETTING校验本身 // 服务端身份未通过校验前,仅放行 CMD_MASTERSETTING校验本身详见
// 其它命令一律静默忽略——既防止未授权服务端 spawn 子连接线程做 DoS // common/client_auth_state.h ClientAuth::IsCommandAllowed 的注释。
// 也防止它发 COMMAND_BYE 之类把客户端进程关掉。 if (!ClientAuth::IsCommandAllowed(szBuffer[0])) {
if (!g_settingsVerified.load(std::memory_order_acquire) &&
szBuffer[0] != CMD_MASTERSETTING) {
return TRUE; return TRUE;
} }
@@ -494,25 +402,10 @@ int DataProcess(void* user, PBYTE szBuffer, ULONG ulLength)
} }
} }
} else if (szBuffer[0] == CMD_MASTERSETTING) { } else if (szBuffer[0] == CMD_MASTERSETTING) {
int settingSize = ulLength - 1; MasterSettings settings;
// 强制要求完整 MasterSettings包含 Signature 字段)。包不完整 → 视为本次响应异常, if (!ClientAuth::HandleMasterSettings(szBuffer + 1, (int)ulLength - 1, &settings)) {
// 不更新 g_settingsVerified让心跳循环的 30s 超时自然把本次连接断开重连 return TRUE; // 包不全或签名失败:让 30s 超时兜底重连
if (settingSize < (int)sizeof(MasterSettings)) {
return TRUE;
} }
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) if (settings.ReportInterval > 0)
g_heartbeatInterval = settings.ReportInterval; g_heartbeatInterval = settings.ReportInterval;
Mprintf("** [%p] MasterSettings: ReportInterval=%ds ***\n", user, g_heartbeatInterval); Mprintf("** [%p] MasterSettings: ReportInterval=%ds ***\n", user, g_heartbeatInterval);
@@ -811,87 +704,14 @@ std::string getScreenResolution()
return "0:0*0"; return "0:0*0";
} }
// 执行命令并返回输出 // execCmd / httpGet / getPublicIP / jsonExtract / getGeoLocation 已抽到
static std::string execCmd(const std::string& cmd) // common/posix_net_helpers.hnamespace PosixNet。下面保留同名 wrapper避免
{ // 改动调用点。Linux 历史调用风格保留:自由函数无 namespace。
std::unique_ptr<FILE, decltype(&pclose)> pipe(popen(cmd.c_str(), "r"), pclose); static inline std::string execCmd(const std::string& cmd) { return PosixNet::execCmd(cmd); }
if (!pipe) return ""; static inline std::string httpGet(const std::string& url, int timeoutSec = 5) { return PosixNet::httpGet(url, timeoutSec); }
char buf[4096]; static inline std::string jsonExtract(const std::string& json, const std::string& key) { return PosixNet::jsonExtract(json, key); }
std::string result; inline std::string getPublicIP() { return PosixNet::getPublicIP(); }
while (fgets(buf, sizeof(buf), pipe.get())) { inline std::string getGeoLocation(const std::string& ip){ return PosixNet::getGeoLocation(ip); }
result += buf;
}
// 去除尾部空白
while (!result.empty() && (result.back() == '\n' || result.back() == '\r' || result.back() == ' '))
result.pop_back();
return result;
}
// HTTP GET 请求(优先 curl备选 wget
static std::string httpGet(const std::string& url, int timeoutSec = 5)
{
std::string t = std::to_string(timeoutSec);
// 优先使用 curl
std::string r = execCmd("curl -s --max-time " + t + " \"" + url + "\" 2>/dev/null");
if (!r.empty()) return r;
// 备选 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;
}
// ============== 守护进程 ============== // ============== 守护进程 ==============
@@ -1128,7 +948,7 @@ int main(int argc, char* argv[])
logInfo.AddReserved(getFileSize(exePath).c_str()); // [18] RES_FILESIZE logInfo.AddReserved(getFileSize(exePath).c_str()); // [18] RES_FILESIZE
// 服务端签名输入:与服务端 AddList 处签名格式一致startTime + "|" + clientID // 服务端签名输入:与服务端 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 字段) // 初始化用户活动检测器(用于心跳包中的 ActiveWnd 字段)
ActivityChecker activityChecker; ActivityChecker activityChecker;
@@ -1143,8 +963,7 @@ int main(int argc, char* argv[])
} }
// 进入新连接,重置服务端身份校验状态 // 进入新连接,重置服务端身份校验状态
g_loginTime = time(nullptr); ClientAuth::OnNewConnection();
g_settingsVerified.store(false, std::memory_order_release);
ClientObject->SendLoginInfo(logInfo.Speed(clock() - c)); ClientObject->SendLoginInfo(logInfo.Speed(clock() - c));
// 心跳保活循环:定时发送心跳包,服务端回复后动态更新 RTT // 心跳保活循环:定时发送心跳包,服务端回复后动态更新 RTT
@@ -1174,10 +993,9 @@ int main(int argc, char* argv[])
if (!ClientObject->IsRunning() || !ClientObject->IsConnected() || g_bExit != S_CLIENT_NORMAL) if (!ClientObject->IsRunning() || !ClientObject->IsConnected() || g_bExit != S_CLIENT_NORMAL)
break; break;
// 登录后 30 秒内必须收到并通过 MasterSettings 校验。失败显式断开本连接 // 30 秒内通过 MasterSettings 校验 → 断开本连接让外层重连,
// 让外层重连。永不退出进程(客户端是常驻服务,服务端不可达不应该让其自杀)。 // 永不退出进程(详见 ClientAuth::IsTimedOut 注释)。
if (!g_settingsVerified.load(std::memory_order_acquire) && g_loginTime > 0 && if (ClientAuth::IsTimedOut()) {
time(nullptr) - g_loginTime > 30) {
ClientObject->Disconnect(); // 关闭 socket防止重连时 fd 泄漏 ClientObject->Disconnect(); // 关闭 socket防止重连时 fd 泄漏
break; break;
} }

View File

@@ -25,17 +25,17 @@ echo ""
set -e set -e
# 1. 停止旧进程 # 1. 停止旧进程
echo "[1/6] 停止旧进程..." echo "[1/7] 停止旧进程..."
pkill -9 -f "$APP_BIN" 2>/dev/null || true pkill -9 -f "$APP_BIN" 2>/dev/null || true
# 2. 重置系统权限(关键步骤!避免权限缓存导致空白桌面) # 2. 重置系统权限(关键步骤!避免权限缓存导致空白桌面)
echo "[2/6] 重置系统权限..." echo "[2/7] 重置系统权限..."
echo " (这会清除屏幕录制和辅助功能的旧授权,需要重新授权)" echo " (这会清除屏幕录制和辅助功能的旧授权,需要重新授权)"
tccutil reset ScreenCapture 2>/dev/null || true tccutil reset ScreenCapture 2>/dev/null || true
tccutil reset Accessibility 2>/dev/null || true tccutil reset Accessibility 2>/dev/null || true
# 3. 创建应用程序包 # 3. 创建应用程序包
echo "[3/6] 创建应用程序..." echo "[3/7] 创建应用程序..."
sudo rm -rf "$APP_DIR" sudo rm -rf "$APP_DIR"
sudo mkdir -p "$APP_DIR/Contents/MacOS" sudo mkdir -p "$APP_DIR/Contents/MacOS"
sudo mkdir -p "$APP_DIR/Contents/Resources" sudo mkdir -p "$APP_DIR/Contents/Resources"
@@ -65,11 +65,11 @@ sudo tee "$APP_DIR/Contents/Info.plist" > /dev/null << 'EOF'
EOF EOF
# 4. 清除隔离属性 # 4. 清除隔离属性
echo "[4/6] 清除隔离属性..." echo "[4/7] 清除隔离属性..."
sudo xattr -cr "$APP_DIR" sudo xattr -cr "$APP_DIR"
# 5. 签名应用 # 5. 签名应用
echo "[5/6] 签名应用..." echo "[5/7] 签名应用..."
sudo codesign --force --deep --sign - "$APP_DIR" sudo codesign --force --deep --sign - "$APP_DIR"
# 6. 添加到登录项(开机自启) # 6. 添加到登录项(开机自启)

View File

@@ -19,6 +19,10 @@
#import "../client/IOCPClient.h" #import "../client/IOCPClient.h"
#define XXH_INLINE_ALL #define XXH_INLINE_ALL
#include "../common/xxhash.h" #include "../common/xxhash.h"
#include "../common/rtt_estimator.h"
#include "../common/client_auth_state.h"
#include "../common/posix_net_helpers.h"
#include "../common/sub_conn_thread.h"
#import "Permissions.h" #import "Permissions.h"
#import "ScreenHandler.h" #import "ScreenHandler.h"
#import "InputHandler.h" #import "InputHandler.h"
@@ -36,13 +40,7 @@ static std::atomic<bool> g_needResendLogin(false); // 分组变更后需要重
// Client ID (calculated from system info, used by ScreenHandler) // Client ID (calculated from system info, used by ScreenHandler)
uint64_t g_myClientID = 0; uint64_t g_myClientID = 0;
// 服务端身份校验:登录消息(签名输入),登录时间,是否已通过校验 // 服务端身份校验全局状态已抽到 common/client_auth_state.hnamespace ClientAuth
// 客户端是常驻服务——服务端可能频繁重启 / 长期离线 / 临时不可达,这些都不应让进程退出。
// 校验失败仅作"本次连接不可信"处理:断开本连接 + 让外层重连。
// g_settingsVerified 跨线程访问 → 用 atomic 保证可见性。
std::string g_loginMsg;
time_t g_loginTime = 0;
std::atomic<bool> g_settingsVerified{false};
// 远程地址:当前为写死状态,如需调试,请按实际情况修改 // 远程地址:当前为写死状态,如需调试,请按实际情况修改
CONNECT_ADDRESS g_SETTINGS = { FLAG_GHOST, "91.99.165.207", "443", CLIENT_TYPE_MACOS }; CONNECT_ADDRESS g_SETTINGS = { FLAG_GHOST, "91.99.165.207", "443", CLIENT_TYPE_MACOS };
@@ -446,49 +444,12 @@ static bool hasCameraDevice()
// ============== Public IP ============== // ============== Public IP ==============
// Execute command and return output // Execute command and return output
static std::string execCmd(const std::string& cmd) // execCmd / httpGet / getPublicIP 已抽到 common/posix_net_helpers.hnamespace PosixNet
{ // 这里保留同名 wrapper 避免改动调用点。Linux 端额外的 jsonExtract / getGeoLocation
std::unique_ptr<FILE, decltype(&pclose)> pipe(popen(cmd.c_str(), "r"), pclose); // macOS 暂未使用,需要时直接用 PosixNet:: 命名空间访问。
if (!pipe) return ""; static inline std::string execCmd(const std::string& cmd) { return PosixNet::execCmd(cmd); }
char buf[4096]; static inline std::string httpGet(const std::string& url, int timeoutSec = 5) { return PosixNet::httpGet(url, timeoutSec); }
std::string result; static inline std::string getPublicIP() { return PosixNet::getPublicIP(); }
while (fgets(buf, sizeof(buf), pipe.get())) {
result += buf;
}
// Trim trailing whitespace
while (!result.empty() && (result.back() == '\n' || result.back() == '\r' || result.back() == ' '))
result.pop_back();
return result;
}
// HTTP GET using curl (macOS has curl built-in)
static std::string httpGet(const std::string& url, int timeoutSec = 5)
{
std::string t = std::to_string(timeoutSec);
return execCmd("curl -s --max-time " + t + " \"" + url + "\" 2>/dev/null");
}
// Get public IP (try multiple sources)
static std::string getPublicIP()
{
static const char* urls[] = {
"https://checkip.amazonaws.com",
"https://api.ipify.org",
"https://ipinfo.io/ip",
"https://icanhazip.com",
"https://ifconfig.me/ip",
};
for (auto& url : urls) {
std::string ip = httpGet(url, 3);
// Validate: non-empty, contains dot, reasonable length
if (!ip.empty() && ip.find('.') != std::string::npos && ip.size() <= 45) {
NSLog(@"getPublicIP: %s (from %s)", ip.c_str(), url);
return ip;
}
}
NSLog(@"getPublicIP: all sources failed");
return "";
}
// ============== Install Time (persistent storage) ============== // ============== Install Time (persistent storage) ==============
@@ -635,7 +596,7 @@ static void fillLoginInfo(LOGIN_INFOR& info)
info.AddReserved(std::to_string(g_myClientID).c_str()); info.AddReserved(std::to_string(g_myClientID).c_str());
// 服务端签名输入:与服务端 AddList 处签名格式一致startTime + "|" + clientID // 服务端签名输入:与服务端 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", NSLog(@"LOGIN_INFOR filled: OS=%s, Host=%s, CPU=%dMHz, PubIP=%s, ClientID=%llu",
osVer.c_str(), hostname.c_str(), info.dwCPUMHz, pubIP.c_str(), g_myClientID); osVer.c_str(), hostname.c_str(), info.dwCPUMHz, pubIP.c_str(), g_myClientID);
@@ -681,120 +642,51 @@ static void daemonize()
} }
// ============== Main Entry Point ============== // ============== Main Entry Point ==============
// RttEstimator + g_rttEstimator + g_heartbeatInterval 已抽到 common/rtt_estimator.h
// RTT 估算器(参考 RFC 6298 算法,与 Windows 端 KernelManager 一致) void* ShellworkingThread(void* /*param*/)
struct RttEstimator {
double srtt = 0.0; // 平滑 RTT (秒)
double rttvar = 0.0; // RTT 波动 (秒)
double rto = 0.0; // 超时时间 (秒)
bool initialized = false;
void update_from_sample(double rtt_ms)
{
// 过滤异常值RTT应在合理范围内 (0, 30000] 毫秒
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 { RunSubConnThread<PTYHandler>(
std::unique_ptr<IOCPClient> ClientObject(new IOCPClient(g_bExit, true)); "ShellworkingThread",
void* clientAddr = ClientObject.get(); [](IOCPClient* c) { return std::unique_ptr<PTYHandler>(new PTYHandler(c)); },
NSLog(@">>> Enter ShellworkingThread [%p]", clientAddr); [](IOCPClient* c, PTYHandler*) {
// 子连接:开启 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);
BYTE bToken = TOKEN_TERMINAL_START; BYTE bToken = TOKEN_TERMINAL_START;
ClientObject->Send2Server((char*)&bToken, 1); c->Send2Server((char*)&bToken, 1);
NSLog(@">>> ShellworkingThread [%p] Send: TOKEN_TERMINAL_START", clientAddr); Mprintf(">>> ShellworkingThread [%p] Send: TOKEN_TERMINAL_START\n", c);
while (ClientObject->IsRunning() && ClientObject->IsConnected() && S_CLIENT_NORMAL == g_bExit) });
Sleep(1000);
// 清除回调,防止重连线程访问已销毁的 handler
ClientObject->setManagerCallBack(nullptr, nullptr, nullptr);
}
NSLog(@">>> Leave ShellworkingThread [%p]", clientAddr);
} catch (const std::exception& e) {
NSLog(@"*** ShellworkingThread exception: %s ***", e.what());
}
return NULL; return NULL;
} }
void* ScreenworkingThread(void* param) void* ScreenworkingThread(void* /*param*/)
{ {
try { RunSubConnThread<ScreenHandler>(
std::unique_ptr<IOCPClient> ClientObject(new IOCPClient(g_bExit, true)); "ScreenworkingThread",
void* clientAddr = ClientObject.get(); [](IOCPClient* c) -> std::unique_ptr<ScreenHandler> {
Mprintf(">>> Enter ScreenworkingThread [%p]\n", clientAddr); // macOS ScreenHandler 需要先 init() 申请录屏权限/抓屏 stream失败 → 返 nullptr
// 子连接:开启 auth。macOS IOCPClient 不带 m_conn显式传入 g_myClientID。 // 让骨架直接 leave跳过 callback 安装
ClientObject->EnableSubConnAuth(true, g_myClientID); auto h = std::unique_ptr<ScreenHandler>(new ScreenHandler(c));
if (!g_bExit && ClientObject->ConnectServer(g_SETTINGS.ServerIP(), g_SETTINGS.ServerPort())) { if (!h->init()) {
std::unique_ptr<ScreenHandler> handler(new ScreenHandler(ClientObject.get()));
if (!handler->init()) {
Mprintf("*** ScreenHandler initialization failed (no permission?) ***\n"); 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 流程一致) // 连接后立即发送完整的 BITMAPINFO 包(与 Windows 端 ScreenManager 流程一致)
handler->sendBitmapInfo(); h->sendBitmapInfo();
Mprintf(">>> ScreenworkingThread [%p] Send: TOKEN_BITMAPINFO\n", clientAddr); Mprintf(">>> ScreenworkingThread [%p] Send: TOKEN_BITMAPINFO\n", c);
while (ClientObject->IsRunning() && ClientObject->IsConnected() && S_CLIENT_NORMAL == g_bExit) });
Sleep(1000);
// 清除回调,防止重连线程访问已销毁的 handler
ClientObject->setManagerCallBack(nullptr, nullptr, nullptr);
}
Mprintf(">>> Leave ScreenworkingThread [%p]\n", clientAddr);
} catch (const std::exception& e) {
Mprintf("*** ScreenworkingThread exception: %s ***\n", e.what());
}
return NULL; return NULL;
} }
void* FileManagerworkingThread(void* param) void* FileManagerworkingThread(void* /*param*/)
{ {
try { RunSubConnThread<FileManager>(
std::unique_ptr<IOCPClient> ClientObject(new IOCPClient(g_bExit, true)); "FileManagerworkingThread",
void* clientAddr = ClientObject.get(); [](IOCPClient* c) { return std::unique_ptr<FileManager>(new FileManager(c)); },
Mprintf(">>> Enter FileManagerworkingThread [%p]\n", clientAddr); [](IOCPClient* c, FileManager*) {
// 子连接:开启 auth。macOS IOCPClient 不带 m_conn显式传入 g_myClientID。 Mprintf(">>> FileManagerworkingThread [%p] initialized\n", c);
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());
}
return NULL; return NULL;
} }
@@ -803,11 +695,9 @@ int DataProcess(void* user, PBYTE szBuffer, ULONG ulLength)
if (szBuffer == nullptr || ulLength == 0) if (szBuffer == nullptr || ulLength == 0)
return TRUE; return TRUE;
// 服务端身份未通过校验前,仅放行 CMD_MASTERSETTING校验本身 // 服务端身份未通过校验前,仅放行 CMD_MASTERSETTING校验本身详见
// 其它命令一律静默忽略——既防止未授权服务端 spawn 子连接线程做 DoS // common/client_auth_state.h ClientAuth::IsCommandAllowed 的注释。
// 也防止它发 COMMAND_BYE 之类把客户端进程关掉。 if (!ClientAuth::IsCommandAllowed(szBuffer[0])) {
if (!g_settingsVerified.load(std::memory_order_acquire) &&
szBuffer[0] != CMD_MASTERSETTING) {
return TRUE; return TRUE;
} }
@@ -867,22 +757,10 @@ int DataProcess(void* user, PBYTE szBuffer, ULONG ulLength)
} }
} }
} else if (szBuffer[0] == CMD_MASTERSETTING) { } else if (szBuffer[0] == CMD_MASTERSETTING) {
int settingSize = ulLength - 1; MasterSettings settings;
// 包不完整 → 不立即退出,让心跳循环 30s 超时断开重连 if (!ClientAuth::HandleMasterSettings(szBuffer + 1, (int)ulLength - 1, &settings)) {
if (settingSize < (int)sizeof(MasterSettings)) { return TRUE; // 包不全或签名失败:让 30s 超时兜底重连
return TRUE;
} }
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) if (settings.ReportInterval > 0)
g_heartbeatInterval = settings.ReportInterval; g_heartbeatInterval = settings.ReportInterval;
Mprintf("** [%p] MasterSettings: ReportInterval=%ds ***\n", user, g_heartbeatInterval); Mprintf("** [%p] MasterSettings: ReportInterval=%ds ***\n", user, g_heartbeatInterval);
@@ -1012,8 +890,7 @@ int main(int argc, const char* argv[])
} }
// 进入新连接,重置服务端身份校验状态 // 进入新连接,重置服务端身份校验状态
g_loginTime = time(nullptr); ClientAuth::OnNewConnection();
g_settingsVerified.store(false, std::memory_order_release);
ClientObject->SendLoginInfo(logInfo.Speed(clock() - c)); ClientObject->SendLoginInfo(logInfo.Speed(clock() - c));
// 心跳保活循环:定时发送心跳包,服务端回复后动态更新 RTT // 心跳保活循环:定时发送心跳包,服务端回复后动态更新 RTT
@@ -1035,10 +912,9 @@ int main(int argc, const char* argv[])
if (!ClientObject->IsRunning() || !ClientObject->IsConnected() || g_bExit != S_CLIENT_NORMAL) if (!ClientObject->IsRunning() || !ClientObject->IsConnected() || g_bExit != S_CLIENT_NORMAL)
break; break;
// 登录后 30 秒内必须收到并通过 MasterSettings 校验。失败显式断开本连接 // 30 秒内通过 MasterSettings 校验 → 断开本连接让外层重连,
// 让外层重连。永不退出进程(客户端是常驻服务,服务端不可达不应该让其自杀)。 // 永不退出进程(详见 ClientAuth::IsTimedOut 注释)。
if (!g_settingsVerified.load(std::memory_order_acquire) && g_loginTime > 0 && if (ClientAuth::IsTimedOut()) {
time(nullptr) - g_loginTime > 30) {
ClientObject->Disconnect(); ClientObject->Disconnect();
break; break;
} }

View File

@@ -7,11 +7,11 @@ echo "=== GhostClient 卸载程序 ==="
echo "" echo ""
# 1. 停止进程 # 1. 停止进程
echo "[1/3] 停止进程..." echo "[1/4] 停止进程..."
pkill -9 -f "$APP_DIR" 2>/dev/null || true pkill -9 -f "$APP_DIR" 2>/dev/null || true
# 2. 删除文件 # 2. 删除文件
echo "[2/3] 删除文件..." echo "[2/4] 删除文件..."
sudo rm -rf "$APP_DIR" sudo rm -rf "$APP_DIR"
rm -rf ~/.config/ghost 2>/dev/null || true rm -rf ~/.config/ghost 2>/dev/null || true
rm -f /tmp/ghost.log 2>/dev/null || true rm -f /tmp/ghost.log 2>/dev/null || true

View File

@@ -8,8 +8,13 @@
#include "InputDlg.h" #include "InputDlg.h"
#include "ZstdArchive.h" #include "ZstdArchive.h"
#include "2015RemoteDlg.h" #include "2015RemoteDlg.h"
#include "CDlgFileSend.h"
#include <Shlobj.h> #include <Shlobj.h>
// V2 接收用:定义在 CPasswordDlg.cpp按本仓约定就地前置声明
std::string GetPwdHash();
std::string GetHMAC(int offset);
#ifdef _DEBUG #ifdef _DEBUG
#define new DEBUG_NEW #define new DEBUG_NEW
#undef THIS_FILE #undef THIS_FILE
@@ -176,6 +181,8 @@ BEGIN_MESSAGE_MAP(CFileManagerDlg, CDialog)
ON_MESSAGE(WM_MY_MESSAGE, OnMyMessage) ON_MESSAGE(WM_MY_MESSAGE, OnMyMessage)
ON_MESSAGE(WM_LOCAL_SEARCH_DONE, OnLocalSearchDone) ON_MESSAGE(WM_LOCAL_SEARCH_DONE, OnLocalSearchDone)
ON_MESSAGE(WM_LOCAL_SEARCH_PROGRESS, OnLocalSearchProgress) ON_MESSAGE(WM_LOCAL_SEARCH_PROGRESS, OnLocalSearchProgress)
ON_MESSAGE(WM_RECVFILEV2_CHUNK, &CFileManagerDlg::OnRecvFileV2Chunk)
ON_MESSAGE(WM_RECVFILEV2_COMPLETE, &CFileManagerDlg::OnRecvFileV2Complete)
//}}AFX_MSG_MAP //}}AFX_MSG_MAP
ON_COMMAND(ID_FILEMANGER_COMPRESS, &CFileManagerDlg::OnFilemangerCompress) ON_COMMAND(ID_FILEMANGER_COMPRESS, &CFileManagerDlg::OnFilemangerCompress)
ON_COMMAND(ID_FILEMANGER_UNCOMPRESS, &CFileManagerDlg::OnFilemangerUncompress) ON_COMMAND(ID_FILEMANGER_UNCOMPRESS, &CFileManagerDlg::OnFilemangerUncompress)
@@ -994,6 +1001,28 @@ void CFileManagerDlg::OnReceiveComplete()
break; break;
case TOKEN_CLIENTID: case TOKEN_CLIENTID:
break; break;
case COMMAND_SEND_FILE_V2:
case COMMAND_FILE_COMPLETE_V2: {
// V2 下载(远程→本地):客户端把 chunk 通过 FileManager 子连接推回服务端。
// 此函数在 NotifyProc -> worker 线程上调用。窗口创建/UI 操作必须回 UI 线程,
// 否则消息泵不通,进度框不会显示(落盘可在任意线程,影响仅限 UI
// 拷贝数据后 PostMessage 回自己UI 线程的 OnRecvFileV2Chunk/Complete 处理。
LPBYTE buf = m_ContextObject->m_DeCompressionBuffer.GetBuffer(0);
unsigned len = m_ContextObject->m_DeCompressionBuffer.GetBufferLen();
size_t minSize = (buf[0] == COMMAND_FILE_COMPLETE_V2)
? sizeof(FileCompletePacketV2) : sizeof(FileChunkPacketV2);
if (len >= minSize) {
// 两种结构 cmd/transferID 偏移一致,可共用 FileChunkPacketV2 取 transferID
uint64_t transferID = ((FileChunkPacketV2*)buf)->transferID;
UINT msg = (buf[0] == COMMAND_FILE_COMPLETE_V2)
? WM_RECVFILEV2_COMPLETE : WM_RECVFILEV2_CHUNK;
// 用 std::pair<vector,uint64> 当 wParam 载体UI 端 delete
auto* payload = new std::pair<std::vector<BYTE>, uint64_t>(
std::vector<BYTE>(buf, buf + len), transferID);
PostMessage(msg, (WPARAM)payload, 0);
}
break;
}
default: default:
SendException(); SendException();
break; break;
@@ -2682,10 +2711,87 @@ void CFileManagerDlg::OnLocalStop()
void CFileManagerDlg::PostNcDestroy() void CFileManagerDlg::PostNcDestroy()
{ {
// TODO: Add your specialized code here and/or call the base class // 清理 V2 接收进度框:注意 CDialogBase::PostNcDestroy 写死 `delete this`
// 对话框关窗时会自删——这里**不能** delete否则双重释放。
// 用 HWND 而非 ptr 判活避免野指针SendMessage(WM_CLOSE) 让它走自己关闭路径。
for (auto& entry : m_FileRecvDlgs) {
HWND hWnd = entry.second.first;
if (hWnd && ::IsWindow(hWnd)) {
::SendMessage(hWnd, WM_CLOSE, 0, 0);
}
}
m_FileRecvDlgs.clear();
__super::PostNcDestroy(); __super::PostNcDestroy();
} }
// V2 下载远程→本地chunk 处理UI 线程上执行,安全创建/操作进度框
LRESULT CFileManagerDlg::OnRecvFileV2Chunk(WPARAM wParam, LPARAM /*lParam*/)
{
auto* payload = (std::pair<std::vector<BYTE>, uint64_t>*)wParam;
if (!payload) return 0;
BYTE* szBuffer = payload->first.data();
size_t len = payload->first.size();
uint64_t transferID = payload->second;
FileChunkPacketV2* pkt = (FileChunkPacketV2*)szBuffer;
// 按 transferID 懒加载/复用进度框
// 注意CDlgFileSend 的 PostNcDestroy 自删CDialogBase 默认行为),
// 窗口被自动关闭后 dlg 是野指针HWND 失效是唯一可信号——重建即可,
// 旧 ptr 不再访问、不能 delete。
auto& entry = m_FileRecvDlgs[transferID];
CDlgFileSend* dlg = entry.second;
if (dlg == nullptr || !::IsWindow(entry.first)) {
dlg = new CDlgFileSend(g_2015RemoteDlg, m_ContextObject->GetServer(),
m_ContextObject, FALSE);
dlg->Create(IDD_DIALOG_FILESEND, GetDesktopWindow());
dlg->SetWindowTextA(_TR("接收文件 (V2)"));
dlg->ShowWindow(SW_SHOW);
dlg->m_bKeepConnection = TRUE; // FileManager 子连接复用,对话框关闭时不断开
entry = { dlg->GetSafeHwnd(), dlg };
}
// 落盘
std::string hash = GetPwdHash(), hmac = GetHMAC(100);
int n = RecvFileChunkV2((char*)szBuffer, len, nullptr, nullptr, hash, hmac, 0);
if (n) {
Mprintf("[FileManager] RecvFileChunkV2 failed: %d\n", n);
}
// 进度
BYTE* name = szBuffer + sizeof(FileChunkPacketV2);
dlg->UpdateProgress(CString((char*)name, (int)pkt->nameLength), FileProgressInfo(pkt));
delete payload;
return 0;
}
// V2 文件完成校验UI 线程
LRESULT CFileManagerDlg::OnRecvFileV2Complete(WPARAM wParam, LPARAM /*lParam*/)
{
auto* payload = (std::pair<std::vector<BYTE>, uint64_t>*)wParam;
if (!payload) return 0;
BYTE* szBuffer = payload->first.data();
size_t len = payload->first.size();
uint64_t transferID = payload->second;
bool verifyOk = HandleFileCompleteV2((const char*)szBuffer, len, 0);
Mprintf("[FileManager] V2 文件校验%s\n", verifyOk ? "通过" : "失败");
// 关闭对应进度框
auto it = m_FileRecvDlgs.find(transferID);
if (it != m_FileRecvDlgs.end()) {
if (::IsWindow(it->second.first)) {
it->second.second->FinishFileSend(verifyOk);
}
m_FileRecvDlgs.erase(it);
}
delete payload;
return 0;
}
void CFileManagerDlg::SendTransferMode() void CFileManagerDlg::SendTransferMode()
{ {
CFileTransferModeDlg dlg(this); CFileTransferModeDlg dlg(this);
@@ -3211,18 +3317,19 @@ void CFileManagerDlg::OnTransferV2ToRemote()
// 通知客户端目标目录(使用远程当前目录) // 通知客户端目标目录(使用远程当前目录)
// 由 SendFilesToClientV2 内部的 COMMAND_C2C_PREPARE 处理 // 由 SendFilesToClientV2 内部的 COMMAND_C2C_PREPARE 处理
// 调用V2传输 - 需要通过IP找到主连接m_ContextObject是子连接 // 调用V2传输 - 通过 clientID 找主连接m_ContextObject 是子连接)
if (g_2015RemoteDlg && m_ContextObject) { // 不能用 GetPeerName() + FindHostByIPNAT/frpc/反代场景下子连接的 socket peer
// 通过子连接的IP地址找到主连接 // 常是 127.0.0.1 或内网地址,跟主连接登录时存的 RES_CLIENT_PUBIP 对不上,
std::string peerIP = m_ContextObject->GetPeerName(); // 会找到错误的 ctx 或返回 NULL剪贴板 V2 走 FindHost(clientID) 没此问题)。
context* mainCtx = g_2015RemoteDlg->FindHostByIP(peerIP); if (g_2015RemoteDlg) {
uint64_t clientID = GetClientID();
context* mainCtx = clientID ? g_2015RemoteDlg->FindHost(clientID) : nullptr;
if (mainCtx) { if (mainCtx) {
// 使用远程当前目录作为目标目录
std::string remoteDir = m_Remote_Path.GetString(); std::string remoteDir = m_Remote_Path.GetString();
g_2015RemoteDlg->SendFilesToClientV2(mainCtx, files, remoteDir); g_2015RemoteDlg->SendFilesToClientV2(mainCtx, files, remoteDir);
ShowMessage(_TRF("V2传输已启动共 %d 个文件 -> %s"), (int)files.size(), remoteDir.c_str()); ShowMessage(_TRF("V2传输已启动共 %d 个文件 -> %s"), (int)files.size(), remoteDir.c_str());
} else { } else {
ShowMessage(_TRF("找不到主连接: %s"), peerIP.c_str()); ShowMessage(_TRF("找不到主连接: clientID=%llu"), clientID);
} }
} }
} }

View File

@@ -36,6 +36,8 @@
#define WM_MY_MESSAGE (WM_USER+300) #define WM_MY_MESSAGE (WM_USER+300)
#define WM_LOCAL_SEARCH_DONE (WM_USER+302) #define WM_LOCAL_SEARCH_DONE (WM_USER+302)
#define WM_LOCAL_SEARCH_PROGRESS (WM_USER+303) #define WM_LOCAL_SEARCH_PROGRESS (WM_USER+303)
#define WM_RECVFILEV2_CHUNK (WM_USER+304)
#define WM_RECVFILEV2_COMPLETE (WM_USER+305)
// FileManagerDlg.h : header file // FileManagerDlg.h : header file
// //
@@ -269,6 +271,15 @@ protected:
void DropItemOnList(CListCtrl* pDragList, CListCtrl* pDropList); void DropItemOnList(CListCtrl* pDragList, CListCtrl* pDropList);
private: private:
bool m_bIsUpload; // 是否是把本地主机传到远程上,标志方向位 bool m_bIsUpload; // 是否是把本地主机传到远程上,标志方向位
// V2 下载远程→本地FileManager 子连接的 NotifyProc 在 worker 线程上
// 直接调 OnReceiveComplete不能在那里 new 进度框(窗口创建在 worker 线程
// 没有消息泵PostMessage 投不出去)。把 chunk 数据拷贝出来 PostMessage 回
// 自己UI 线程)处理,参考 ScreenSpyDlg 同样的模式。按 transferID 维护进度框。
std::map<uint64_t, std::pair<HWND, class CDlgFileSend*>> m_FileRecvDlgs;
afx_msg LRESULT OnRecvFileV2Chunk(WPARAM wParam, LPARAM lParam);
afx_msg LRESULT OnRecvFileV2Complete(WPARAM wParam, LPARAM lParam);
bool MakeSureDirectoryPathExists(LPCTSTR pszDirPath); bool MakeSureDirectoryPathExists(LPCTSTR pszDirPath);
void SendTransferMode(); void SendTransferMode();
void SendFileData(); void SendFileData();