Files
SimpleRemoter/macos/main.mm

951 lines
34 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#import <Cocoa/Cocoa.h>
#import <sys/sysctl.h>
#import <sys/stat.h>
#import <mach/mach.h>
#import <mach-o/dyld.h>
#import <pwd.h>
#import <signal.h>
#import <unistd.h>
#import <fcntl.h>
#import <IOKit/IOKitLib.h>
#import <IOKit/pwr_mgt/IOPMLib.h>
#import <fstream>
#import <thread>
#import <atomic>
#import <memory>
#import <string>
#import <map>
#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"
#import "SystemManager.h"
#import "../common/PTYHandler.h"
#import "../common/FileManager.h"
#import "../common/FileTransferV2.h"
#import "../common/logger.h"
#import "ClipboardHandler.h"
// Global state
static std::atomic<bool> g_running(true);
static std::atomic<bool> g_needResendLogin(false); // 分组变更后需要重发登录信息
// Client ID (calculated from system info, used by ScreenHandler)
uint64_t g_myClientID = 0;
// 服务端身份校验全局状态已抽到 common/client_auth_state.hnamespace ClientAuth
// 远程地址:当前为写死状态,如需调试,请按实际情况修改
CONNECT_ADDRESS g_SETTINGS = { FLAG_GHOST, "91.99.165.207", "443", CLIENT_TYPE_MACOS };
State g_bExit = S_CLIENT_NORMAL;
// ============== Configuration File Functions ==============
// Config path: ~/.config/ghost/config.conf (same as Linux)
// Format: key=value (one per line)
static std::string g_configDir;
static std::string g_configPath;
static std::map<std::string, std::string> g_configData;
// Initialize config paths
static void initConfigPaths()
{
if (!g_configDir.empty()) return; // Already initialized
const char* home = getenv("HOME");
if (!home) {
struct passwd* pw = getpwuid(getuid());
if (pw) home = pw->pw_dir;
}
if (!home) home = "/tmp";
g_configDir = std::string(home) + "/.config/ghost";
g_configPath = g_configDir + "/config.conf";
}
// Recursively create directory
static void mkdirRecursive(const std::string& path)
{
size_t pos = 0;
while ((pos = path.find('/', pos + 1)) != std::string::npos) {
mkdir(path.substr(0, pos).c_str(), 0755);
}
mkdir(path.c_str(), 0755);
}
// Load all config from file
static void loadConfig()
{
initConfigPaths();
g_configData.clear();
std::ifstream file(g_configPath);
if (!file.is_open()) return;
std::string line;
while (std::getline(file, line)) {
size_t eq = line.find('=');
if (eq != std::string::npos) {
g_configData[line.substr(0, eq)] = line.substr(eq + 1);
}
}
}
// Save all config to file
static void saveConfig()
{
initConfigPaths();
mkdirRecursive(g_configDir);
std::ofstream file(g_configPath, std::ios::trunc);
if (!file.is_open()) {
NSLog(@"Failed to save config to %s", g_configPath.c_str());
return;
}
for (const auto& kv : g_configData) {
file << kv.first << "=" << kv.second << "\n";
}
NSLog(@"Config saved to %s", g_configPath.c_str());
}
// Get config string value
static std::string getConfigStr(const std::string& key, const std::string& def = "")
{
auto it = g_configData.find(key);
return it != g_configData.end() ? it->second : def;
}
// Set config string value
static void setConfigStr(const std::string& key, const std::string& value)
{
g_configData[key] = value;
saveConfig();
}
// Save group name to config file
static void saveGroupName(const std::string& groupName)
{
setConfigStr("group_name", groupName);
NSLog(@"Group name saved: %s", groupName.c_str());
}
// Load group name from config file
static std::string loadGroupName()
{
return getConfigStr("group_name");
}
// ============== System Information Functions ==============
// Get macOS version string (e.g., "macOS 14.0 Sonoma")
static std::string getMacOSVersion()
{
NSOperatingSystemVersion version = [[NSProcessInfo processInfo] operatingSystemVersion];
NSString* versionString = [NSString stringWithFormat:@"macOS %ld.%ld.%ld",
(long)version.majorVersion,
(long)version.minorVersion,
(long)version.patchVersion];
return std::string([versionString UTF8String]);
}
// Get hostname
static std::string getHostname()
{
char hostname[256] = {};
gethostname(hostname, sizeof(hostname));
return std::string(hostname);
}
// Get CPU model and frequency
static std::string getCPUInfo()
{
char buf[256] = {};
size_t size = sizeof(buf);
if (sysctlbyname("machdep.cpu.brand_string", buf, &size, NULL, 0) == 0) {
return std::string(buf);
}
return "Unknown CPU";
}
// Get CPU frequency in MHz
static int getCPUFrequencyMHz()
{
uint64_t freq = 0;
size_t size = sizeof(freq);
if (sysctlbyname("hw.cpufrequency_max", &freq, &size, NULL, 0) == 0) {
return (int)(freq / 1000000);
}
return 0;
}
// Get number of CPU cores
static int getCPUCores()
{
int cores = 0;
size_t size = sizeof(cores);
if (sysctlbyname("hw.ncpu", &cores, &size, NULL, 0) == 0) {
return cores;
}
return 1;
}
// Get total physical memory in GB
static double getMemoryGB()
{
int64_t memSize = 0;
size_t size = sizeof(memSize);
if (sysctlbyname("hw.memsize", &memSize, &size, NULL, 0) == 0) {
return (double)memSize / (1024.0 * 1024.0 * 1024.0);
}
return 0;
}
// Get current username
static std::string getUsername()
{
struct passwd* pw = getpwuid(getuid());
if (pw && pw->pw_name) {
return std::string(pw->pw_name);
}
const char* user = getenv("USER");
return user ? std::string(user) : "unknown";
}
// 读取 IOKit 维护的 IOPlatformUUID与 Windows MachineGuid 等价)
// 这是主板/系统级 UUID由 IOPlatformExpertDevice 服务提供,重装系统通常不变。
// 对应Windows HKLM\Software\Microsoft\Cryptography\MachineGuid
// Linux /etc/machine-id
static std::string getMachineId()
{
std::string result;
io_service_t platformExpert = IOServiceGetMatchingService(
kIOMasterPortDefault,
IOServiceMatching("IOPlatformExpertDevice"));
if (platformExpert != IO_OBJECT_NULL) {
CFTypeRef uuidProperty = IORegistryEntryCreateCFProperty(
platformExpert, CFSTR(kIOPlatformUUIDKey),
kCFAllocatorDefault, 0);
if (uuidProperty != nullptr) {
if (CFGetTypeID(uuidProperty) == CFStringGetTypeID()) {
CFStringRef uuidStr = (CFStringRef)uuidProperty;
char buf[64] = {};
if (CFStringGetCString(uuidStr, buf, sizeof(buf), kCFStringEncodingUTF8)) {
result = buf;
}
}
CFRelease(uuidProperty);
}
IOObjectRelease(platformExpert);
}
return result;
}
// 路径归一化macOS 版):解析符号链接 + 转小写
// realpath 把 /Applications/foo/../bar 之类折回规范形式;
// 小写化保持与 Windows/Linux 跨端一致。macOS HFS+/APFS 默认大小写不敏感,
// 转小写不改变文件标识、但避免路径串大小写差异引起 ID 不同。
static std::string normalizeExePathLower(const std::string& path)
{
char resolved[PATH_MAX] = {};
std::string out;
if (realpath(path.c_str(), resolved) != nullptr) {
out = resolved;
} else {
out = path; // 解析失败:用原值
}
for (auto& c : out) {
if (c >= 'A' && c <= 'Z') c = c - 'A' + 'a';
}
return out;
}
// Get screen resolution
static std::string getScreenResolution()
{
NSScreen* mainScreen = [NSScreen mainScreen];
if (mainScreen) {
NSRect frame = [mainScreen frame];
return [NSString stringWithFormat:@"1:%dx%d",
(int)frame.size.width, (int)frame.size.height].UTF8String;
}
return "0:0x0";
}
// Get executable path
static std::string getExecutablePath()
{
char path[PATH_MAX];
uint32_t size = sizeof(path);
if (_NSGetExecutablePath(path, &size) == 0) {
return std::string(path);
}
return "";
}
// Get current time string (Beijing time, UTC+8)
static std::string getTimeString()
{
NSDateFormatter* formatter = [[NSDateFormatter alloc] init];
[formatter setDateFormat:@"yyyy-MM-dd HH:mm:ss"];
[formatter setTimeZone:[NSTimeZone timeZoneForSecondsFromGMT:8*3600]];
NSString* dateString = [formatter stringFromDate:[NSDate date]];
return std::string([dateString UTF8String]);
}
// Get user idle time in seconds (time since last keyboard/mouse input)
static double getUserIdleTime()
{
// CGEventSourceSecondsSinceLastEventType returns seconds since last event
// kCGEventSourceStateCombinedSessionState includes all input sources
CFTimeInterval idleTime = CGEventSourceSecondsSinceLastEventType(
kCGEventSourceStateCombinedSessionState,
kCGAnyInputEventType
);
// Defensive: ensure non-negative (edge case protection)
return idleTime > 0 ? idleTime : 0;
}
// Check if screen is locked
static bool isScreenLocked()
{
// Method 1: Check CGSession dictionary for screen lock status
CFDictionaryRef sessionDict = CGSessionCopyCurrentDictionary();
if (sessionDict) {
// Check for "CGSSessionScreenIsLocked" key
CFBooleanRef screenLocked = (CFBooleanRef)CFDictionaryGetValue(
sessionDict, CFSTR("CGSSessionScreenIsLocked"));
if (screenLocked && CFBooleanGetValue(screenLocked)) {
CFRelease(sessionDict);
return true;
}
CFRelease(sessionDict);
}
// Method 2: Check if loginwindow is frontmost (screen saver / lock screen)
NSRunningApplication* frontApp = [[NSWorkspace sharedWorkspace] frontmostApplication];
if (frontApp) {
NSString* bundleId = [frontApp bundleIdentifier];
if ([bundleId isEqualToString:@"com.apple.loginwindow"] ||
[bundleId isEqualToString:@"com.apple.ScreenSaver.Engine"]) {
return true;
}
}
return false;
}
// Format time as HH:MM:SS with prefix
static std::string formatStatusTime(const char* prefix, double seconds)
{
int totalSecs = (int)seconds;
int hours = totalSecs / 3600;
int mins = (totalSecs % 3600) / 60;
int secs = totalSecs % 60;
char buffer[64];
snprintf(buffer, sizeof(buffer), "%s: %02d:%02d:%02d", prefix, hours, mins, secs);
return std::string(buffer);
}
// Get active application name or idle/locked status (works for background processes)
static std::string getActiveApp()
{
double idleTime = getUserIdleTime();
// Check if screen is locked first
if (isScreenLocked()) {
return formatStatusTime("Locked", idleTime);
}
// Check user idle time (matches Windows/Linux: 6 seconds threshold)
// If idle for more than 6 seconds, report inactive status
if (idleTime >= 6.0) {
return formatStatusTime("Inactive", idleTime);
}
// Use CGWindowListCopyWindowInfo to get the frontmost window
// This works reliably even when running as a background/nohup process
CFArrayRef windowList = CGWindowListCopyWindowInfo(
kCGWindowListOptionOnScreenOnly | kCGWindowListExcludeDesktopElements,
kCGNullWindowID
);
if (windowList) {
CFIndex count = CFArrayGetCount(windowList);
for (CFIndex i = 0; i < count; i++) {
CFDictionaryRef window = (CFDictionaryRef)CFArrayGetValueAtIndex(windowList, i);
// Get window layer - layer 0 is normal windows
CFNumberRef layerRef = (CFNumberRef)CFDictionaryGetValue(window, kCGWindowLayer);
int layer = 0;
if (layerRef) {
CFNumberGetValue(layerRef, kCFNumberIntType, &layer);
}
// Skip non-normal windows (menu bar, dock, etc.)
if (layer != 0) continue;
// Get owner name (application name)
CFStringRef ownerName = (CFStringRef)CFDictionaryGetValue(window, kCGWindowOwnerName);
if (ownerName) {
char buffer[256] = {};
if (CFStringGetCString(ownerName, buffer, sizeof(buffer), kCFStringEncodingUTF8)) {
CFRelease(windowList);
return std::string(buffer);
}
}
}
CFRelease(windowList);
}
// Fallback to NSWorkspace (may not work for background processes)
NSRunningApplication* app = [[NSWorkspace sharedWorkspace] frontmostApplication];
if (app) {
NSString* name = [app localizedName];
if (name) {
return std::string([name UTF8String]);
}
}
return "";
}
// ============== Check if camera exists ==============
static bool hasCameraDevice()
{
// Most MacBooks have built-in FaceTime camera
// Check model identifier to determine if it's a MacBook
char model[256] = {};
size_t size = sizeof(model);
if (sysctlbyname("hw.model", model, &size, NULL, 0) == 0) {
std::string modelStr(model);
// MacBooks (Air/Pro) always have cameras
if (modelStr.find("MacBook") != std::string::npos) {
return true;
}
// iMac also has camera
if (modelStr.find("iMac") != std::string::npos) {
return true;
}
}
// Mac Mini and Mac Pro typically don't have built-in cameras
return false;
}
// ============== Public IP ==============
// Execute command and return output
// execCmd / httpGet / getPublicIP 已抽到 common/posix_net_helpers.hnamespace PosixNet
// 这里保留同名 wrapper 避免改动调用点。Linux 端额外的 jsonExtract / getGeoLocation
// macOS 暂未使用,需要时直接用 PosixNet:: 命名空间访问。
static inline std::string execCmd(const std::string& cmd) { return PosixNet::execCmd(cmd); }
static inline std::string httpGet(const std::string& url, int timeoutSec = 5) { return PosixNet::httpGet(url, timeoutSec); }
static inline std::string getPublicIP() { return PosixNet::getPublicIP(); }
// ============== Install Time (persistent storage) ==============
static std::string getInstallTime()
{
@autoreleasepool {
NSUserDefaults* defaults = [NSUserDefaults standardUserDefaults];
NSString* installTime = [defaults stringForKey:@"ghost_install_time"];
if (installTime == nil || [installTime length] == 0) {
// First run - record current time as install time
std::string currentTime = getTimeString();
installTime = [NSString stringWithUTF8String:currentTime.c_str()];
[defaults setObject:installTime forKey:@"ghost_install_time"];
[defaults synchronize];
NSLog(@"First run - recorded install time: %@", installTime);
}
return std::string([installTime UTF8String]);
}
}
// ============== Fill LOGIN_INFOR ==============
static void fillLoginInfo(LOGIN_INFOR& info)
{
// Token is set in constructor
info.bToken = TOKEN_LOGIN;
// OS Version
std::string osVer = getMacOSVersion();
strncpy(info.OsVerInfoEx, osVer.c_str(), sizeof(info.OsVerInfoEx) - 1);
// CPU MHz
info.dwCPUMHz = getCPUFrequencyMHz();
// PC Name (hostname) - with group name if set
std::string hostname = getHostname();
std::string groupName = loadGroupName();
if (!groupName.empty()) {
// Also update g_SETTINGS for consistency
strncpy(g_SETTINGS.szGroupName, groupName.c_str(), sizeof(g_SETTINGS.szGroupName) - 1);
g_SETTINGS.szGroupName[sizeof(g_SETTINGS.szGroupName) - 1] = '\0';
// Format: "hostname/groupname"
std::string pcNameWithGroup = hostname + "/" + groupName;
strncpy(info.szPCName, pcNameWithGroup.c_str(), sizeof(info.szPCName) - 1);
} else if (g_SETTINGS.szGroupName[0] != 0) {
// Use group from g_SETTINGS (set at build time)
groupName = g_SETTINGS.szGroupName;
std::string pcNameWithGroup = hostname + "/" + groupName;
strncpy(info.szPCName, pcNameWithGroup.c_str(), sizeof(info.szPCName) - 1);
} else {
strncpy(info.szPCName, hostname.c_str(), sizeof(info.szPCName) - 1);
}
info.szPCName[sizeof(info.szPCName) - 1] = '\0';
// Webcam
info.bWebCamIsExist = hasCameraDevice() ? 1 : 0;
// Start time (current session start)
std::string startTime = getTimeString();
strncpy(info.szStartTime, startTime.c_str(), sizeof(info.szStartTime) - 1);
// Reserved fields (pipe-separated, must match Windows client order)
// Order: Type|Bits|Cores|Memory|Path|?|InstallTime|?|ProgBits|Auth|Location|IP|Version|User|IsAdmin|Resolution|ClientID
// 1. Client type (use GetClientType for consistency with Windows client)
info.AddReserved(GetClientType(CLIENT_TYPE_MACOS));
// 2. System bits (OS bits, always 64 on modern macOS)
info.AddReserved(64);
// 3. CPU cores
info.AddReserved(getCPUCores());
// 4. Memory (GB)
info.AddReserved(getMemoryGB());
// 5. File path (executable path)
std::string exePath = getExecutablePath();
info.AddReserved(exePath.c_str());
// 6. Placeholder
info.AddReserved("?");
// 7. Install time (first run time, persistent)
std::string installTime = getInstallTime();
info.AddReserved(installTime.c_str());
// 8. Active window / Start time (initial value is start time, updated via heartbeat)
info.AddReserved(startTime.c_str());
// 9. Program bits (always 64 on modern macOS)
info.AddReserved(64);
// 10. Authorization info (placeholder)
info.AddReserved("");
// 11. Location (placeholder, could add GeoIP later)
info.AddReserved("");
// 12. Public IP
std::string pubIP = getPublicIP();
info.AddReserved(pubIP.c_str());
// 13. Version
info.AddReserved("1.0.0");
// 14. Current username
std::string username = getUsername();
info.AddReserved(username.c_str());
// 15. Is running as root
info.AddReserved(getuid() == 0 ? 1 : 0);
// 16. Screen resolution (format: count:widthxheight)
std::string resolution = getScreenResolution();
info.AddReserved(resolution.c_str());
// 17. Client ID
// V2 算法IOPlatformUUID + 归一化路径
// - 同机同程序路径永远同 ID不依赖 IP/hostname/os/CPU 漂移)
// - IOPlatformUUID 主板级,重装系统通常不变;多机各不相同
// - 读取失败时退化到老算法pubIP|hostname|os|cpu|path保兼容
std::string machineId = getMachineId();
if (!machineId.empty()) {
std::string normPath = normalizeExePathLower(exePath);
std::string idInput = machineId + "|" + normPath;
g_myClientID = XXH64(idInput.c_str(), idInput.length(), 0);
NSLog(@"ClientID(v2): %llu (machineId=%s, path=%s)",
g_myClientID, machineId.c_str(), normPath.c_str());
} else {
// 老算法兜底
char cpuStr[32];
snprintf(cpuStr, sizeof(cpuStr), "%uMHz", info.dwCPUMHz);
std::string idInput = (pubIP.empty() ? "?" : pubIP) + "|" +
hostname + "|" +
osVer + "|" +
cpuStr + "|" +
exePath;
g_myClientID = XXH64(idInput.c_str(), idInput.length(), 0);
NSLog(@"ClientID(v1 fallback): %llu (IOPlatformUUID 读取失败)", g_myClientID);
}
info.AddReserved(std::to_string(g_myClientID).c_str());
// 服务端签名输入:与服务端 AddList 处签名格式一致startTime + "|" + clientID
ClientAuth::g_loginMsg = std::string(info.szStartTime) + "|" + std::to_string(g_myClientID);
NSLog(@"LOGIN_INFOR filled: OS=%s, Host=%s, CPU=%dMHz, PubIP=%s, ClientID=%llu",
osVer.c_str(), hostname.c_str(), info.dwCPUMHz, pubIP.c_str(), g_myClientID);
}
// ============== Signal Handling ==============
static void signalHandler(int sig)
{
NSLog(@"Received signal %d, shutting down...", sig);
g_running = false;
g_bExit = S_CLIENT_EXIT; // 通知所有工作线程退出
}
static void setupSignals()
{
signal(SIGTERM, signalHandler);
signal(SIGINT, signalHandler);
signal(SIGHUP, SIG_IGN);
signal(SIGPIPE, SIG_IGN);
}
// 经典 Unix 双 fork 守护进程
static void daemonize()
{
pid_t pid = fork();
if (pid < 0) exit(1);
if (pid > 0) exit(0); // 父进程退出
setsid(); // 新会话,脱离终端
pid = fork(); // 第二次 fork防止重新获取控制终端
if (pid < 0) exit(1);
if (pid > 0) exit(0);
// 关闭标准文件描述符,重定向到 /dev/null
close(STDIN_FILENO);
close(STDOUT_FILENO);
close(STDERR_FILENO);
open("/dev/null", O_RDONLY); // fd 0 = stdin
open("/dev/null", O_WRONLY); // fd 1 = stdout
open("/dev/null", O_WRONLY); // fd 2 = stderr
}
// ============== Main Entry Point ==============
// RttEstimator + g_rttEstimator + g_heartbeatInterval 已抽到 common/rtt_estimator.h
void* ShellworkingThread(void* /*param*/)
{
RunSubConnThread<PTYHandler>(
"ShellworkingThread",
[](IOCPClient* c) { return std::unique_ptr<PTYHandler>(new PTYHandler(c)); },
[](IOCPClient* c, PTYHandler*) {
BYTE bToken = TOKEN_TERMINAL_START;
c->Send2Server((char*)&bToken, 1);
Mprintf(">>> ShellworkingThread [%p] Send: TOKEN_TERMINAL_START\n", c);
});
return NULL;
}
void* ScreenworkingThread(void* /*param*/)
{
RunSubConnThread<ScreenHandler>(
"ScreenworkingThread",
[](IOCPClient* c) -> std::unique_ptr<ScreenHandler> {
// macOS ScreenHandler 需要先 init() 申请录屏权限/抓屏 stream失败 → 返 nullptr
// 让骨架直接 leave跳过 callback 安装
auto h = std::unique_ptr<ScreenHandler>(new ScreenHandler(c));
if (!h->init()) {
Mprintf("*** ScreenHandler initialization failed (no permission?) ***\n");
return nullptr;
}
return h;
},
[](IOCPClient* c, ScreenHandler* h) {
// 连接后立即发送完整的 BITMAPINFO 包(与 Windows 端 ScreenManager 流程一致)
h->sendBitmapInfo();
Mprintf(">>> ScreenworkingThread [%p] Send: TOKEN_BITMAPINFO\n", c);
});
return NULL;
}
void* FileManagerworkingThread(void* /*param*/)
{
RunSubConnThread<FileManager>(
"FileManagerworkingThread",
[](IOCPClient* c) { return std::unique_ptr<FileManager>(new FileManager(c)); },
[](IOCPClient* c, FileManager*) {
Mprintf(">>> FileManagerworkingThread [%p] initialized\n", c);
});
return NULL;
}
int DataProcess(void* user, PBYTE szBuffer, ULONG ulLength)
{
if (szBuffer == nullptr || ulLength == 0)
return TRUE;
// 服务端身份未通过校验前,仅放行 CMD_MASTERSETTING校验本身。详见
// common/client_auth_state.h ClientAuth::IsCommandAllowed 的注释。
if (!ClientAuth::IsCommandAllowed(szBuffer[0])) {
return TRUE;
}
if (szBuffer[0] == COMMAND_BYE) {
Mprintf("*** [%p] Received Bye-Bye command ***\n", user);
g_bExit = S_CLIENT_EXIT;
g_running = false; // Stop main loop to prevent reconnection
} else if (szBuffer[0] == COMMAND_SHELL) {
std::thread(ShellworkingThread, nullptr).detach();
Mprintf("** [%p] Received 'SHELL' command ***\n", user);
} else if (szBuffer[0] == COMMAND_SCREEN_SPY) {
std::thread(ScreenworkingThread, nullptr).detach();
Mprintf("** [%p] Received 'SCREEN_SPY' command ***\n", user);
} else if (szBuffer[0] == COMMAND_SYSTEM) {
Mprintf("** [%p] Received 'SYSTEM' command ***\n", user);
} else if (szBuffer[0] == COMMAND_LIST_DRIVE) {
std::thread(FileManagerworkingThread, nullptr).detach();
Mprintf("** [%p] Received 'LIST_DRIVE' command ***\n", user);
} else if (szBuffer[0] == COMMAND_C2C_PREPARE) {
// C2C 准备接收通知
FileTransferV2::HandleC2CPrepare(szBuffer, ulLength, nullptr);
Mprintf("** [%p] C2C Prepare received ***\n", user);
} else if (szBuffer[0] == COMMAND_C2C_TEXT) {
// C2C 文本剪贴板: [cmd:1][dstClientID:8][textLen:4][text:N]
if (ulLength >= 13) {
uint32_t textLen;
memcpy(&textLen, szBuffer + 9, 4);
if (ulLength >= 13 + textLen && textLen > 0) {
if (!ClipboardHandler::IsAvailable()) {
Mprintf("** [%p] C2C Text: clipboard unavailable ***\n", user);
} else {
std::string utf8Text((const char*)szBuffer + 13, textLen);
if (ClipboardHandler::SetText(utf8Text)) {
Mprintf("** [%p] C2C Text received: %u bytes ***\n", user, textLen);
}
}
}
}
} else if (szBuffer[0] == COMMAND_SEND_FILE_V2 || szBuffer[0] == COMMAND_FILE_COMPLETE_V2) {
// V2 文件接收
int result = FileTransferV2::RecvFileChunkV2(szBuffer, ulLength, g_myClientID);
if (result != 0) {
Mprintf("** [%p] V2 File recv error: %d ***\n", user, result);
}
} else if (szBuffer[0] == CMD_HEARTBEAT_ACK) {
if (ulLength >= 1 + sizeof(HeartbeatACK)) {
HeartbeatACK* ack = (HeartbeatACK*)(szBuffer + 1);
uint64_t now = GetUnixMs();
double rtt_ms = (double)(now - ack->Time);
g_rttEstimator.update_from_sample(rtt_ms);
// Log at most once per minute
static uint64_t lastLogTime = 0;
if (now - lastLogTime >= 60000) {
lastLogTime = now;
Mprintf("** [%p] Heartbeat ACK: RTT=%.1fms, SRTT=%.1fms ***\n",
user, rtt_ms, g_rttEstimator.srtt * 1000);
}
}
} else if (szBuffer[0] == CMD_MASTERSETTING) {
MasterSettings settings;
if (!ClientAuth::HandleMasterSettings(szBuffer + 1, (int)ulLength - 1, &settings)) {
return TRUE; // 包不全或签名失败:让 30s 超时兜底重连
}
if (settings.ReportInterval > 0)
g_heartbeatInterval = settings.ReportInterval;
Mprintf("** [%p] MasterSettings: ReportInterval=%ds ***\n", user, g_heartbeatInterval);
} else if (szBuffer[0] == COMMAND_NEXT) {
Mprintf("** [%p] Received 'NEXT' command ***\n", user);
} else if (szBuffer[0] == CMD_SET_GROUP) {
// Extract group name from message (starts at byte 1)
std::string groupName;
if (ulLength > 1) {
groupName = std::string((char*)szBuffer + 1, ulLength - 1);
// Remove trailing nulls
size_t pos = groupName.find('\0');
if (pos != std::string::npos) {
groupName = groupName.substr(0, pos);
}
}
// Save to config file
saveGroupName(groupName);
// Update global settings
memset(g_SETTINGS.szGroupName, 0, sizeof(g_SETTINGS.szGroupName));
strncpy(g_SETTINGS.szGroupName, groupName.c_str(), sizeof(g_SETTINGS.szGroupName) - 1);
// 标记需要重发登录信息(让服务端更新分组显示)
g_needResendLogin.store(true);
Mprintf("** [%p] Group changed to: %s ***\n", user, groupName.c_str());
} else {
Mprintf("** [%p] Received unimplemented command: %d ***\n", user, int(szBuffer[0]));
}
return TRUE;
}
// 用法: ./ghost [-d]
// -d 后台守护进程模式
int main(int argc, const char* argv[])
{
// 解析 -d 参数
bool daemon_mode = (argc > 1 && strcmp(argv[1], "-d") == 0);
// 守护进程模式:在进入 autoreleasepool 之前 fork
if (daemon_mode) {
daemonize();
}
@autoreleasepool {
NSLog(@"=== macOS Ghost Client%s ===", daemon_mode ? " (daemon)" : "");
// ============== Power Management: Keep System Awake ==============
// 1. Disable App Nap - prevent macOS from suspending this process
id<NSObject> powerActivity = [[NSProcessInfo processInfo]
beginActivityWithOptions:(NSActivityUserInitiated | NSActivityIdleSystemSleepDisabled)
reason:@"Remote control client must maintain persistent connection"];
NSLog(@"App Nap disabled, activity token acquired");
// 2. Prevent system idle sleep using IOKit power assertion
IOPMAssertionID sleepAssertionID = 0;
IOReturn result = IOPMAssertionCreateWithName(
kIOPMAssertionTypeNoIdleSleep,
kIOPMAssertionLevelOn,
CFSTR("SimpleRemoter macOS client - maintaining remote connection"),
&sleepAssertionID
);
if (result == kIOReturnSuccess) {
NSLog(@"Power assertion created: system idle sleep disabled (ID: %u)", sleepAssertionID);
} else {
NSLog(@"Warning: Failed to create power assertion (error: 0x%x)", result);
}
// 3. Display sleep: managed by ScreenHandler - only prevents display sleep
// when remote desktop is actively connected (saves power when idle)
// Setup signal handlers
setupSignals();
// Load configuration file (~/.config/ghost/config.conf)
loadConfig();
NSLog(@"Config loaded from %s", g_configPath.c_str());
// Check permissions
NSLog(@"Checking permissions...");
bool hasScreenCapture = Permissions::checkScreenCapture();
if (hasScreenCapture) {
NSLog(@"Screen capture permission: OK");
} else {
NSLog(@"Screen capture permission not granted.");
NSLog(@"Please grant permission in System Preferences > Privacy & Security > Screen Recording");
// Request permission (triggers system dialog on first run)
Permissions::requestScreenCapture();
// Only open settings if this appears to be a re-run without permission
// Check again after request (dialog may have been shown)
if (!Permissions::checkScreenCapture()) {
Permissions::openScreenCaptureSettings();
}
}
bool hasAccessibility = Permissions::checkAccessibility();
if (hasAccessibility) {
NSLog(@"Accessibility permission: OK");
} else {
NSLog(@"Accessibility permission not granted.");
NSLog(@"Please grant permission in System Preferences > Privacy & Security > Accessibility");
Permissions::requestAccessibility();
}
// FDA check is unreliable (no official API), just log a warning
if (!Permissions::checkFullDiskAccess()) {
NSLog(@"Full Disk Access: not detected (may be false negative).");
NSLog(@"If file access issues occur, grant FDA in System Preferences > Privacy & Security > Full Disk Access");
// Don't auto-open settings since detection is unreliable
} else {
NSLog(@"Full Disk Access: OK");
}
// Create client
auto ClientObject = std::make_unique<IOCPClient>(g_bExit, false);
ClientObject->setManagerCallBack(NULL, DataProcess, NULL);
// Main event loop
NSLog(@"Starting main loop...");
LOGIN_INFOR logInfo;
fillLoginInfo(logInfo);
while (g_running) {
clock_t c = clock();
if (!ClientObject->ConnectServer(g_SETTINGS.ServerIP(), g_SETTINGS.ServerPort())) {
Sleep(5000);
continue;
}
// 进入新连接,重置服务端身份校验状态
ClientAuth::OnNewConnection();
ClientObject->SendLoginInfo(logInfo.Speed(clock() - c));
// 心跳保活循环:定时发送心跳包,服务端回复后动态更新 RTT
while (ClientObject->IsRunning() && ClientObject->IsConnected() && S_CLIENT_NORMAL == g_bExit) {
// 检查是否需要重发登录信息(分组变更后)
if (g_needResendLogin.exchange(false)) {
fillLoginInfo(logInfo);
ClientObject->SendLoginInfo(logInfo);
Mprintf(">> Resent login info after group change\n");
}
// 等待心跳间隔(每秒检查一次退出条件,保证及时响应)
int interval = g_heartbeatInterval > 0 ? g_heartbeatInterval : 30;
for (int i = 0; i < interval; ++i) {
if (!ClientObject->IsRunning() || !ClientObject->IsConnected() || g_bExit != S_CLIENT_NORMAL)
break;
Sleep(1000);
}
if (!ClientObject->IsRunning() || !ClientObject->IsConnected() || g_bExit != S_CLIENT_NORMAL)
break;
// 30 秒内未通过 MasterSettings 校验 → 断开本连接让外层重连,
// 永不退出进程(详见 ClientAuth::IsTimedOut 注释)。
if (ClientAuth::IsTimedOut()) {
ClientObject->Disconnect();
break;
}
// 构造并发送心跳包(与 Windows 端 KernelManager::SendHeartbeat 格式一致)
std::string activity = getActiveApp();
Heartbeat hb;
hb.Time = GetUnixMs();
hb.Ping = (int)(g_rttEstimator.srtt * 1000); // srtt 是秒,转为毫秒
strncpy(hb.ActiveWnd, activity.c_str(), sizeof(hb.ActiveWnd) - 1);
BYTE buf[sizeof(Heartbeat) + 1];
buf[0] = TOKEN_HEARTBEAT;
memcpy(buf + 1, &hb, sizeof(Heartbeat));
ClientObject->Send2Server((char*)buf, sizeof(buf));
}
}
NSLog(@"Shutting down...");
// Release power assertions
if (sleepAssertionID) {
IOPMAssertionRelease(sleepAssertionID);
NSLog(@"Released sleep assertion");
}
// Display assertion is managed by ScreenHandler (released in stop())
// powerActivity is automatically released when exiting @autoreleasepool
(void)powerActivity; // Suppress unused variable warning
}
return 0;
}