diff --git a/common/PTYHandler.h b/common/PTYHandler.h index 980d7fb..d12fa88 100644 --- a/common/PTYHandler.h +++ b/common/PTYHandler.h @@ -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); diff --git a/common/client_auth_state.h b/common/client_auth_state.h new file mode 100644 index 0000000..f138307 --- /dev/null +++ b/common/client_auth_state.h @@ -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 + acquire/release 内存序保证可见性 +// +// C++17 inline 变量保证多翻译单元共享同一实例,无 ODR 冲突。 +#pragma once + +#include +#include +#include +#include + +#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 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 diff --git a/common/posix_net_helpers.h b/common/posix_net_helpers.h new file mode 100644 index 0000000..7fac087 --- /dev/null +++ b/common/posix_net_helpers.h @@ -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 +#include +#include + +#include "common/logger.h" + +namespace PosixNet { + +// 执行 shell 命令,捕获其 stdout 输出(trim 末尾空白后返回) +inline std::string execCmd(const std::string& cmd) +{ + std::unique_ptr 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 diff --git a/common/rtt_estimator.h b/common/rtt_estimator.h new file mode 100644 index 0000000..1937604 --- /dev/null +++ b/common/rtt_estimator.h @@ -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 + +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 更新 diff --git a/common/sub_conn_thread.h b/common/sub_conn_thread.h new file mode 100644 index 0000000..f4e9792 --- /dev/null +++ b/common/sub_conn_thread.h @@ -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 +#include + +#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(IOCPClient*) +// 返回 nullptr 表示初始化失败(如权限拒绝),线程会跳过 callback 安装直接 leave。 +// OnReadyFn 签名: void(IOCPClient*, HandlerT*) +// handler 装上 callback 后立即调用,可在此发送首包或做额外 setup。 +template +inline void RunSubConnThread(const char* threadName, CreateFn createHandler, OnReadyFn onReady) +{ + try { + std::unique_ptr 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 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()); + } +} diff --git a/linux/main.cpp b/linux/main.cpp index 4c1ff65..b8a4ef4 100644 --- a/linux/main.cpp +++ b/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 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 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 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 handler(new PTYHandler(ClientObject.get())); - ClientObject->setManagerCallBack(handler.get(), IOCPManager::DataProcess, IOCPManager::ReconnectProcess); + RunSubConnThread( + "ShellworkingThread", + [](IOCPClient* c) { return std::unique_ptr(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 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 handler(new ScreenHandler(ClientObject.get())); - ClientObject->setManagerCallBack(handler.get(), IOCPManager::DataProcess, IOCPManager::ReconnectProcess); + RunSubConnThread( + "ScreenworkingThread", + [](IOCPClient* c) { return std::unique_ptr(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 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 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( + "SystemManagerThread", + [](IOCPClient* c) { return std::unique_ptr(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 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 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( + "FileManagerThread", + [](IOCPClient* c) { return std::unique_ptr(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 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; } diff --git a/macos/install.sh b/macos/install.sh index cc87136..b4b6279 100644 --- a/macos/install.sh +++ b/macos/install.sh @@ -25,17 +25,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 +65,11 @@ 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] 签名应用..." +echo "[5/7] 签名应用..." sudo codesign --force --deep --sign - "$APP_DIR" # 6. 添加到登录项(开机自启) diff --git a/macos/main.mm b/macos/main.mm index 22fa9da..f3e2f31 100644 --- a/macos/main.mm +++ b/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 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 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 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 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 handler(new PTYHandler(ClientObject.get())); - ClientObject->setManagerCallBack(handler.get(), IOCPManager::DataProcess, IOCPManager::ReconnectProcess); + RunSubConnThread( + "ShellworkingThread", + [](IOCPClient* c) { return std::unique_ptr(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 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 handler(new ScreenHandler(ClientObject.get())); - if (!handler->init()) { + RunSubConnThread( + "ScreenworkingThread", + [](IOCPClient* c) -> std::unique_ptr { + // macOS ScreenHandler 需要先 init() 申请录屏权限/抓屏 stream,失败 → 返 nullptr + // 让骨架直接 leave,跳过 callback 安装 + auto h = std::unique_ptr(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 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 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( + "FileManagerworkingThread", + [](IOCPClient* c) { return std::unique_ptr(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; } diff --git a/macos/uninstall.sh b/macos/uninstall.sh index 8047bff..cbc8533 100644 --- a/macos/uninstall.sh +++ b/macos/uninstall.sh @@ -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