Client first packet on every sub-connection signs (clientID || timestamp || nonce) and waits for server ack. Server verifies signature and pins clientID on the sub-connection ctx, eliminating IP-reverse-lookup unreliability for NAT/localhost scenarios. Sub-conn coverage: Win 12 sites, Linux/macOS 3-4 each. Main connection keeps existing TOKEN_LOGIN flow unchanged. Includes: - Protocol structs sized to 512/256 bytes with reserved space for future extensions (locale, OS info, session token, etc.) - 5-min timestamp tolerance (Kerberos-grade replay window) - 10-sec client wait for cross-pacific / weak-network tolerance - Fix RemoveFromHostList side-effect ordering: MarkDeviceOffline and m_ActiveWndW.erase now only fire when ctx is actually removed from m_HostList, preventing sub-conn disconnects from misreporting main as offline (regression introduced by auth-set clientID on sub ctx) - Fix latent bug: IOCPClient::m_conn was never assigned in ctor, leaving GetConnectionAddress() always NULL and FileManager V2 transfer's srcClientID always 0 Breaking change: new client cannot use sub-features against old server. New server tolerates legacy clients (no auth). Future tightening can reject unauthenticated sub-connections via IsAuthenticated() flag. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1034 lines
37 KiB
Plaintext
1034 lines
37 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"
|
||
#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;
|
||
|
||
// 远程地址:当前为写死状态,如需调试,请按实际情况修改
|
||
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
|
||
static 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;
|
||
}
|
||
// Trim trailing whitespace
|
||
while (!result.empty() && (result.back() == '\n' || result.back() == '\r' || result.back() == ' '))
|
||
result.pop_back();
|
||
return result;
|
||
}
|
||
|
||
// HTTP GET using curl (macOS has curl built-in)
|
||
static std::string httpGet(const std::string& url, int timeoutSec = 5)
|
||
{
|
||
std::string t = std::to_string(timeoutSec);
|
||
return execCmd("curl -s --max-time " + t + " \"" + url + "\" 2>/dev/null");
|
||
}
|
||
|
||
// Get public IP (try multiple sources)
|
||
static std::string getPublicIP()
|
||
{
|
||
static const char* urls[] = {
|
||
"https://checkip.amazonaws.com",
|
||
"https://api.ipify.org",
|
||
"https://ipinfo.io/ip",
|
||
"https://icanhazip.com",
|
||
"https://ifconfig.me/ip",
|
||
};
|
||
for (auto& url : urls) {
|
||
std::string ip = httpGet(url, 3);
|
||
// Validate: non-empty, contains dot, reasonable length
|
||
if (!ip.empty() && ip.find('.') != std::string::npos && ip.size() <= 45) {
|
||
NSLog(@"getPublicIP: %s (from %s)", ip.c_str(), url);
|
||
return ip;
|
||
}
|
||
}
|
||
NSLog(@"getPublicIP: all sources failed");
|
||
return "";
|
||
}
|
||
|
||
// ============== Install Time (persistent storage) ==============
|
||
|
||
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());
|
||
|
||
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 ==============
|
||
|
||
// 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)
|
||
{
|
||
try {
|
||
std::unique_ptr<IOCPClient> 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<PTYHandler> handler(new PTYHandler(ClientObject.get()));
|
||
ClientObject->setManagerCallBack(handler.get(), IOCPManager::DataProcess, IOCPManager::ReconnectProcess);
|
||
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());
|
||
}
|
||
return NULL;
|
||
}
|
||
|
||
void* ScreenworkingThread(void* param)
|
||
{
|
||
try {
|
||
std::unique_ptr<IOCPClient> 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<ScreenHandler> handler(new ScreenHandler(ClientObject.get()));
|
||
if (!handler->init()) {
|
||
Mprintf("*** ScreenHandler initialization failed (no permission?) ***\n");
|
||
return NULL;
|
||
}
|
||
ClientObject->setManagerCallBack(handler.get(), IOCPManager::DataProcess, IOCPManager::ReconnectProcess);
|
||
// 连接后立即发送完整的 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());
|
||
}
|
||
return NULL;
|
||
}
|
||
|
||
void* FileManagerworkingThread(void* param)
|
||
{
|
||
try {
|
||
std::unique_ptr<IOCPClient> 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<FileManager> handler(new FileManager(ClientObject.get()));
|
||
ClientObject->setManagerCallBack(handler.get(), IOCPManager::DataProcess, IOCPManager::ReconnectProcess);
|
||
Mprintf(">>> FileManagerworkingThread [%p] initialized\n", clientAddr);
|
||
while (ClientObject->IsRunning() && ClientObject->IsConnected() && S_CLIENT_NORMAL == g_bExit)
|
||
Sleep(1000);
|
||
// 清除回调,防止重连线程访问已销毁的 handler
|
||
ClientObject->setManagerCallBack(nullptr, nullptr, nullptr);
|
||
}
|
||
Mprintf(">>> Leave FileManagerworkingThread [%p]\n", clientAddr);
|
||
} catch (const std::exception& e) {
|
||
Mprintf("*** FileManagerworkingThread exception: %s ***\n", e.what());
|
||
}
|
||
return NULL;
|
||
}
|
||
|
||
int DataProcess(void* user, PBYTE szBuffer, ULONG ulLength)
|
||
{
|
||
if (szBuffer == nullptr || ulLength == 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) {
|
||
int settingSize = ulLength - 1;
|
||
if (settingSize >= (int)sizeof(int)) { // 至少包含 ReportInterval
|
||
MasterSettings settings = {};
|
||
memcpy(&settings, szBuffer + 1, settingSize < (int)sizeof(MasterSettings) ? settingSize : sizeof(MasterSettings));
|
||
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;
|
||
}
|
||
|
||
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;
|
||
|
||
// 构造并发送心跳包(与 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;
|
||
}
|