967 lines
35 KiB
Plaintext
967 lines
35 KiB
Plaintext
#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.h(namespace 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.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) ==============
|
||
|
||
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 ==============
|
||
|
||
// 注意:signal handler 内不能调 NSLog/Mprintf(NSLog 走 Foundation 锁,
|
||
// Mprintf 走 Logger mutex/condvar),都不是 async-signal-safe。只在这里
|
||
// 记 sig_atomic_t 标志位,main 退出循环后再补一行日志。
|
||
static volatile sig_atomic_t g_lastSignal = 0;
|
||
static void signalHandler(int sig)
|
||
{
|
||
g_lastSignal = 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));
|
||
}
|
||
}
|
||
|
||
// 退出原因留痕:signal handler 不能直接打日志,在这里补一行。
|
||
if (g_lastSignal != 0) {
|
||
Mprintf(">>> Exit by signal %d (g_bExit=%d)\n",
|
||
(int)g_lastSignal, (int)g_bExit);
|
||
} else {
|
||
Mprintf(">>> Exit normally (g_bExit=%d)\n", (int)g_bExit);
|
||
}
|
||
|
||
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
|
||
|
||
// 显式停止日志,确保上面 Mprintf 的退出原因落盘。
|
||
// 不依赖 ~Logger() 的静态析构次序,避免后续新增日志相关静态对象时踩坑。
|
||
Logger::getInstance().stop();
|
||
}
|
||
|
||
return 0;
|
||
}
|