Refactor: extract Linux/macOS client shared code into common
This commit is contained in:
@@ -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());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user