Compare commits
9 Commits
v1.3.2
...
36423b1c7c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
36423b1c7c | ||
|
|
a3611d9fc1 | ||
|
|
9ae5529458 | ||
|
|
171fa750e5 | ||
|
|
8ed9ba8426 | ||
|
|
fd3838a151 | ||
|
|
56419f8ecb | ||
|
|
bb6fd7b1b9 | ||
|
|
3607f1d768 |
16
ReadMe.md
16
ReadMe.md
@@ -494,27 +494,31 @@ make
|
||||
|
||||
**系统要求**:
|
||||
- macOS 10.15 (Catalina) 及以上
|
||||
- 架构支持:Intel (x64) 和 Apple Silicon (arm64) 通用二进制
|
||||
- 需要授予系统权限:屏幕录制、辅助功能、完全磁盘访问
|
||||
|
||||
**功能支持**:
|
||||
|
||||
| 功能 | 状态 | 实现 |
|
||||
|------|------|------|
|
||||
| 远程桌面 | ✅ | CoreGraphics 屏幕捕获,H.264 硬件编码 |
|
||||
| 远程桌面 | ✅ | CoreGraphics 屏幕捕获,VideoToolbox H.264 硬件编码 |
|
||||
| 鼠标控制 | ✅ | CGEvent 模拟,支持双击、拖拽 |
|
||||
| 键盘控制 | ✅ | CGEvent 模拟,完整键码映射 |
|
||||
| 光标同步 | ✅ | 实时同步远程光标样式 |
|
||||
| 远程终端 | ✅ | PTY 交互式 Shell(zsh/bash) |
|
||||
| 文件管理 | ✅ | 双向传输、V2 协议、大文件支持 |
|
||||
| 心跳/RTT | ✅ | RFC 6298 RTT 估算 |
|
||||
| 文件管理 | ⏳ | 开发中 |
|
||||
| 远程终端 | ⏳ | 开发中 |
|
||||
| 分组管理 | ✅ | 持久化配置文件 |
|
||||
| 进程管理 | ⏳ | 开发中 |
|
||||
| 剪贴板 | ⏳ | 开发中 |
|
||||
|
||||
**编译方式**:
|
||||
|
||||
```bash
|
||||
cd macos
|
||||
mkdir build && cd build
|
||||
cmake ..
|
||||
make
|
||||
./build.sh
|
||||
# 或手动编译:
|
||||
# mkdir build && cd build && cmake .. && make
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
16
ReadMe_EN.md
16
ReadMe_EN.md
@@ -479,27 +479,31 @@ make
|
||||
|
||||
**System Requirements**:
|
||||
- macOS 10.15 (Catalina) or later
|
||||
- Architecture: Universal Binary (Intel x64 + Apple Silicon arm64)
|
||||
- Required permissions: Screen Recording, Accessibility, Full Disk Access
|
||||
|
||||
**Feature Support**:
|
||||
|
||||
| Feature | Status | Implementation |
|
||||
|---------|--------|----------------|
|
||||
| Remote Desktop | ✅ | CoreGraphics screen capture, H.264 hardware encoding |
|
||||
| Remote Desktop | ✅ | CoreGraphics screen capture, VideoToolbox 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 |
|
||||
| Remote Terminal | ✅ | PTY interactive shell (zsh/bash) |
|
||||
| File Management | ✅ | Bidirectional transfer, V2 protocol, large file support |
|
||||
| Heartbeat/RTT | ✅ | RFC 6298 RTT estimation |
|
||||
| File Management | ⏳ | In development |
|
||||
| Remote Terminal | ⏳ | In development |
|
||||
| Group Management | ✅ | Persistent configuration file |
|
||||
| Process Management | ⏳ | In development |
|
||||
| Clipboard | ⏳ | In development |
|
||||
|
||||
**Build Instructions**:
|
||||
|
||||
```bash
|
||||
cd macos
|
||||
mkdir build && cd build
|
||||
cmake ..
|
||||
make
|
||||
./build.sh
|
||||
# Or manually:
|
||||
# mkdir build && cd build && cmake .. && make
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
16
ReadMe_TW.md
16
ReadMe_TW.md
@@ -478,27 +478,31 @@ make
|
||||
|
||||
**系統要求**:
|
||||
- macOS 10.15 (Catalina) 及以上
|
||||
- 架構支援:Intel (x64) 和 Apple Silicon (arm64) 通用二進位
|
||||
- 需要授予系統權限:螢幕錄製、輔助使用、完全磁碟存取
|
||||
|
||||
**功能支援**:
|
||||
|
||||
| 功能 | 狀態 | 實作 |
|
||||
|------|------|------|
|
||||
| 遠端桌面 | ✅ | CoreGraphics 螢幕擷取,H.264 硬體編碼 |
|
||||
| 遠端桌面 | ✅ | CoreGraphics 螢幕擷取,VideoToolbox H.264 硬體編碼 |
|
||||
| 滑鼠控制 | ✅ | CGEvent 模擬,支援雙擊、拖曳 |
|
||||
| 鍵盤控制 | ✅ | CGEvent 模擬,完整鍵碼對應 |
|
||||
| 游標同步 | ✅ | 即時同步遠端游標樣式 |
|
||||
| 遠端終端 | ✅ | PTY 互動式 Shell(zsh/bash) |
|
||||
| 檔案管理 | ✅ | 雙向傳輸、V2 協定、大檔案支援 |
|
||||
| 心跳/RTT | ✅ | RFC 6298 RTT 估算 |
|
||||
| 檔案管理 | ⏳ | 開發中 |
|
||||
| 遠端終端 | ⏳ | 開發中 |
|
||||
| 分組管理 | ✅ | 持久化設定檔 |
|
||||
| 程序管理 | ⏳ | 開發中 |
|
||||
| 剪貼簿 | ⏳ | 開發中 |
|
||||
|
||||
**編譯方式**:
|
||||
|
||||
```bash
|
||||
cd macos
|
||||
mkdir build && cd build
|
||||
cmake ..
|
||||
make
|
||||
./build.sh
|
||||
# 或手動編譯:
|
||||
# mkdir build && cd build && cmake .. && make
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
@@ -1,8 +1,24 @@
|
||||
/**
|
||||
* FileManager.h - Unix File Manager
|
||||
*
|
||||
* Implements file transfer between Windows server and Unix client.
|
||||
* Supports: browse, upload, download, delete, rename, create folder
|
||||
*
|
||||
* PLATFORM SUPPORT:
|
||||
* - Linux: Supported
|
||||
* - macOS: Supported
|
||||
* - Windows: NOT SUPPORTED (Windows uses different file APIs)
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#if defined(_WIN32) || defined(_WIN64)
|
||||
#error "FileManager.h is not supported on Windows."
|
||||
#endif
|
||||
|
||||
#include <dirent.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/statvfs.h>
|
||||
#include <iconv.h>
|
||||
#include <unistd.h>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
@@ -11,15 +27,19 @@
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
#include <cerrno>
|
||||
|
||||
#ifdef __APPLE__
|
||||
#include <sys/mount.h> // macOS: statfs for filesystem type
|
||||
#endif
|
||||
|
||||
#include <iconv.h> // Character encoding conversion (GBK <-> UTF-8)
|
||||
|
||||
// FileTransferV2 is in the same directory (common/)
|
||||
#include "FileTransferV2.h"
|
||||
|
||||
// 外部声明 clientID(在 main.cpp 中定义)
|
||||
// External declaration of clientID (defined in main.cpp/main.mm)
|
||||
extern uint64_t g_myClientID;
|
||||
|
||||
// ============== Linux File Manager ==============
|
||||
// Implements file transfer between Windows server and Linux client
|
||||
// Supports: browse, upload, download, delete, rename, create folder
|
||||
|
||||
#define MAX_SEND_BUFFER 65535
|
||||
|
||||
class FileManager : public IOCPManager
|
||||
@@ -222,6 +242,13 @@ private:
|
||||
// ---- Get root filesystem type ----
|
||||
static std::string getRootFsType()
|
||||
{
|
||||
#ifdef __APPLE__
|
||||
struct statfs sf;
|
||||
if (statfs("/", &sf) == 0) {
|
||||
return std::string(sf.f_fstypename); // "apfs", "hfs", etc.
|
||||
}
|
||||
return "apfs";
|
||||
#else
|
||||
std::ifstream f("/proc/mounts");
|
||||
std::string line;
|
||||
while (std::getline(f, line)) {
|
||||
@@ -232,6 +259,7 @@ private:
|
||||
}
|
||||
}
|
||||
return "ext4";
|
||||
#endif
|
||||
}
|
||||
|
||||
// ---- Ensure parent directory exists (mkdir -p for parent of file path) ----
|
||||
@@ -307,7 +335,11 @@ private:
|
||||
memcpy(buf + offset + 2, &totalMB, sizeof(unsigned long));
|
||||
memcpy(buf + offset + 6, &freeMB, sizeof(unsigned long));
|
||||
|
||||
#ifdef __APPLE__
|
||||
const char* typeName = "macOS";
|
||||
#else
|
||||
const char* typeName = "Linux";
|
||||
#endif
|
||||
int typeNameLen = strlen(typeName) + 1;
|
||||
memcpy(buf + offset + 10, typeName, typeNameLen);
|
||||
|
||||
@@ -1,7 +1,24 @@
|
||||
/**
|
||||
* FileTransferV2.h - Unix V2 File Transfer Protocol
|
||||
*
|
||||
* Implements V2 file transfer protocol for Unix clients.
|
||||
* Supports: receive files from server/C2C, send files to server/C2C
|
||||
*
|
||||
* PLATFORM SUPPORT:
|
||||
* - Linux: Supported
|
||||
* - macOS: Supported
|
||||
* - Windows: NOT SUPPORTED (Windows uses different file APIs)
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "common/commands.h"
|
||||
#include "common/file_upload.h"
|
||||
#include "client/IOCPClient.h"
|
||||
|
||||
#if defined(_WIN32) || defined(_WIN64)
|
||||
#error "FileTransferV2.h is not supported on Windows."
|
||||
#endif
|
||||
|
||||
#include "commands.h"
|
||||
#include "file_upload.h"
|
||||
#include "../client/IOCPClient.h"
|
||||
#include <unistd.h>
|
||||
#include <sys/stat.h>
|
||||
#include <fcntl.h>
|
||||
@@ -15,10 +32,6 @@
|
||||
#include <fstream>
|
||||
#include <thread>
|
||||
|
||||
// ============== Linux V2 File Transfer ==============
|
||||
// Implements V2 file transfer protocol for Linux client
|
||||
// Supports: receive files from server/C2C, send files to server/C2C
|
||||
|
||||
class FileTransferV2
|
||||
{
|
||||
public:
|
||||
270
common/PTYHandler.h
Normal file
270
common/PTYHandler.h
Normal file
@@ -0,0 +1,270 @@
|
||||
/**
|
||||
* PTYHandler.h - Unix Pseudo Terminal Handler
|
||||
*
|
||||
* This file provides pseudo terminal (PTY) functionality for remote shell access.
|
||||
*
|
||||
* PLATFORM SUPPORT:
|
||||
* - Linux: Supported
|
||||
* - macOS: Supported
|
||||
* - Windows: NOT SUPPORTED (Windows uses different terminal APIs)
|
||||
*
|
||||
* USAGE:
|
||||
* #include "common/PTYHandler.h"
|
||||
*
|
||||
* PTYHandler* handler = new PTYHandler(clientObject);
|
||||
* clientObject->setManagerCallBack(handler, ...);
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#if defined(_WIN32) || defined(_WIN64)
|
||||
#error "PTYHandler.h is not supported on Windows. Use Windows ConPTY or other APIs instead."
|
||||
#endif
|
||||
|
||||
// Platform-specific includes
|
||||
#ifdef __APPLE__
|
||||
#include <util.h> // macOS: openpty()
|
||||
#else
|
||||
#include <pty.h> // Linux: openpty()
|
||||
#endif
|
||||
|
||||
// Common Unix includes
|
||||
#include <unistd.h>
|
||||
#include <fcntl.h>
|
||||
#include <termios.h>
|
||||
#include <sys/ioctl.h>
|
||||
#include <sys/wait.h>
|
||||
#include <signal.h>
|
||||
#include <errno.h>
|
||||
#include <string.h>
|
||||
#include <thread>
|
||||
#include <atomic>
|
||||
#include <stdexcept>
|
||||
|
||||
#include "commands.h"
|
||||
#include "../client/IOCPClient.h"
|
||||
|
||||
/**
|
||||
* PTYHandler - Pseudo Terminal Handler
|
||||
*
|
||||
* Manages a pseudo terminal for remote shell access.
|
||||
* Inherits from IOCPManager to integrate with the IOCP client framework.
|
||||
*/
|
||||
class PTYHandler : public IOCPManager
|
||||
{
|
||||
public:
|
||||
// Non-copyable, non-movable (owns system resources)
|
||||
PTYHandler(const PTYHandler&) = delete;
|
||||
PTYHandler& operator=(const PTYHandler&) = delete;
|
||||
PTYHandler(PTYHandler&&) = delete;
|
||||
PTYHandler& operator=(PTYHandler&&) = delete;
|
||||
|
||||
PTYHandler(IOCPClient* client) : m_client(client), m_running(false), m_master_fd(-1), m_slave_fd(-1), m_child_pid(-1)
|
||||
{
|
||||
if (!client) {
|
||||
throw std::invalid_argument("IOCPClient pointer cannot be null");
|
||||
}
|
||||
|
||||
// Create pseudo terminal pair
|
||||
if (openpty(&m_master_fd, &m_slave_fd, nullptr, nullptr, nullptr) == -1) {
|
||||
throw std::runtime_error("Failed to create pseudo terminal");
|
||||
}
|
||||
|
||||
// Set master fd to non-blocking mode
|
||||
int flags = fcntl(m_master_fd, F_GETFL, 0);
|
||||
fcntl(m_master_fd, F_SETFL, flags | O_NONBLOCK);
|
||||
|
||||
// Start shell process
|
||||
startShell();
|
||||
}
|
||||
|
||||
~PTYHandler()
|
||||
{
|
||||
m_running = false;
|
||||
if (m_readThread.joinable()) {
|
||||
m_readThread.join();
|
||||
}
|
||||
if (m_master_fd >= 0) {
|
||||
close(m_master_fd);
|
||||
}
|
||||
if (m_slave_fd >= 0) {
|
||||
close(m_slave_fd);
|
||||
}
|
||||
if (m_child_pid > 0) {
|
||||
// Check if child is still running before killing
|
||||
int status;
|
||||
pid_t result = waitpid(m_child_pid, &status, WNOHANG);
|
||||
if (result == 0) {
|
||||
// Child still running, terminate it
|
||||
kill(m_child_pid, SIGTERM);
|
||||
waitpid(m_child_pid, nullptr, 0);
|
||||
}
|
||||
// If result == m_child_pid, child already exited and was reaped
|
||||
// If result == -1, child was already reaped elsewhere
|
||||
}
|
||||
}
|
||||
|
||||
// Start the PTY read thread
|
||||
void Start()
|
||||
{
|
||||
bool expected = false;
|
||||
if (!m_running.compare_exchange_strong(expected, true)) return;
|
||||
m_readThread = std::thread(&PTYHandler::readFromPTY, this);
|
||||
}
|
||||
|
||||
// Handle incoming data from server
|
||||
virtual VOID OnReceive(PBYTE data, ULONG size) override
|
||||
{
|
||||
if (size && data[0] == COMMAND_NEXT) {
|
||||
Start();
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle terminal resize command
|
||||
if (size >= 5 && data[0] == CMD_TERMINAL_RESIZE) {
|
||||
short cols, rows;
|
||||
memcpy(&cols, data + 1, sizeof(short));
|
||||
memcpy(&rows, data + 3, sizeof(short));
|
||||
SetWindowSize(cols, rows);
|
||||
return;
|
||||
}
|
||||
|
||||
// Write data to PTY
|
||||
if (size > 0) {
|
||||
ssize_t total = 0;
|
||||
while (total < (ssize_t)size) {
|
||||
ssize_t written = write(m_master_fd, (char*)data + total, size - total);
|
||||
if (written == -1) {
|
||||
if (errno == EAGAIN || errno == EINTR) continue;
|
||||
break;
|
||||
}
|
||||
total += written;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Set terminal window size
|
||||
void SetWindowSize(int cols, int rows)
|
||||
{
|
||||
struct winsize ws;
|
||||
ws.ws_col = cols;
|
||||
ws.ws_row = rows;
|
||||
ws.ws_xpixel = 0;
|
||||
ws.ws_ypixel = 0;
|
||||
|
||||
if (ioctl(m_master_fd, TIOCSWINSZ, &ws) == -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Send SIGWINCH to child process to notify window size change
|
||||
if (m_child_pid > 0) {
|
||||
kill(m_child_pid, SIGWINCH);
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
int m_master_fd;
|
||||
int m_slave_fd;
|
||||
IOCPClient* m_client;
|
||||
std::thread m_readThread;
|
||||
std::atomic<bool> m_running;
|
||||
pid_t m_child_pid;
|
||||
|
||||
void startShell()
|
||||
{
|
||||
m_child_pid = fork();
|
||||
if (m_child_pid == -1) {
|
||||
close(m_master_fd);
|
||||
close(m_slave_fd);
|
||||
throw std::runtime_error("Failed to fork shell process");
|
||||
}
|
||||
|
||||
if (m_child_pid == 0) {
|
||||
// Child process
|
||||
setsid(); // Create new session, become session leader
|
||||
|
||||
// Set slave PTY as controlling terminal (required for Ctrl+C to work)
|
||||
// This must be done after setsid() and before dup2()
|
||||
ioctl(m_slave_fd, TIOCSCTTY, 0);
|
||||
|
||||
// Redirect stdin/stdout/stderr to slave PTY
|
||||
dup2(m_slave_fd, STDIN_FILENO);
|
||||
dup2(m_slave_fd, STDOUT_FILENO);
|
||||
dup2(m_slave_fd, STDERR_FILENO);
|
||||
close(m_master_fd);
|
||||
close(m_slave_fd);
|
||||
|
||||
// Set terminal environment for xterm.js compatibility
|
||||
setenv("TERM", "xterm-256color", 1);
|
||||
setenv("COLORTERM", "truecolor", 1);
|
||||
|
||||
#ifdef __APPLE__
|
||||
// macOS locale settings
|
||||
setenv("LANG", "en_US.UTF-8", 1);
|
||||
setenv("LC_ALL", "en_US.UTF-8", 1);
|
||||
|
||||
// Try zsh first (macOS default), fallback to bash
|
||||
if (access("/bin/zsh", X_OK) == 0) {
|
||||
execl("/bin/zsh", "zsh", "-i", nullptr);
|
||||
}
|
||||
execl("/bin/bash", "bash", "-i", nullptr);
|
||||
#else
|
||||
// Linux locale settings (C.UTF-8 is most portable)
|
||||
setenv("LANG", "C.UTF-8", 1);
|
||||
setenv("LC_ALL", "C.UTF-8", 1);
|
||||
|
||||
// Start interactive bash
|
||||
execl("/bin/bash", "bash", "-i", nullptr);
|
||||
#endif
|
||||
_exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
void readFromPTY()
|
||||
{
|
||||
char buffer[4096];
|
||||
while (m_running) {
|
||||
// Check if child process has exited
|
||||
int status;
|
||||
pid_t result = waitpid(m_child_pid, &status, WNOHANG);
|
||||
if (result == m_child_pid) {
|
||||
// Shell exited, send close notification
|
||||
if (m_client) {
|
||||
BYTE closeToken = TOKEN_TERMINAL_CLOSE;
|
||||
m_client->Send2Server((char*)&closeToken, 1);
|
||||
}
|
||||
m_running = false;
|
||||
break;
|
||||
}
|
||||
|
||||
ssize_t bytes_read = read(m_master_fd, buffer, sizeof(buffer) - 1);
|
||||
if (bytes_read > 0) {
|
||||
if (m_client) {
|
||||
m_client->Send2Server(buffer, bytes_read);
|
||||
}
|
||||
} else if (bytes_read == 0) {
|
||||
// EOF - PTY closed
|
||||
if (m_client) {
|
||||
BYTE closeToken = TOKEN_TERMINAL_CLOSE;
|
||||
m_client->Send2Server((char*)&closeToken, 1);
|
||||
}
|
||||
m_running = false;
|
||||
break;
|
||||
} else if (bytes_read == -1) {
|
||||
if (errno == EAGAIN || errno == EWOULDBLOCK) {
|
||||
usleep(10000); // 10ms
|
||||
} else if (errno == EIO) {
|
||||
// EIO typically means PTY slave closed (shell exited)
|
||||
if (m_client) {
|
||||
BYTE closeToken = TOKEN_TERMINAL_CLOSE;
|
||||
m_client->Send2Server((char*)&closeToken, 1);
|
||||
}
|
||||
m_running = false;
|
||||
break;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -321,6 +321,16 @@ inline const char* getFileName(const char* path)
|
||||
#endif
|
||||
#elif defined(_WIN32)
|
||||
#define Mprintf(format, ...) Logger::getInstance().log(getFileName((__FILE__)), __LINE__, (format), __VA_ARGS__)
|
||||
#elif defined(__APPLE__)
|
||||
// macOS: 使用 NSLog 输出到系统日志(可通过 Console.app 查看)
|
||||
#ifdef Mprintf
|
||||
#undef Mprintf
|
||||
#endif
|
||||
#ifdef __OBJC__
|
||||
#define Mprintf(format, ...) NSLog(@"%@", [NSString stringWithFormat:@(format), ##__VA_ARGS__])
|
||||
#else
|
||||
#define Mprintf(format, ...) printf(format, ##__VA_ARGS__)
|
||||
#endif
|
||||
#else
|
||||
// Linux: 覆盖 commands.h 中的 printf 回退定义,改用 Logger 写文件
|
||||
#ifdef Mprintf
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
#include "client/IOCPClient.h"
|
||||
#include "LinuxConfig.h"
|
||||
#include "ClipboardHandler.h"
|
||||
#include "FileTransferV2.h"
|
||||
#include "common/FileTransferV2.h"
|
||||
#include <dlfcn.h>
|
||||
#include <sys/stat.h>
|
||||
#include <thread>
|
||||
@@ -651,11 +651,21 @@ public:
|
||||
// Double-check after acquiring lock
|
||||
if (m_destroyed) return;
|
||||
|
||||
// Prevent starting if thread is already running or joinable
|
||||
if (m_captureThread.joinable()) return;
|
||||
// If already running, just send TOKEN_BITMAPINFO again
|
||||
// This allows server to create additional dialogs (MFC can open while Web is active)
|
||||
if (m_captureThread.joinable() || m_running.load()) {
|
||||
Mprintf(">>> ScreenHandler already running, sending TOKEN_BITMAPINFO for new dialog\n");
|
||||
SendBitmapInfo();
|
||||
return;
|
||||
}
|
||||
|
||||
bool expected = false;
|
||||
if (!m_running.compare_exchange_strong(expected, true)) return;
|
||||
if (!m_running.compare_exchange_strong(expected, true)) {
|
||||
// Race condition: another thread started first, send bitmap info
|
||||
Mprintf(">>> ScreenHandler race, sending TOKEN_BITMAPINFO for new dialog\n");
|
||||
SendBitmapInfo();
|
||||
return;
|
||||
}
|
||||
|
||||
m_captureThread = std::thread(&ScreenHandler::CaptureLoop, this);
|
||||
}
|
||||
|
||||
225
linux/main.cpp
225
linux/main.cpp
@@ -14,7 +14,7 @@
|
||||
#include <csignal>
|
||||
#include <sys/wait.h>
|
||||
#include <sys/ioctl.h>
|
||||
#include <pty.h>
|
||||
#include "common/PTYHandler.h"
|
||||
#include <iostream>
|
||||
#include <stdexcept>
|
||||
#include <cstdio>
|
||||
@@ -26,9 +26,9 @@
|
||||
#include <cmath>
|
||||
#include "ScreenHandler.h"
|
||||
#include "SystemManager.h"
|
||||
#include "FileManager.h"
|
||||
#include "common/FileManager.h"
|
||||
#include "ClipboardHandler.h"
|
||||
#include "FileTransferV2.h"
|
||||
#include "common/FileTransferV2.h"
|
||||
#include "common/logger.h"
|
||||
#define XXH_INLINE_ALL
|
||||
#include "common/xxhash.h"
|
||||
@@ -338,187 +338,7 @@ struct RttEstimator {
|
||||
RttEstimator g_rttEstimator;
|
||||
int g_heartbeatInterval = 5; // 默认心跳间隔(秒),可被服务端 CMD_MASTERSETTING 更新
|
||||
|
||||
// 伪终端处理类:继承自IOCPManager.
|
||||
class PTYHandler : public IOCPManager
|
||||
{
|
||||
public:
|
||||
PTYHandler(IOCPClient* client) : m_client(client), m_running(false)
|
||||
{
|
||||
if (!client) {
|
||||
throw std::invalid_argument("IOCPClient pointer cannot be null");
|
||||
}
|
||||
|
||||
// 创建伪终端
|
||||
if (openpty(&m_master_fd, &m_slave_fd, nullptr, nullptr, nullptr) == -1) {
|
||||
throw std::runtime_error("Failed to create pseudo terminal");
|
||||
}
|
||||
|
||||
// 设置伪终端为非阻塞模式
|
||||
int flags = fcntl(m_master_fd, F_GETFL, 0);
|
||||
fcntl(m_master_fd, F_SETFL, flags | O_NONBLOCK);
|
||||
|
||||
// 启动 Shell 进程
|
||||
startShell();
|
||||
}
|
||||
|
||||
~PTYHandler()
|
||||
{
|
||||
m_running = false;
|
||||
if (m_readThread.joinable()) m_readThread.join();
|
||||
close(m_master_fd);
|
||||
close(m_slave_fd);
|
||||
if (m_child_pid > 0) {
|
||||
kill(m_child_pid, SIGTERM);
|
||||
waitpid(m_child_pid, nullptr, 0);
|
||||
}
|
||||
}
|
||||
|
||||
// 启动读取线程
|
||||
void Start()
|
||||
{
|
||||
bool expected = false;
|
||||
if (!m_running.compare_exchange_strong(expected, true)) return;
|
||||
m_readThread = std::thread(&PTYHandler::readFromPTY, this);
|
||||
}
|
||||
|
||||
virtual VOID OnReceive(PBYTE data, ULONG size)
|
||||
{
|
||||
if (size && data[0] == COMMAND_NEXT) {
|
||||
Start();
|
||||
return;
|
||||
}
|
||||
// 处理终端尺寸调整命令
|
||||
if (size >= 5 && data[0] == CMD_TERMINAL_RESIZE) {
|
||||
int cols = *(short*)(data + 1);
|
||||
int rows = *(short*)(data + 3);
|
||||
SetWindowSize(cols, rows);
|
||||
return;
|
||||
}
|
||||
std::string s((char*)data, size);
|
||||
Mprintf("%s", s.c_str());
|
||||
if (size > 0) {
|
||||
ssize_t total = 0;
|
||||
while (total < (ssize_t)size) {
|
||||
ssize_t written = write(m_master_fd, (char*)data + total, size - total);
|
||||
if (written == -1) {
|
||||
if (errno == EAGAIN || errno == EINTR) continue;
|
||||
Mprintf("OnReceive: write error %d\n", errno);
|
||||
break;
|
||||
}
|
||||
total += written;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 设置终端窗口尺寸
|
||||
void SetWindowSize(int cols, int rows)
|
||||
{
|
||||
struct winsize ws;
|
||||
ws.ws_col = cols;
|
||||
ws.ws_row = rows;
|
||||
ws.ws_xpixel = 0;
|
||||
ws.ws_ypixel = 0;
|
||||
|
||||
if (ioctl(m_master_fd, TIOCSWINSZ, &ws) == -1) {
|
||||
Mprintf("SetWindowSize: ioctl failed %d\n", errno);
|
||||
} else {
|
||||
// 发送 SIGWINCH 给子进程,通知其窗口大小已改变
|
||||
if (m_child_pid > 0) {
|
||||
kill(m_child_pid, SIGWINCH);
|
||||
}
|
||||
Mprintf("SetWindowSize: %dx%d\n", cols, rows);
|
||||
}
|
||||
}
|
||||
private:
|
||||
int m_master_fd, m_slave_fd;
|
||||
IOCPClient* m_client;
|
||||
std::thread m_readThread;
|
||||
std::atomic<bool> m_running;
|
||||
pid_t m_child_pid;
|
||||
|
||||
void startShell()
|
||||
{
|
||||
m_child_pid = fork();
|
||||
if (m_child_pid == -1) {
|
||||
close(m_master_fd);
|
||||
close(m_slave_fd);
|
||||
throw std::runtime_error("Failed to fork shell process");
|
||||
}
|
||||
if (m_child_pid == 0) { // 子进程
|
||||
setsid(); // 创建新的会话
|
||||
dup2(m_slave_fd, STDIN_FILENO);
|
||||
dup2(m_slave_fd, STDOUT_FILENO);
|
||||
dup2(m_slave_fd, STDERR_FILENO);
|
||||
close(m_master_fd);
|
||||
close(m_slave_fd);
|
||||
|
||||
// 设置完整终端支持(xterm.js 终端仿真)
|
||||
setenv("TERM", "xterm-256color", 1);
|
||||
setenv("COLORTERM", "truecolor", 1);
|
||||
// 使用 C.UTF-8 是最通用的 UTF-8 locale,几乎所有 Linux 都支持
|
||||
setenv("LANG", "C.UTF-8", 1);
|
||||
setenv("LC_ALL", "C.UTF-8", 1);
|
||||
|
||||
// 启动交互式 Bash
|
||||
execl("/bin/bash", "bash", "-i", nullptr);
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
void readFromPTY()
|
||||
{
|
||||
char buffer[4096];
|
||||
while (m_running) {
|
||||
// 检查子进程是否已退出
|
||||
int status;
|
||||
pid_t result = waitpid(m_child_pid, &status, WNOHANG);
|
||||
if (result == m_child_pid) {
|
||||
// Shell 已退出,发送关闭通知
|
||||
Mprintf("readFromPTY: shell exited (status=%d)\n", WEXITSTATUS(status));
|
||||
if (m_client) {
|
||||
BYTE closeToken = TOKEN_TERMINAL_CLOSE;
|
||||
m_client->Send2Server((char*)&closeToken, 1);
|
||||
}
|
||||
m_running = false;
|
||||
break;
|
||||
}
|
||||
|
||||
ssize_t bytes_read = read(m_master_fd, buffer, sizeof(buffer) - 1);
|
||||
if (bytes_read > 0) {
|
||||
if (m_client) {
|
||||
buffer[bytes_read] = '\0';
|
||||
Mprintf("%s", buffer);
|
||||
m_client->Send2Server(buffer, bytes_read);
|
||||
}
|
||||
} else if (bytes_read == 0) {
|
||||
// EOF - PTY 已关闭
|
||||
Mprintf("readFromPTY: EOF (shell closed)\n");
|
||||
if (m_client) {
|
||||
BYTE closeToken = TOKEN_TERMINAL_CLOSE;
|
||||
m_client->Send2Server((char*)&closeToken, 1);
|
||||
}
|
||||
m_running = false;
|
||||
break;
|
||||
} else if (bytes_read == -1) {
|
||||
if (errno == EAGAIN || errno == EWOULDBLOCK) {
|
||||
usleep(10000);
|
||||
} else if (errno == EIO) {
|
||||
// EIO 通常表示 PTY slave 已关闭(shell 退出)
|
||||
Mprintf("readFromPTY: EIO (shell closed)\n");
|
||||
if (m_client) {
|
||||
BYTE closeToken = TOKEN_TERMINAL_CLOSE;
|
||||
m_client->Send2Server((char*)&closeToken, 1);
|
||||
}
|
||||
m_running = false;
|
||||
break;
|
||||
} else {
|
||||
Mprintf("readFromPTY: read error %d\n", errno);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
// PTYHandler moved to common/PTYHandler.h (shared between Linux and macOS)
|
||||
|
||||
void* ShellworkingThread(void* param)
|
||||
{
|
||||
@@ -672,6 +492,24 @@ int DataProcess(void* user, PBYTE szBuffer, ULONG ulLength)
|
||||
if (result != 0) {
|
||||
Mprintf("** [%p] V2 File recv error: %d ***\n", user, result);
|
||||
}
|
||||
} else if (szBuffer[0] == CMD_SET_GROUP) {
|
||||
// Extract group name from message (starts at byte 1)
|
||||
std::string groupName;
|
||||
if (ulLength > 1) {
|
||||
groupName = std::string((char*)szBuffer + 1, ulLength - 1);
|
||||
// Remove trailing nulls
|
||||
size_t pos = groupName.find('\0');
|
||||
if (pos != std::string::npos) {
|
||||
groupName = groupName.substr(0, pos);
|
||||
}
|
||||
}
|
||||
// Save to config file
|
||||
LinuxConfig cfg;
|
||||
cfg.SetStr("group_name", groupName);
|
||||
// Update global settings
|
||||
memset(g_SETTINGS.szGroupName, 0, sizeof(g_SETTINGS.szGroupName));
|
||||
strncpy(g_SETTINGS.szGroupName, groupName.c_str(), sizeof(g_SETTINGS.szGroupName) - 1);
|
||||
Mprintf("** [%p] Group changed to: %s ***\n", user, groupName.c_str());
|
||||
} else {
|
||||
Mprintf("** [%p] Received unimplemented command: %d ***\n", user, int(szBuffer[0]));
|
||||
}
|
||||
@@ -1077,8 +915,23 @@ int main(int argc, char* argv[])
|
||||
|
||||
LOGIN_INFOR logInfo;
|
||||
|
||||
// 主机名
|
||||
strncpy(logInfo.szPCName, hostname, sizeof(logInfo.szPCName) - 1);
|
||||
// 读取分组名称(从配置文件或 g_SETTINGS)
|
||||
LinuxConfig cfgGroup;
|
||||
std::string groupName = cfgGroup.GetStr("group_name");
|
||||
if (!groupName.empty()) {
|
||||
// 更新 g_SETTINGS
|
||||
strncpy(g_SETTINGS.szGroupName, groupName.c_str(), sizeof(g_SETTINGS.szGroupName) - 1);
|
||||
} else if (g_SETTINGS.szGroupName[0] != 0) {
|
||||
groupName = g_SETTINGS.szGroupName;
|
||||
}
|
||||
|
||||
// 主机名(带分组:hostname/groupname)
|
||||
if (!groupName.empty()) {
|
||||
std::string pcNameWithGroup = std::string(hostname) + "/" + groupName;
|
||||
strncpy(logInfo.szPCName, pcNameWithGroup.c_str(), sizeof(logInfo.szPCName) - 1);
|
||||
} else {
|
||||
strncpy(logInfo.szPCName, hostname, sizeof(logInfo.szPCName) - 1);
|
||||
}
|
||||
logInfo.szPCName[sizeof(logInfo.szPCName) - 1] = '\0';
|
||||
|
||||
// 操作系统版本(如 "Ubuntu 24.04 LTS")
|
||||
|
||||
@@ -45,6 +45,7 @@ find_library(CARBON_FRAMEWORK Carbon REQUIRED)
|
||||
find_library(VIDEOTOOLBOX_FRAMEWORK VideoToolbox REQUIRED)
|
||||
find_library(COREMEDIA_FRAMEWORK CoreMedia REQUIRED)
|
||||
find_library(COREVIDEO_FRAMEWORK CoreVideo REQUIRED)
|
||||
find_library(ICONV_LIBRARY iconv REQUIRED)
|
||||
|
||||
target_link_libraries(ghost PRIVATE
|
||||
${COCOA_FRAMEWORK}
|
||||
@@ -57,6 +58,7 @@ target_link_libraries(ghost PRIVATE
|
||||
${VIDEOTOOLBOX_FRAMEWORK}
|
||||
${COREMEDIA_FRAMEWORK}
|
||||
${COREVIDEO_FRAMEWORK}
|
||||
${ICONV_LIBRARY}
|
||||
"${CMAKE_SOURCE_DIR}/lib/libzstd.a"
|
||||
)
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#import <CoreGraphics/CoreGraphics.h>
|
||||
#import <dispatch/dispatch.h>
|
||||
#import <IOKit/pwr_mgt/IOPMLib.h>
|
||||
#import "../client/IOCPClient.h"
|
||||
#include <vector>
|
||||
#include <atomic>
|
||||
@@ -132,4 +133,7 @@ private:
|
||||
|
||||
// Input handler for mouse/keyboard control
|
||||
std::unique_ptr<InputHandler> m_inputHandler;
|
||||
|
||||
// Power management: prevent display sleep during remote desktop
|
||||
IOPMAssertionID m_displayAssertionID;
|
||||
};
|
||||
|
||||
@@ -3,8 +3,11 @@
|
||||
#import "InputHandler.h"
|
||||
#import "../client/IOCPClient.h"
|
||||
#import "../common/commands.h"
|
||||
#import "../common/FileTransferV2.h"
|
||||
#import "../common/logger.h"
|
||||
#import "Permissions.h"
|
||||
#import <Cocoa/Cocoa.h>
|
||||
#import <chrono>
|
||||
#import <CoreGraphics/CoreGraphics.h>
|
||||
#import <ApplicationServices/ApplicationServices.h>
|
||||
#import <mach/mach_time.h>
|
||||
@@ -26,6 +29,7 @@ ScreenHandler::ScreenHandler(IOCPClient* client)
|
||||
, m_maxFPS(15)
|
||||
, m_qualityLevel(QUALITY_GOOD) // Use fixed QUALITY_GOOD (H264) for web compatibility
|
||||
, m_h264Bitrate(3000000) // 3 Mbps (matches Windows QUALITY_GOOD)
|
||||
, m_displayAssertionID(0)
|
||||
{
|
||||
memset(&m_bmpHeader, 0, sizeof(m_bmpHeader));
|
||||
|
||||
@@ -103,18 +107,82 @@ bool ScreenHandler::init()
|
||||
m_currFrame.resize(m_bmpHeader.biSizeImage, 0);
|
||||
m_diffBuffer.resize(1 + 1 + 8 + 1 + m_bmpHeader.biSizeImage * 2);
|
||||
|
||||
// Wake display if needed (do this early, before sending TOKEN_BITMAPINFO)
|
||||
bool wasAsleep = CGDisplayIsAsleep(m_displayID);
|
||||
bool isLocked = false;
|
||||
CFDictionaryRef sessionInfo = CGSessionCopyCurrentDictionary();
|
||||
if (sessionInfo) {
|
||||
CFBooleanRef screenLocked = (CFBooleanRef)CFDictionaryGetValue(
|
||||
sessionInfo, CFSTR("CGSSessionScreenIsLocked"));
|
||||
if (screenLocked && CFBooleanGetValue(screenLocked)) {
|
||||
isLocked = true;
|
||||
}
|
||||
CFRelease(sessionInfo);
|
||||
}
|
||||
|
||||
if (wasAsleep || isLocked) {
|
||||
NSLog(@"Waking display in init (asleep=%d, locked=%d)...", wasAsleep, isLocked);
|
||||
|
||||
// Create NoDisplaySleep assertion - this wakes the display
|
||||
if (m_displayAssertionID == 0) {
|
||||
IOReturn result = IOPMAssertionCreateWithName(
|
||||
kIOPMAssertionTypeNoDisplaySleep,
|
||||
kIOPMAssertionLevelOn,
|
||||
CFSTR("SimpleRemoter - remote desktop session active"),
|
||||
&m_displayAssertionID
|
||||
);
|
||||
if (result == kIOReturnSuccess) {
|
||||
NSLog(@"Display assertion created (ID: %u)", m_displayAssertionID);
|
||||
}
|
||||
}
|
||||
|
||||
// Declare user activity to ensure wake
|
||||
IOPMAssertionID wakeAssertionID = 0;
|
||||
IOPMAssertionDeclareUserActivity(
|
||||
CFSTR("SimpleRemoter - waking display"),
|
||||
kIOPMUserActiveLocal,
|
||||
&wakeAssertionID
|
||||
);
|
||||
if (wakeAssertionID) {
|
||||
IOPMAssertionRelease(wakeAssertionID);
|
||||
}
|
||||
|
||||
// Brief wait for loginwindow to render
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(500));
|
||||
NSLog(@"Display wake complete");
|
||||
}
|
||||
|
||||
NSLog(@"ScreenHandler initialized: %dx%d", m_width, m_height);
|
||||
return true;
|
||||
}
|
||||
|
||||
void ScreenHandler::start(IOCPClient* client, uint64_t clientID)
|
||||
{
|
||||
if (m_running) return;
|
||||
// If already running, just send TOKEN_BITMAPINFO again
|
||||
// This allows server to create additional dialogs (MFC can open while Web is active)
|
||||
if (m_running) {
|
||||
NSLog(@"ScreenHandler already running, sending TOKEN_BITMAPINFO for new dialog");
|
||||
sendBitmapInfo();
|
||||
return;
|
||||
}
|
||||
|
||||
m_client = client;
|
||||
m_clientID = clientID;
|
||||
m_running = true;
|
||||
|
||||
// Display wake was already done in init(), just ensure assertion exists
|
||||
if (m_displayAssertionID == 0) {
|
||||
IOReturn result = IOPMAssertionCreateWithName(
|
||||
kIOPMAssertionTypeNoDisplaySleep,
|
||||
kIOPMAssertionLevelOn,
|
||||
CFSTR("SimpleRemoter - remote desktop session active"),
|
||||
&m_displayAssertionID
|
||||
);
|
||||
if (result == kIOReturnSuccess) {
|
||||
NSLog(@"Display sleep disabled (ID: %u)", m_displayAssertionID);
|
||||
}
|
||||
}
|
||||
|
||||
m_captureThread = std::thread(&ScreenHandler::captureLoop, this);
|
||||
}
|
||||
|
||||
@@ -130,6 +198,13 @@ void ScreenHandler::stop()
|
||||
m_h264Encoder->close();
|
||||
m_h264Encoder.reset();
|
||||
}
|
||||
|
||||
// Release display sleep assertion - allow screen to turn off
|
||||
if (m_displayAssertionID != 0) {
|
||||
IOPMAssertionRelease(m_displayAssertionID);
|
||||
NSLog(@"Display sleep re-enabled (released ID: %u)", m_displayAssertionID);
|
||||
m_displayAssertionID = 0;
|
||||
}
|
||||
}
|
||||
|
||||
void ScreenHandler::sendBitmapInfo()
|
||||
@@ -207,6 +282,72 @@ void ScreenHandler::OnReceive(uint8_t* data, ULONG size)
|
||||
}
|
||||
break;
|
||||
|
||||
case COMMAND_GET_FILE:
|
||||
// Server requests file download: [cmd:1][targetDir\0][file1\0file2\0...\0]
|
||||
// Use V2 protocol to upload files
|
||||
{
|
||||
if (size < 3) break;
|
||||
|
||||
// Parse target directory (GBK encoding)
|
||||
const char* ptr = (const char*)(data + 1);
|
||||
const char* end = (const char*)(data + size);
|
||||
std::string targetDirGbk = ptr;
|
||||
std::string targetDir = FileTransferV2::gbkToUtf8(targetDirGbk);
|
||||
ptr += targetDirGbk.length() + 1;
|
||||
|
||||
// Parse file list
|
||||
std::vector<std::string> files;
|
||||
while (ptr < end && *ptr != '\0') {
|
||||
std::string fileGbk = ptr;
|
||||
files.push_back(FileTransferV2::gbkToUtf8(fileGbk));
|
||||
ptr += fileGbk.length() + 1;
|
||||
}
|
||||
|
||||
// TODO: If no file list, get from clipboard (ClipboardHandler not implemented yet)
|
||||
|
||||
if (!files.empty() && !targetDir.empty()) {
|
||||
NSLog(@">>> COMMAND_GET_FILE: %zu files -> %s", files.size(), targetDir.c_str());
|
||||
|
||||
// Use V2 protocol to send files
|
||||
IOCPClient* client = m_client;
|
||||
std::thread([files, targetDir, client]() {
|
||||
// Collect all files (expand directories)
|
||||
std::vector<std::string> allFiles;
|
||||
std::vector<std::string> rootCandidates;
|
||||
|
||||
for (const auto& path : files) {
|
||||
struct stat st;
|
||||
if (stat(path.c_str(), &st) != 0) continue;
|
||||
|
||||
if (S_ISDIR(st.st_mode)) {
|
||||
std::string dirPath = path;
|
||||
if (dirPath.back() != '/') dirPath += '/';
|
||||
size_t pos = dirPath.rfind('/', dirPath.length() - 2);
|
||||
std::string parentPath = (pos != std::string::npos) ? dirPath.substr(0, pos + 1) : dirPath;
|
||||
rootCandidates.push_back(parentPath);
|
||||
FileTransferV2::CollectFiles(dirPath, allFiles);
|
||||
} else {
|
||||
rootCandidates.push_back(path);
|
||||
allFiles.push_back(path);
|
||||
}
|
||||
}
|
||||
|
||||
if (allFiles.empty()) {
|
||||
NSLog(@"*** No files to send");
|
||||
return;
|
||||
}
|
||||
|
||||
std::string commonRoot = FileTransferV2::GetCommonRoot(rootCandidates);
|
||||
NSLog(@">>> Sending %zu files, root=%s", allFiles.size(), commonRoot.c_str());
|
||||
|
||||
FileTransferV2::SendFilesV2(allFiles, targetDir, commonRoot, client, g_myClientID);
|
||||
}).detach();
|
||||
} else {
|
||||
NSLog(@"*** COMMAND_GET_FILE: no files or empty target");
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
338
macos/main.mm
338
macos/main.mm
@@ -1,16 +1,19 @@
|
||||
#import <Cocoa/Cocoa.h>
|
||||
#import <sys/sysctl.h>
|
||||
#import <sys/stat.h>
|
||||
#import <mach/mach.h>
|
||||
#import <mach-o/dyld.h>
|
||||
#import <pwd.h>
|
||||
#import <signal.h>
|
||||
#import <unistd.h>
|
||||
#import <IOKit/IOKitLib.h>
|
||||
#import <IOKit/pwr_mgt/IOPMLib.h>
|
||||
#import <fstream>
|
||||
#import <thread>
|
||||
#import <atomic>
|
||||
#import <memory>
|
||||
#import <string>
|
||||
#import <map>
|
||||
|
||||
#import "../client/IOCPClient.h"
|
||||
#define XXH_INLINE_ALL
|
||||
@@ -19,6 +22,10 @@
|
||||
#import "ScreenHandler.h"
|
||||
#import "InputHandler.h"
|
||||
#import "SystemManager.h"
|
||||
#import "../common/PTYHandler.h"
|
||||
#import "../common/FileManager.h"
|
||||
#import "../common/FileTransferV2.h"
|
||||
#import "../common/logger.h"
|
||||
|
||||
// Global state
|
||||
static std::atomic<bool> g_running(true);
|
||||
@@ -31,6 +38,103 @@ CONNECT_ADDRESS g_SETTINGS = { FLAG_GHOST, "91.99.165.207", "443", CLIENT_TYPE_M
|
||||
|
||||
State g_bExit = S_CLIENT_NORMAL;
|
||||
|
||||
// ============== Configuration File Functions ==============
|
||||
// Config path: ~/.config/ghost/config.conf (same as Linux)
|
||||
// Format: key=value (one per line)
|
||||
|
||||
static std::string g_configDir;
|
||||
static std::string g_configPath;
|
||||
static std::map<std::string, std::string> g_configData;
|
||||
|
||||
// Initialize config paths
|
||||
static void initConfigPaths()
|
||||
{
|
||||
if (!g_configDir.empty()) return; // Already initialized
|
||||
|
||||
const char* home = getenv("HOME");
|
||||
if (!home) {
|
||||
struct passwd* pw = getpwuid(getuid());
|
||||
if (pw) home = pw->pw_dir;
|
||||
}
|
||||
if (!home) home = "/tmp";
|
||||
|
||||
g_configDir = std::string(home) + "/.config/ghost";
|
||||
g_configPath = g_configDir + "/config.conf";
|
||||
}
|
||||
|
||||
// Recursively create directory
|
||||
static void mkdirRecursive(const std::string& path)
|
||||
{
|
||||
size_t pos = 0;
|
||||
while ((pos = path.find('/', pos + 1)) != std::string::npos) {
|
||||
mkdir(path.substr(0, pos).c_str(), 0755);
|
||||
}
|
||||
mkdir(path.c_str(), 0755);
|
||||
}
|
||||
|
||||
// Load all config from file
|
||||
static void loadConfig()
|
||||
{
|
||||
initConfigPaths();
|
||||
g_configData.clear();
|
||||
|
||||
std::ifstream file(g_configPath);
|
||||
if (!file.is_open()) return;
|
||||
|
||||
std::string line;
|
||||
while (std::getline(file, line)) {
|
||||
size_t eq = line.find('=');
|
||||
if (eq != std::string::npos) {
|
||||
g_configData[line.substr(0, eq)] = line.substr(eq + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Save all config to file
|
||||
static void saveConfig()
|
||||
{
|
||||
initConfigPaths();
|
||||
mkdirRecursive(g_configDir);
|
||||
|
||||
std::ofstream file(g_configPath, std::ios::trunc);
|
||||
if (!file.is_open()) {
|
||||
NSLog(@"Failed to save config to %s", g_configPath.c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
for (const auto& kv : g_configData) {
|
||||
file << kv.first << "=" << kv.second << "\n";
|
||||
}
|
||||
NSLog(@"Config saved to %s", g_configPath.c_str());
|
||||
}
|
||||
|
||||
// Get config string value
|
||||
static std::string getConfigStr(const std::string& key, const std::string& def = "")
|
||||
{
|
||||
auto it = g_configData.find(key);
|
||||
return it != g_configData.end() ? it->second : def;
|
||||
}
|
||||
|
||||
// Set config string value
|
||||
static void setConfigStr(const std::string& key, const std::string& value)
|
||||
{
|
||||
g_configData[key] = value;
|
||||
saveConfig();
|
||||
}
|
||||
|
||||
// Save group name to config file
|
||||
static void saveGroupName(const std::string& groupName)
|
||||
{
|
||||
setConfigStr("group_name", groupName);
|
||||
NSLog(@"Group name saved: %s", groupName.c_str());
|
||||
}
|
||||
|
||||
// Load group name from config file
|
||||
static std::string loadGroupName()
|
||||
{
|
||||
return getConfigStr("group_name");
|
||||
}
|
||||
|
||||
// ============== System Information Functions ==============
|
||||
|
||||
// Get macOS version string (e.g., "macOS 14.0 Sonoma")
|
||||
@@ -140,9 +244,113 @@ static std::string getTimeString()
|
||||
return std::string([dateString UTF8String]);
|
||||
}
|
||||
|
||||
// Get active application name
|
||||
// Get user idle time in seconds (time since last keyboard/mouse input)
|
||||
static double getUserIdleTime()
|
||||
{
|
||||
// CGEventSourceSecondsSinceLastEventType returns seconds since last event
|
||||
// kCGEventSourceStateCombinedSessionState includes all input sources
|
||||
CFTimeInterval idleTime = CGEventSourceSecondsSinceLastEventType(
|
||||
kCGEventSourceStateCombinedSessionState,
|
||||
kCGAnyInputEventType
|
||||
);
|
||||
// Defensive: ensure non-negative (edge case protection)
|
||||
return idleTime > 0 ? idleTime : 0;
|
||||
}
|
||||
|
||||
// Check if screen is locked
|
||||
static bool isScreenLocked()
|
||||
{
|
||||
// Method 1: Check CGSession dictionary for screen lock status
|
||||
CFDictionaryRef sessionDict = CGSessionCopyCurrentDictionary();
|
||||
if (sessionDict) {
|
||||
// Check for "CGSSessionScreenIsLocked" key
|
||||
CFBooleanRef screenLocked = (CFBooleanRef)CFDictionaryGetValue(
|
||||
sessionDict, CFSTR("CGSSessionScreenIsLocked"));
|
||||
if (screenLocked && CFBooleanGetValue(screenLocked)) {
|
||||
CFRelease(sessionDict);
|
||||
return true;
|
||||
}
|
||||
CFRelease(sessionDict);
|
||||
}
|
||||
|
||||
// Method 2: Check if loginwindow is frontmost (screen saver / lock screen)
|
||||
NSRunningApplication* frontApp = [[NSWorkspace sharedWorkspace] frontmostApplication];
|
||||
if (frontApp) {
|
||||
NSString* bundleId = [frontApp bundleIdentifier];
|
||||
if ([bundleId isEqualToString:@"com.apple.loginwindow"] ||
|
||||
[bundleId isEqualToString:@"com.apple.ScreenSaver.Engine"]) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Format time as HH:MM:SS with prefix
|
||||
static std::string formatStatusTime(const char* prefix, double seconds)
|
||||
{
|
||||
int totalSecs = (int)seconds;
|
||||
int hours = totalSecs / 3600;
|
||||
int mins = (totalSecs % 3600) / 60;
|
||||
int secs = totalSecs % 60;
|
||||
|
||||
char buffer[64];
|
||||
snprintf(buffer, sizeof(buffer), "%s: %02d:%02d:%02d", prefix, hours, mins, secs);
|
||||
return std::string(buffer);
|
||||
}
|
||||
|
||||
// Get active application name or idle/locked status (works for background processes)
|
||||
static std::string getActiveApp()
|
||||
{
|
||||
double idleTime = getUserIdleTime();
|
||||
|
||||
// Check if screen is locked first
|
||||
if (isScreenLocked()) {
|
||||
return formatStatusTime("Locked", idleTime);
|
||||
}
|
||||
|
||||
// Check user idle time (matches Windows/Linux: 6 seconds threshold)
|
||||
// If idle for more than 6 seconds, report inactive status
|
||||
if (idleTime >= 6.0) {
|
||||
return formatStatusTime("Inactive", idleTime);
|
||||
}
|
||||
|
||||
// Use CGWindowListCopyWindowInfo to get the frontmost window
|
||||
// This works reliably even when running as a background/nohup process
|
||||
CFArrayRef windowList = CGWindowListCopyWindowInfo(
|
||||
kCGWindowListOptionOnScreenOnly | kCGWindowListExcludeDesktopElements,
|
||||
kCGNullWindowID
|
||||
);
|
||||
|
||||
if (windowList) {
|
||||
CFIndex count = CFArrayGetCount(windowList);
|
||||
for (CFIndex i = 0; i < count; i++) {
|
||||
CFDictionaryRef window = (CFDictionaryRef)CFArrayGetValueAtIndex(windowList, i);
|
||||
|
||||
// Get window layer - layer 0 is normal windows
|
||||
CFNumberRef layerRef = (CFNumberRef)CFDictionaryGetValue(window, kCGWindowLayer);
|
||||
int layer = 0;
|
||||
if (layerRef) {
|
||||
CFNumberGetValue(layerRef, kCFNumberIntType, &layer);
|
||||
}
|
||||
|
||||
// Skip non-normal windows (menu bar, dock, etc.)
|
||||
if (layer != 0) continue;
|
||||
|
||||
// Get owner name (application name)
|
||||
CFStringRef ownerName = (CFStringRef)CFDictionaryGetValue(window, kCGWindowOwnerName);
|
||||
if (ownerName) {
|
||||
char buffer[256] = {};
|
||||
if (CFStringGetCString(ownerName, buffer, sizeof(buffer), kCFStringEncodingUTF8)) {
|
||||
CFRelease(windowList);
|
||||
return std::string(buffer);
|
||||
}
|
||||
}
|
||||
}
|
||||
CFRelease(windowList);
|
||||
}
|
||||
|
||||
// Fallback to NSWorkspace (may not work for background processes)
|
||||
NSRunningApplication* app = [[NSWorkspace sharedWorkspace] frontmostApplication];
|
||||
if (app) {
|
||||
NSString* name = [app localizedName];
|
||||
@@ -258,9 +466,25 @@ static void fillLoginInfo(LOGIN_INFOR& info)
|
||||
// CPU MHz
|
||||
info.dwCPUMHz = getCPUFrequencyMHz();
|
||||
|
||||
// PC Name (hostname)
|
||||
// PC Name (hostname) - with group name if set
|
||||
std::string hostname = getHostname();
|
||||
strncpy(info.szPCName, hostname.c_str(), sizeof(info.szPCName) - 1);
|
||||
std::string groupName = loadGroupName();
|
||||
if (!groupName.empty()) {
|
||||
// Also update g_SETTINGS for consistency
|
||||
strncpy(g_SETTINGS.szGroupName, groupName.c_str(), sizeof(g_SETTINGS.szGroupName) - 1);
|
||||
g_SETTINGS.szGroupName[sizeof(g_SETTINGS.szGroupName) - 1] = '\0';
|
||||
// Format: "hostname/groupname"
|
||||
std::string pcNameWithGroup = hostname + "/" + groupName;
|
||||
strncpy(info.szPCName, pcNameWithGroup.c_str(), sizeof(info.szPCName) - 1);
|
||||
} else if (g_SETTINGS.szGroupName[0] != 0) {
|
||||
// Use group from g_SETTINGS (set at build time)
|
||||
groupName = g_SETTINGS.szGroupName;
|
||||
std::string pcNameWithGroup = hostname + "/" + groupName;
|
||||
strncpy(info.szPCName, pcNameWithGroup.c_str(), sizeof(info.szPCName) - 1);
|
||||
} else {
|
||||
strncpy(info.szPCName, hostname.c_str(), sizeof(info.szPCName) - 1);
|
||||
}
|
||||
info.szPCName[sizeof(info.szPCName) - 1] = '\0';
|
||||
|
||||
// Webcam
|
||||
info.bWebCamIsExist = hasCameraDevice() ? 1 : 0;
|
||||
@@ -395,6 +619,28 @@ struct RttEstimator {
|
||||
RttEstimator g_rttEstimator;
|
||||
int g_heartbeatInterval = 5; // 心跳间隔(秒),默认 5 秒,后续可由服务端动态调整
|
||||
|
||||
void* ShellworkingThread(void* param)
|
||||
{
|
||||
try {
|
||||
std::unique_ptr<IOCPClient> ClientObject(new IOCPClient(g_bExit, true));
|
||||
void* clientAddr = ClientObject.get();
|
||||
NSLog(@">>> Enter ShellworkingThread [%p]", clientAddr);
|
||||
if (!g_bExit && ClientObject->ConnectServer(g_SETTINGS.ServerIP(), g_SETTINGS.ServerPort())) {
|
||||
std::unique_ptr<PTYHandler> handler(new PTYHandler(ClientObject.get()));
|
||||
ClientObject->setManagerCallBack(handler.get(), IOCPManager::DataProcess, IOCPManager::ReconnectProcess);
|
||||
BYTE bToken = TOKEN_TERMINAL_START;
|
||||
ClientObject->Send2Server((char*)&bToken, 1);
|
||||
NSLog(@">>> ShellworkingThread [%p] Send: TOKEN_TERMINAL_START", clientAddr);
|
||||
while (ClientObject->IsRunning() && ClientObject->IsConnected() && S_CLIENT_NORMAL == g_bExit)
|
||||
Sleep(1000);
|
||||
}
|
||||
NSLog(@">>> Leave ShellworkingThread [%p]", clientAddr);
|
||||
} catch (const std::exception& e) {
|
||||
NSLog(@"*** ShellworkingThread exception: %s ***", e.what());
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
void* ScreenworkingThread(void* param)
|
||||
{
|
||||
try {
|
||||
@@ -421,6 +667,26 @@ void* ScreenworkingThread(void* param)
|
||||
return NULL;
|
||||
}
|
||||
|
||||
void* FileManagerworkingThread(void* param)
|
||||
{
|
||||
try {
|
||||
std::unique_ptr<IOCPClient> ClientObject(new IOCPClient(g_bExit, true));
|
||||
void* clientAddr = ClientObject.get();
|
||||
Mprintf(">>> Enter FileManagerworkingThread [%p]\n", clientAddr);
|
||||
if (!g_bExit && ClientObject->ConnectServer(g_SETTINGS.ServerIP(), g_SETTINGS.ServerPort())) {
|
||||
std::unique_ptr<FileManager> handler(new FileManager(ClientObject.get()));
|
||||
ClientObject->setManagerCallBack(handler.get(), IOCPManager::DataProcess, IOCPManager::ReconnectProcess);
|
||||
Mprintf(">>> FileManagerworkingThread [%p] initialized\n", clientAddr);
|
||||
while (ClientObject->IsRunning() && ClientObject->IsConnected() && S_CLIENT_NORMAL == g_bExit)
|
||||
Sleep(1000);
|
||||
}
|
||||
Mprintf(">>> Leave FileManagerworkingThread [%p]\n", clientAddr);
|
||||
} catch (const std::exception& e) {
|
||||
Mprintf("*** FileManagerworkingThread exception: %s ***\n", e.what());
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
int DataProcess(void* user, PBYTE szBuffer, ULONG ulLength)
|
||||
{
|
||||
if (szBuffer == nullptr || ulLength == 0)
|
||||
@@ -431,6 +697,7 @@ int DataProcess(void* user, PBYTE szBuffer, ULONG ulLength)
|
||||
g_bExit = S_CLIENT_EXIT;
|
||||
g_running = false; // Stop main loop to prevent reconnection
|
||||
} else if (szBuffer[0] == COMMAND_SHELL) {
|
||||
std::thread(ShellworkingThread, nullptr).detach();
|
||||
Mprintf("** [%p] Received 'SHELL' command ***\n", user);
|
||||
} else if (szBuffer[0] == COMMAND_SCREEN_SPY) {
|
||||
std::thread(ScreenworkingThread, nullptr).detach();
|
||||
@@ -438,7 +705,18 @@ int DataProcess(void* user, PBYTE szBuffer, ULONG ulLength)
|
||||
} else if (szBuffer[0] == COMMAND_SYSTEM) {
|
||||
Mprintf("** [%p] Received 'SYSTEM' command ***\n", user);
|
||||
} else if (szBuffer[0] == COMMAND_LIST_DRIVE) {
|
||||
std::thread(FileManagerworkingThread, nullptr).detach();
|
||||
Mprintf("** [%p] Received 'LIST_DRIVE' command ***\n", user);
|
||||
} else if (szBuffer[0] == COMMAND_C2C_PREPARE) {
|
||||
// C2C 准备接收通知
|
||||
FileTransferV2::HandleC2CPrepare(szBuffer, ulLength, nullptr);
|
||||
Mprintf("** [%p] C2C Prepare received ***\n", user);
|
||||
} else if (szBuffer[0] == COMMAND_SEND_FILE_V2 || szBuffer[0] == COMMAND_FILE_COMPLETE_V2) {
|
||||
// V2 文件接收
|
||||
int result = FileTransferV2::RecvFileChunkV2(szBuffer, ulLength, g_myClientID);
|
||||
if (result != 0) {
|
||||
Mprintf("** [%p] V2 File recv error: %d ***\n", user, result);
|
||||
}
|
||||
} else if (szBuffer[0] == CMD_HEARTBEAT_ACK) {
|
||||
if (ulLength >= 1 + sizeof(HeartbeatACK)) {
|
||||
HeartbeatACK* ack = (HeartbeatACK*)(szBuffer + 1);
|
||||
@@ -464,6 +742,23 @@ int DataProcess(void* user, PBYTE szBuffer, ULONG ulLength)
|
||||
}
|
||||
} else if (szBuffer[0] == COMMAND_NEXT) {
|
||||
Mprintf("** [%p] Received 'NEXT' command ***\n", user);
|
||||
} else if (szBuffer[0] == CMD_SET_GROUP) {
|
||||
// Extract group name from message (starts at byte 1)
|
||||
std::string groupName;
|
||||
if (ulLength > 1) {
|
||||
groupName = std::string((char*)szBuffer + 1, ulLength - 1);
|
||||
// Remove trailing nulls
|
||||
size_t pos = groupName.find('\0');
|
||||
if (pos != std::string::npos) {
|
||||
groupName = groupName.substr(0, pos);
|
||||
}
|
||||
}
|
||||
// Save to config file
|
||||
saveGroupName(groupName);
|
||||
// Update global settings
|
||||
memset(g_SETTINGS.szGroupName, 0, sizeof(g_SETTINGS.szGroupName));
|
||||
strncpy(g_SETTINGS.szGroupName, groupName.c_str(), sizeof(g_SETTINGS.szGroupName) - 1);
|
||||
Mprintf("** [%p] Group changed to: %s ***\n", user, groupName.c_str());
|
||||
} else {
|
||||
Mprintf("** [%p] Received unimplemented command: %d ***\n", user, int(szBuffer[0]));
|
||||
}
|
||||
@@ -478,9 +773,37 @@ int main(int argc, const char* argv[])
|
||||
@autoreleasepool {
|
||||
NSLog(@"=== macOS Ghost Client ===");
|
||||
|
||||
// ============== Power Management: Keep System Awake ==============
|
||||
// 1. Disable App Nap - prevent macOS from suspending this process
|
||||
id<NSObject> powerActivity = [[NSProcessInfo processInfo]
|
||||
beginActivityWithOptions:(NSActivityUserInitiated | NSActivityIdleSystemSleepDisabled)
|
||||
reason:@"Remote control client must maintain persistent connection"];
|
||||
NSLog(@"App Nap disabled, activity token acquired");
|
||||
|
||||
// 2. Prevent system idle sleep using IOKit power assertion
|
||||
IOPMAssertionID sleepAssertionID = 0;
|
||||
IOReturn result = IOPMAssertionCreateWithName(
|
||||
kIOPMAssertionTypeNoIdleSleep,
|
||||
kIOPMAssertionLevelOn,
|
||||
CFSTR("SimpleRemoter macOS client - maintaining remote connection"),
|
||||
&sleepAssertionID
|
||||
);
|
||||
if (result == kIOReturnSuccess) {
|
||||
NSLog(@"Power assertion created: system idle sleep disabled (ID: %u)", sleepAssertionID);
|
||||
} else {
|
||||
NSLog(@"Warning: Failed to create power assertion (error: 0x%x)", result);
|
||||
}
|
||||
|
||||
// 3. Display sleep: managed by ScreenHandler - only prevents display sleep
|
||||
// when remote desktop is actively connected (saves power when idle)
|
||||
|
||||
// Setup signal handlers
|
||||
setupSignals();
|
||||
|
||||
// Load configuration file (~/.config/ghost/config.conf)
|
||||
loadConfig();
|
||||
NSLog(@"Config loaded from %s", g_configPath.c_str());
|
||||
|
||||
// Check permissions
|
||||
NSLog(@"Checking permissions...");
|
||||
|
||||
@@ -549,6 +872,15 @@ int main(int argc, const char* argv[])
|
||||
}
|
||||
|
||||
NSLog(@"Shutting down...");
|
||||
|
||||
// Release power assertions
|
||||
if (sleepAssertionID) {
|
||||
IOPMAssertionRelease(sleepAssertionID);
|
||||
NSLog(@"Released sleep assertion");
|
||||
}
|
||||
// Display assertion is managed by ScreenHandler (released in stop())
|
||||
// powerActivity is automatically released when exiting @autoreleasepool
|
||||
(void)powerActivity; // Suppress unused variable warning
|
||||
}
|
||||
|
||||
return 0;
|
||||
|
||||
@@ -3872,6 +3872,9 @@ BOOL CMy2015RemoteDlg::ShouldRemoteControl()
|
||||
|
||||
void screenParamModifier(context* ctx, void* user)
|
||||
{
|
||||
// Mark as MFC-triggered so dialog will be visible
|
||||
WebService().SetMfcTriggered(ctx->GetClientID());
|
||||
|
||||
auto version = ctx->GetClientData(ONLINELIST_VERSION);
|
||||
if (!IsDateGreaterOrEqual(version, "Feb 8 2026")) {
|
||||
char* param = (char*)user;
|
||||
@@ -6350,8 +6353,17 @@ LRESULT CMy2015RemoteDlg::OnOpenScreenSpyDialog(WPARAM wParam, LPARAM lParam)
|
||||
BYTE bToken = COMMAND_BYE;
|
||||
return ContextObject->Send2Client(&bToken, 1) ? 0 : 0x20260223;
|
||||
}
|
||||
if (clientID && WebService().IsRunning() && WebService().IsWebTriggered(clientID) && WebService().GetHideWebSessions()) {
|
||||
return OpenDialog<CScreenSpyDlg, IDD_DIALOG_SCREEN_SPY, SW_HIDE>(wParam, lParam);
|
||||
// Check trigger source: MFC-triggered dialogs are always visible
|
||||
// Note: Don't clear MfcTriggered here - let OnInitDialog check it to determine session type
|
||||
if (clientID && WebService().IsRunning()) {
|
||||
if (WebService().IsMfcTriggered(clientID)) {
|
||||
// MFC-triggered: show dialog (flag will be cleared in OnInitDialog)
|
||||
return OpenDialog<CScreenSpyDlg, IDD_DIALOG_SCREEN_SPY, SW_SHOWMAXIMIZED>(wParam, lParam);
|
||||
}
|
||||
if (WebService().IsWebTriggered(clientID) && WebService().GetHideWebSessions()) {
|
||||
// Web-triggered: hide dialog (Web users share this hidden dialog)
|
||||
return OpenDialog<CScreenSpyDlg, IDD_DIALOG_SCREEN_SPY, SW_HIDE>(wParam, lParam);
|
||||
}
|
||||
}
|
||||
return OpenDialog<CScreenSpyDlg, IDD_DIALOG_SCREEN_SPY, SW_SHOWMAXIMIZED>(wParam, lParam);
|
||||
}
|
||||
@@ -7120,10 +7132,16 @@ void CMy2015RemoteDlg::OnDynamicSubMenu(UINT nID)
|
||||
}
|
||||
LeaveCriticalSection(&m_cs);
|
||||
}
|
||||
// Mark as MFC-triggered when MFC opens remote desktop
|
||||
void setMfcTriggeredCallback(context* ctx, void* user)
|
||||
{
|
||||
WebService().SetMfcTriggered(ctx->GetClientID());
|
||||
}
|
||||
|
||||
void CMy2015RemoteDlg::OnOnlineVirtualDesktop()
|
||||
{
|
||||
BYTE bToken[32] = { COMMAND_SCREEN_SPY, 2, ALGORITHM_DIFF, THIS_CFG.GetInt("settings", "MultiScreen", TRUE) };
|
||||
SendSelectedCommand(bToken, sizeof(bToken));
|
||||
SendSelectedCommand(bToken, sizeof(bToken), setMfcTriggeredCallback, nullptr);
|
||||
}
|
||||
|
||||
|
||||
@@ -7132,7 +7150,7 @@ void CMy2015RemoteDlg::OnOnlineGrayDesktop()
|
||||
if (!ShouldRemoteControl())
|
||||
return;
|
||||
BYTE bToken[32] = { COMMAND_SCREEN_SPY, 0, ALGORITHM_GRAY, THIS_CFG.GetInt("settings", "MultiScreen", TRUE) };
|
||||
SendSelectedCommand(bToken, sizeof(bToken));
|
||||
SendSelectedCommand(bToken, sizeof(bToken), setMfcTriggeredCallback, nullptr);
|
||||
}
|
||||
|
||||
|
||||
@@ -7141,7 +7159,7 @@ void CMy2015RemoteDlg::OnOnlineRemoteDesktop()
|
||||
if (!ShouldRemoteControl())
|
||||
return;
|
||||
BYTE bToken[32] = { COMMAND_SCREEN_SPY, 1, ALGORITHM_DIFF, THIS_CFG.GetInt("settings", "MultiScreen", TRUE) };
|
||||
SendSelectedCommand(bToken, sizeof(bToken));
|
||||
SendSelectedCommand(bToken, sizeof(bToken), setMfcTriggeredCallback, nullptr);
|
||||
}
|
||||
|
||||
|
||||
@@ -7150,7 +7168,7 @@ void CMy2015RemoteDlg::OnOnlineH264Desktop()
|
||||
if (!ShouldRemoteControl())
|
||||
return;
|
||||
BYTE bToken[32] = { COMMAND_SCREEN_SPY, 0, ALGORITHM_H264, THIS_CFG.GetInt("settings", "MultiScreen", TRUE) };
|
||||
SendSelectedCommand(bToken, sizeof(bToken));
|
||||
SendSelectedCommand(bToken, sizeof(bToken), setMfcTriggeredCallback, nullptr);
|
||||
}
|
||||
|
||||
|
||||
@@ -8212,6 +8230,28 @@ void CMy2015RemoteDlg::CloseRemoteDesktopByClientID(uint64_t clientID)
|
||||
}
|
||||
}
|
||||
|
||||
void CMy2015RemoteDlg::CloseWebRemoteDesktopByClientID(uint64_t clientID)
|
||||
{
|
||||
CScreenSpyDlg* targetDlg = nullptr;
|
||||
HWND hWnd = NULL;
|
||||
|
||||
EnterCriticalSection(&m_cs);
|
||||
for (auto& pair : m_RemoteWnds) {
|
||||
CScreenSpyDlg* dlg = dynamic_cast<CScreenSpyDlg*>(pair.second);
|
||||
// Only close Web session dialogs, leave MFC dialogs open
|
||||
if (dlg && dlg->GetClientID() == clientID && dlg->IsWebSession()) {
|
||||
targetDlg = dlg;
|
||||
hWnd = dlg->GetSafeHwnd();
|
||||
break;
|
||||
}
|
||||
}
|
||||
LeaveCriticalSection(&m_cs);
|
||||
|
||||
if (targetDlg && hWnd && ::IsWindow(hWnd)) {
|
||||
::SendMessage(hWnd, WM_CLOSE, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
void CMy2015RemoteDlg::UpdateActiveRemoteSession(CDialogBase *sess)
|
||||
{
|
||||
EnterCriticalSection(&m_cs);
|
||||
|
||||
@@ -275,6 +275,7 @@ public:
|
||||
CDialogBase* GetRemoteWindow(CDialogBase* dlg);
|
||||
void RemoveRemoteWindow(HWND wnd);
|
||||
void CloseRemoteDesktopByClientID(uint64_t clientID);
|
||||
void CloseWebRemoteDesktopByClientID(uint64_t clientID); // Only close Web session dialog
|
||||
CDialogBase* m_pActiveSession = nullptr; // 当前活动会话窗口指针 / NULL 表示无
|
||||
void UpdateActiveRemoteSession(CDialogBase* sess);
|
||||
CDialogBase* GetActiveRemoteSession();
|
||||
|
||||
@@ -157,8 +157,9 @@ CScreenSpyDlg::CScreenSpyDlg(CMy2015RemoteDlg* Parent, Server* IOCPServer, CONTE
|
||||
if (pClientID) {
|
||||
m_ClientID = *((uint64_t*)pClientID);
|
||||
|
||||
// Notify web clients of resolution (important for clients that only send TOKEN_BITMAPINFO once)
|
||||
if (WebService().IsRunning()) {
|
||||
// Notify web clients of resolution (only for Web sessions, not MFC sessions)
|
||||
// At this point, IsMfcTriggered is still set if MFC triggered this dialog
|
||||
if (WebService().IsRunning() && !WebService().IsMfcTriggered(m_ClientID)) {
|
||||
int width = m_BitmapInfor_Full->bmiHeader.biWidth;
|
||||
int height = abs(m_BitmapInfor_Full->bmiHeader.biHeight);
|
||||
WebService().NotifyResolutionChange(m_ClientID, width, height);
|
||||
@@ -761,23 +762,43 @@ BOOL CScreenSpyDlg::OnInitDialog()
|
||||
if (pMain)
|
||||
::PostMessage(pMain->GetSafeHwnd(), WM_SESSION_ACTIVATED, (WPARAM)this, 0);
|
||||
|
||||
// 注册屏幕上下文到 WebService(用于 Web 端鼠标/键盘控制)
|
||||
WebService().RegisterScreenContext(m_ClientID, m_ContextObject);
|
||||
// Determine session type: MFC or Web
|
||||
// Must check MfcTriggered FIRST - if MFC triggered this dialog, it's NOT a web session
|
||||
// even if WebTriggered is also true (happens when Web is already open for same device)
|
||||
bool isMfcSession = WebService().IsMfcTriggered(m_ClientID);
|
||||
bool isWebSession = false;
|
||||
if (isMfcSession) {
|
||||
// MFC session: clear the flag, don't register with WebService
|
||||
WebService().ClearMfcTriggered(m_ClientID);
|
||||
// m_bIsWebSession remains false (default)
|
||||
} else {
|
||||
// Check if this is a Web session
|
||||
isWebSession = WebService().IsWebTriggered(m_ClientID) && WebService().GetHideWebSessions();
|
||||
|
||||
// Hide window if this session was triggered by web client
|
||||
if (WebService().IsWebTriggered(m_ClientID) && WebService().GetHideWebSessions()) {
|
||||
m_bHide = true;
|
||||
ShowWindow(SW_HIDE);
|
||||
// Only register screen context for Web sessions
|
||||
// MFC dialogs handle input directly via m_ContextObject, don't need WebService registry
|
||||
// This prevents MFC close from deleting Web's context (they share same device_id key)
|
||||
if (isWebSession) {
|
||||
WebService().RegisterScreenContext(m_ClientID, m_ContextObject);
|
||||
m_bHide = true;
|
||||
m_bIsWebSession = true;
|
||||
ShowWindow(SW_HIDE);
|
||||
}
|
||||
}
|
||||
|
||||
Mprintf("[ScreenSpy] Dialog created for device %llu, isMfcSession=%d, isWebSession=%d\n",
|
||||
m_ClientID, isMfcSession ? 1 : 0, isWebSession ? 1 : 0);
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
|
||||
VOID CScreenSpyDlg::OnClose()
|
||||
{
|
||||
// 注销屏幕上下文(Web 端控制)
|
||||
WebService().UnregisterScreenContext(m_ClientID);
|
||||
// Only unregister if this is a Web session (we only registered for Web sessions)
|
||||
if (m_bIsWebSession) {
|
||||
WebService().UnregisterScreenContext(m_ClientID);
|
||||
}
|
||||
|
||||
m_bIsClosed = true;
|
||||
m_bIsCtrl = FALSE;
|
||||
@@ -964,18 +985,11 @@ VOID CScreenSpyDlg::OnReceiveComplete()
|
||||
PrepareDrawing(m_BitmapInfor_Full);
|
||||
// 分辨率切换完成,允许解码
|
||||
m_bResolutionChanging = false;
|
||||
// Notify web clients of resolution change
|
||||
if (WebService().IsRunning()) {
|
||||
// Notify web clients of resolution change (only for Web session dialogs)
|
||||
if (m_bIsWebSession && WebService().IsRunning()) {
|
||||
int width = m_BitmapInfor_Full->bmiHeader.biWidth;
|
||||
int height = abs(m_BitmapInfor_Full->bmiHeader.biHeight);
|
||||
WebService().NotifyResolutionChange(m_ClientID, width, height);
|
||||
|
||||
// Hide window if this session was triggered by web client (and hiding is enabled)
|
||||
if (WebService().IsWebTriggered(m_ClientID) && WebService().GetHideWebSessions()) {
|
||||
m_bHide = true;
|
||||
ShowWindow(SW_HIDE);
|
||||
Mprintf("[ScreenSpyDlg] Web-triggered session, hiding window for device %llu\n", m_ClientID);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -1266,8 +1280,8 @@ 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()) {
|
||||
// 通知 Web 客户端光标变化 (只有 Web 会话的对话框才广播)
|
||||
if (m_bIsWebSession && WebService().IsRunning()) {
|
||||
WebService().BroadcastCursor(m_ClientID, m_bCursorIndex);
|
||||
}
|
||||
if (m_bIsCtrl && !m_bIsTraceCursor) {//替换指定窗口所属类的WNDCLASSEX结构
|
||||
@@ -1317,8 +1331,8 @@ VOID CScreenSpyDlg::DrawNextScreenDiff(bool keyFrame)
|
||||
bChange = TRUE;
|
||||
}
|
||||
}
|
||||
// Broadcast H264 keyframe to web clients
|
||||
if (NextScreenLength > 0 && WebService().IsRunning()) {
|
||||
// Broadcast H264 keyframe to web clients (only for Web session dialogs)
|
||||
if (m_bIsWebSession && 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
|
||||
@@ -1376,9 +1390,9 @@ VOID CScreenSpyDlg::DrawNextScreenDiff(bool keyFrame)
|
||||
bChange = TRUE;
|
||||
}
|
||||
}
|
||||
// Broadcast H264 frame to web clients
|
||||
// Broadcast H264 frame to web clients (only for Web session dialogs)
|
||||
// Format: [DeviceID:4][FrameType:1][DataLen:4][H264Data:N]
|
||||
if (NextScreenLength > 0 && WebService().IsRunning()) {
|
||||
if (m_bIsWebSession && NextScreenLength > 0 && WebService().IsRunning()) {
|
||||
// Detect H264 keyframe by checking NAL unit type
|
||||
// NAL type 5 = IDR slice (keyframe), NAL type 7 = SPS, NAL type 8 = PPS
|
||||
bool isKeyFrame = false;
|
||||
@@ -1463,8 +1477,8 @@ VOID CScreenSpyDlg::DrawScrollFrame()
|
||||
m_bCursorIndex = m_ContextObject->InDeCompressedBuffer.GetBuffer(2 + sizeof(POINT))[0];
|
||||
if (bOldCursorIndex != m_bCursorIndex) {
|
||||
bChange = TRUE;
|
||||
// 通知 Web 客户端光标变化
|
||||
if (WebService().IsRunning()) {
|
||||
// 通知 Web 客户端光标变化 (只有 Web 会话的对话框才广播)
|
||||
if (m_bIsWebSession && WebService().IsRunning()) {
|
||||
WebService().BroadcastCursor(m_ClientID, m_bCursorIndex);
|
||||
}
|
||||
}
|
||||
@@ -2305,8 +2319,8 @@ BOOL CScreenSpyDlg::PreTranslateMessage(MSG* pMsg)
|
||||
MSG wheelMsg = *pMsg;
|
||||
wheelMsg.lParam = MAKELPARAM(pt.x, pt.y);
|
||||
SendScaledMouseMessage(&wheelMsg, true);
|
||||
return TRUE; // 已处理,阻止继续分发到 OnMouseWheel
|
||||
}
|
||||
break;
|
||||
case WM_KEYDOWN:
|
||||
case WM_KEYUP:
|
||||
case WM_SYSKEYDOWN:
|
||||
@@ -2682,7 +2696,20 @@ void CScreenSpyDlg::OnLButtonUp(UINT nFlags, CPoint point)
|
||||
|
||||
BOOL CScreenSpyDlg::OnMouseWheel(UINT nFlags, short zDelta, CPoint pt)
|
||||
{
|
||||
return __super::OnMouseWheel(nFlags, zDelta, pt);
|
||||
// Convert screen coordinates to client coordinates
|
||||
ScreenToClient(&pt);
|
||||
|
||||
// Build MSG structure for SendScaledMouseMessage
|
||||
MSG msg = {};
|
||||
msg.hwnd = m_hWnd;
|
||||
msg.message = WM_MOUSEWHEEL;
|
||||
msg.wParam = MAKEWPARAM(nFlags, zDelta);
|
||||
msg.lParam = MAKELPARAM(pt.x, pt.y);
|
||||
msg.time = GetTickCount();
|
||||
msg.pt = { pt.x, pt.y };
|
||||
|
||||
SendScaledMouseMessage(&msg, true);
|
||||
return TRUE; // Message handled, don't pass to parent
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
#include <imm.h>
|
||||
#include <map>
|
||||
#include <atomic>
|
||||
#include "IOCPServer.h"
|
||||
#include "..\..\client\CursorInfo.h"
|
||||
#include "VideoDlg.h"
|
||||
@@ -153,6 +154,10 @@ public:
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
// Check if this dialog was created by Web request (shared by Web users)
|
||||
bool IsWebSession() const { return m_bIsWebSession.load(); }
|
||||
void SetWebSession(bool isWeb) { m_bIsWebSession.store(isWeb); }
|
||||
|
||||
VOID SendNext(void);
|
||||
VOID OnReceiveComplete();
|
||||
HDC m_hFullDC;
|
||||
@@ -186,6 +191,7 @@ public:
|
||||
int m_FrameID;
|
||||
HIMC m_hOldIMC = NULL; // 保存原始 IME 上下文,控制模式切换时使用
|
||||
bool m_bHide = false;
|
||||
std::atomic<bool> m_bIsWebSession{false}; // True if this dialog was created by Web request (atomic for thread safety)
|
||||
std::string m_strSaveNotice; // 截图保存路径提示
|
||||
ULONGLONG m_nSaveNoticeTime = 0; // 截图提示开始时间
|
||||
BOOL m_bUsingFRP = FALSE;
|
||||
|
||||
@@ -997,10 +997,14 @@ inline std::string GetWebPageHTML() {
|
||||
<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">
|
||||
<select id="new-role" onchange="onRoleChange()">
|
||||
<option value="viewer">Viewer (read-only)</option>
|
||||
<option value="admin">Admin (full access)</option>
|
||||
</select>
|
||||
<div class="groups-section" id="groups-section">
|
||||
<label style="font-size:13px;color:#aaa;display:block;margin:8px 0 4px;">Allowed Groups:</label>
|
||||
<div id="groups-checkboxes" style="max-height:120px;overflow-y:auto;background:rgba(0,0,0,0.2);border-radius:6px;padding:6px 8px;"></div>
|
||||
</div>
|
||||
<button onclick="createUser()">Create User</button>
|
||||
</div>
|
||||
<div class="user-list">
|
||||
@@ -1286,6 +1290,11 @@ inline std::string GetWebPageHTML() {
|
||||
renderUsersList(msg.users);
|
||||
}
|
||||
break;
|
||||
case 'groups':
|
||||
if (msg.ok) {
|
||||
renderGroupsCheckboxes(msg.groups);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
)HTML";
|
||||
@@ -1661,7 +1670,35 @@ inline std::string GetWebPageHTML() {
|
||||
function openUsersModal() {
|
||||
document.getElementById('users-modal').classList.add('active');
|
||||
document.getElementById('user-msg').innerHTML = '';
|
||||
document.getElementById('new-role').value = 'viewer'; // Reset to default
|
||||
onRoleChange(); // Update groups section visibility
|
||||
listUsers();
|
||||
getGroups();
|
||||
}
|
||||
|
||||
function getGroups() {
|
||||
if (ws && ws.readyState === WebSocket.OPEN && token) {
|
||||
ws.send(JSON.stringify({ cmd: 'get_groups', token }));
|
||||
}
|
||||
}
|
||||
|
||||
function renderGroupsCheckboxes(groups) {
|
||||
const container = document.getElementById('groups-checkboxes');
|
||||
if (!groups || groups.length === 0) {
|
||||
container.innerHTML = '<span style="color:#666;font-size:12px;">No groups available</span>';
|
||||
return;
|
||||
}
|
||||
container.innerHTML = groups.map(g =>
|
||||
'<label style="display:flex;align-items:center;padding:3px 0;cursor:pointer;white-space:nowrap;">' +
|
||||
'<input type="checkbox" value="' + escapeHtml(g) + '" style="margin:0 6px 0 0;flex-shrink:0;width:14px;height:14px;">' +
|
||||
escapeHtml(g) + '</label>'
|
||||
).join('');
|
||||
}
|
||||
|
||||
function onRoleChange() {
|
||||
const role = document.getElementById('new-role').value;
|
||||
const groupsSection = document.getElementById('groups-section');
|
||||
groupsSection.style.display = (role === 'admin') ? 'none' : 'block';
|
||||
}
|
||||
|
||||
function closeUsersModal() {
|
||||
@@ -1685,8 +1722,12 @@ inline std::string GetWebPageHTML() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Collect selected groups
|
||||
const checkboxes = document.querySelectorAll('#groups-checkboxes input[type="checkbox"]:checked');
|
||||
const allowed_groups = Array.from(checkboxes).map(cb => cb.value);
|
||||
|
||||
if (ws && ws.readyState === WebSocket.OPEN && token) {
|
||||
ws.send(JSON.stringify({ cmd: 'create_user', token, username, password, role }));
|
||||
ws.send(JSON.stringify({ cmd: 'create_user', token, username, password, role, allowed_groups }));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1712,10 +1753,14 @@ inline std::string GetWebPageHTML() {
|
||||
container.innerHTML = users.map(u => {
|
||||
const isAdmin = u.role === 'admin';
|
||||
const canDelete = u.username !== 'admin'; // Cannot delete built-in admin
|
||||
const groups = u.allowed_groups || [];
|
||||
const groupsText = u.username === 'admin' ? '(all)' :
|
||||
(groups.length > 0 ? groups.join(', ') : '(none)');
|
||||
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 class="groups" style="font-size:11px;color:#888;margin-top:2px;">Groups: ' + escapeHtml(groupsText) + '</div>' +
|
||||
'</div>' +
|
||||
(canDelete ? '<button class="delete-btn" onclick="deleteUser(\'' + escapeHtml(u.username) + '\')">Delete</button>' : '') +
|
||||
'</div>';
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <shlobj.h>
|
||||
#include <set>
|
||||
|
||||
// Algorithm constants (same as ScreenSpyDlg.cpp)
|
||||
#define ALGORITHM_H264 2
|
||||
@@ -363,6 +364,8 @@ void CWebService::ServerThread(int port) {
|
||||
HandleDeleteUser(ws_ptr, msg);
|
||||
} else if (cmd == "list_users") {
|
||||
HandleListUsers(ws_ptr, token);
|
||||
} else if (cmd == "get_groups") {
|
||||
HandleGetGroups(ws_ptr, token);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -566,7 +569,7 @@ void CWebService::HandleGetDevices(void* ws_ptr, const std::string& token) {
|
||||
return;
|
||||
}
|
||||
|
||||
SendText(ws_ptr, BuildDeviceListJson());
|
||||
SendText(ws_ptr, BuildDeviceListJson(username));
|
||||
}
|
||||
|
||||
void CWebService::HandleConnect(void* ws_ptr, const std::string& token, uint64_t device_id) {
|
||||
@@ -588,6 +591,32 @@ void CWebService::HandleConnect(void* ws_ptr, const std::string& token, uint64_t
|
||||
return;
|
||||
}
|
||||
|
||||
// Check group permission (admin can access all devices)
|
||||
if (username != "admin") {
|
||||
std::string deviceGroup = ctx->GetGroupName();
|
||||
if (deviceGroup.empty()) deviceGroup = "default";
|
||||
|
||||
bool hasAccess = false;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_UsersMutex);
|
||||
for (const auto& u : m_Users) {
|
||||
if (u.username == username) {
|
||||
for (const auto& g : u.allowed_groups) {
|
||||
if (g == deviceGroup) {
|
||||
hasAccess = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!hasAccess) {
|
||||
SendText(ws_ptr, BuildJsonResponse("connect_result", false, "Permission denied"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Check max clients per device
|
||||
int current_count = GetWebClientCount(device_id);
|
||||
if (current_count >= m_nMaxClientsPerDevice) {
|
||||
@@ -954,12 +983,23 @@ void CWebService::HandleCreateUser(void* ws_ptr, const std::string& msg) {
|
||||
std::string newPassword = root.get("password", "").asString();
|
||||
std::string newRole = root.get("role", "viewer").asString();
|
||||
|
||||
// Parse allowed_groups array
|
||||
std::vector<std::string> allowedGroups;
|
||||
const Json::Value& groups = root["allowed_groups"];
|
||||
if (groups.isArray()) {
|
||||
for (const auto& g : groups) {
|
||||
if (g.isString() && !g.asString().empty()) {
|
||||
allowedGroups.push_back(g.asString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (newUsername.empty() || newPassword.empty()) {
|
||||
SendText(ws_ptr, BuildJsonResponse("create_user_result", false, "Username and password required"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (CreateUser(newUsername, newPassword, newRole)) {
|
||||
if (CreateUser(newUsername, newPassword, newRole, allowedGroups)) {
|
||||
SendText(ws_ptr, BuildJsonResponse("create_user_result", true));
|
||||
} else {
|
||||
SendText(ws_ptr, BuildJsonResponse("create_user_result", false, "Failed to create user (may already exist)"));
|
||||
@@ -1009,18 +1049,27 @@ void CWebService::HandleListUsers(void* ws_ptr, const std::string& token) {
|
||||
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);
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_UsersMutex);
|
||||
for (const auto& u : m_Users) {
|
||||
Json::Value user;
|
||||
user["username"] = u.username;
|
||||
user["role"] = u.role;
|
||||
|
||||
// Include allowed_groups
|
||||
Json::Value groups(Json::arrayValue);
|
||||
for (const auto& g : u.allowed_groups) {
|
||||
groups.append(g);
|
||||
}
|
||||
user["allowed_groups"] = groups;
|
||||
|
||||
usersArray.append(user);
|
||||
}
|
||||
}
|
||||
res["users"] = usersArray;
|
||||
|
||||
@@ -1030,6 +1079,48 @@ void CWebService::HandleListUsers(void* ws_ptr, const std::string& token) {
|
||||
SendText(ws_ptr, json);
|
||||
}
|
||||
|
||||
void CWebService::HandleGetGroups(void* ws_ptr, const std::string& token) {
|
||||
std::string username, role;
|
||||
if (!ValidateToken(token, username, role)) {
|
||||
SendText(ws_ptr, BuildJsonResponse("groups", false, "Invalid token"));
|
||||
return;
|
||||
}
|
||||
|
||||
// Only admin can get groups list (for user management)
|
||||
if (role != "admin") {
|
||||
SendText(ws_ptr, BuildJsonResponse("groups", false, "Permission denied"));
|
||||
return;
|
||||
}
|
||||
|
||||
// Collect all unique groups from online devices
|
||||
std::set<std::string> groups;
|
||||
groups.insert("default"); // Always include default group
|
||||
|
||||
if (m_pParentDlg) {
|
||||
EnterCriticalSection(&m_pParentDlg->m_cs);
|
||||
for (context* ctx : m_pParentDlg->m_HostList) {
|
||||
if (!ctx || !ctx->IsLogin()) continue;
|
||||
std::string g = ctx->GetGroupName();
|
||||
groups.insert(g.empty() ? "default" : g);
|
||||
}
|
||||
LeaveCriticalSection(&m_pParentDlg->m_cs);
|
||||
}
|
||||
|
||||
// Build response
|
||||
Json::Value res;
|
||||
res["cmd"] = "groups";
|
||||
res["ok"] = true;
|
||||
res["groups"] = Json::Value(Json::arrayValue);
|
||||
for (const auto& g : groups) {
|
||||
res["groups"].append(g);
|
||||
}
|
||||
|
||||
Json::StreamWriterBuilder builder;
|
||||
builder["indentation"] = "";
|
||||
std::string json = Json::writeString(builder, res);
|
||||
SendText(ws_ptr, json);
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// Token Management (delegated to WebServiceAuth module)
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
@@ -1170,6 +1261,14 @@ void CWebService::LoadUsers() {
|
||||
user.salt = u.get("salt", "").asString();
|
||||
user.role = u.get("role", "viewer").asString();
|
||||
|
||||
// Load allowed_groups
|
||||
const Json::Value& groups = u["allowed_groups"];
|
||||
if (groups.isArray()) {
|
||||
for (const auto& g : groups) {
|
||||
user.allowed_groups.push_back(g.asString());
|
||||
}
|
||||
}
|
||||
|
||||
if (!user.password_hash.empty()) {
|
||||
m_Users.push_back(user);
|
||||
loaded++;
|
||||
@@ -1197,6 +1296,14 @@ void CWebService::SaveUsers() {
|
||||
user["password_hash"] = u.password_hash;
|
||||
user["salt"] = u.salt;
|
||||
user["role"] = u.role;
|
||||
|
||||
// Save allowed_groups
|
||||
Json::Value groups(Json::arrayValue);
|
||||
for (const auto& g : u.allowed_groups) {
|
||||
groups.append(g);
|
||||
}
|
||||
user["allowed_groups"] = groups;
|
||||
|
||||
users.append(user);
|
||||
}
|
||||
|
||||
@@ -1217,7 +1324,8 @@ void CWebService::SaveUsers() {
|
||||
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) {
|
||||
bool CWebService::CreateUser(const std::string& username, const std::string& password, const std::string& role,
|
||||
const std::vector<std::string>& allowed_groups) {
|
||||
if (username.empty() || password.empty()) return false;
|
||||
if (username == "admin") return false; // Cannot create user named "admin"
|
||||
if (role != "admin" && role != "viewer") return false;
|
||||
@@ -1236,9 +1344,11 @@ bool CWebService::CreateUser(const std::string& username, const std::string& pas
|
||||
user.salt = GenerateSalt();
|
||||
user.password_hash = WSAuth::ComputeSHA256(password + user.salt);
|
||||
user.role = role;
|
||||
user.allowed_groups = allowed_groups;
|
||||
|
||||
m_Users.push_back(user);
|
||||
Mprintf("[WebService] Created user: %s (role: %s)\n", username.c_str(), role.c_str());
|
||||
Mprintf("[WebService] Created user: %s (role: %s, groups: %d)\n",
|
||||
username.c_str(), role.c_str(), (int)allowed_groups.size());
|
||||
}
|
||||
|
||||
// Save to file (outside lock scope since SaveUsers acquires its own lock)
|
||||
@@ -1295,17 +1405,47 @@ std::string CWebService::BuildJsonResponse(const std::string& cmd, bool ok, cons
|
||||
return Json::writeString(builder, res);
|
||||
}
|
||||
|
||||
std::string CWebService::BuildDeviceListJson() {
|
||||
std::string CWebService::BuildDeviceListJson(const std::string& username) {
|
||||
Json::Value res;
|
||||
res["cmd"] = "device_list";
|
||||
res["devices"] = Json::Value(Json::arrayValue);
|
||||
|
||||
// Get user's allowed groups for filtering (skip for admin or empty username)
|
||||
std::vector<std::string> allowedGroups;
|
||||
bool filterByGroup = false;
|
||||
if (!username.empty() && username != "admin") {
|
||||
std::lock_guard<std::mutex> lock(m_UsersMutex);
|
||||
for (const auto& u : m_Users) {
|
||||
if (u.username == username) {
|
||||
allowedGroups = u.allowed_groups;
|
||||
filterByGroup = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (m_pParentDlg) {
|
||||
// Access device list with lock
|
||||
EnterCriticalSection(&m_pParentDlg->m_cs);
|
||||
for (context* ctx : m_pParentDlg->m_HostList) {
|
||||
if (!ctx || !ctx->IsLogin()) continue;
|
||||
|
||||
// Get device group (empty = "default")
|
||||
std::string deviceGroup = ctx->GetGroupName();
|
||||
if (deviceGroup.empty()) deviceGroup = "default";
|
||||
|
||||
// Filter by allowed groups if user is not admin
|
||||
if (filterByGroup) {
|
||||
bool allowed = false;
|
||||
for (const auto& g : allowedGroups) {
|
||||
if (g == deviceGroup) {
|
||||
allowed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!allowed) continue; // Skip device not in allowed groups
|
||||
}
|
||||
|
||||
Json::Value device;
|
||||
// Use string for ID to avoid JavaScript number precision loss
|
||||
device["id"] = std::to_string(ctx->GetClientID());
|
||||
@@ -1332,6 +1472,9 @@ std::string CWebService::BuildDeviceListJson() {
|
||||
device["activeWindow"] = AnsiToUtf8(activeWindow);
|
||||
device["online"] = true;
|
||||
|
||||
// Add device group to response
|
||||
device["group"] = deviceGroup;
|
||||
|
||||
// Get screen info from client's reported resolution
|
||||
// Format: "n:MxN" where n=monitor count, M=width, N=height
|
||||
CString resolution = ctx->GetAdditionalData(RES_RESOLUTION);
|
||||
@@ -1509,9 +1652,13 @@ bool CWebService::StartRemoteDesktop(uint64_t device_id) {
|
||||
context* ctx = m_pParentDlg->FindHost(device_id);
|
||||
if (!ctx) return false;
|
||||
|
||||
// Close any existing remote desktop for this device first
|
||||
// This prevents duplicate dialogs when user reconnects quickly
|
||||
m_pParentDlg->CloseRemoteDesktopByClientID(device_id);
|
||||
// Check if there's already a Web session for this device
|
||||
// Only reuse if Web has already triggered AND a Web dialog exists
|
||||
// This ensures MFC and Web have independent dialogs
|
||||
if (IsWebTriggered(device_id) && HasActiveSession(device_id)) {
|
||||
Mprintf("[WebService] Reusing existing Web session for device %llu\n", device_id);
|
||||
return true; // Web session exists, new web user joins watching
|
||||
}
|
||||
|
||||
// Mark as web-triggered (dialog should be hidden)
|
||||
{
|
||||
@@ -1520,7 +1667,8 @@ bool CWebService::StartRemoteDesktop(uint64_t device_id) {
|
||||
}
|
||||
|
||||
// Send COMMAND_SCREEN_SPY with H264 algorithm
|
||||
// Format: [COMMAND_SCREEN_SPY:1][DXGI:1][Algorithm:1][MultiScreen:1]
|
||||
// If client is already capturing (MFC opened first), it will re-send TOKEN_BITMAPINFO
|
||||
// This creates a new hidden Web dialog while MFC dialog remains visible
|
||||
BYTE bToken[32] = { 0 };
|
||||
bToken[0] = COMMAND_SCREEN_SPY;
|
||||
bToken[1] = 0; // DXGI mode: 0=GDI
|
||||
@@ -1544,10 +1692,11 @@ void CWebService::StopRemoteDesktop(uint64_t device_id) {
|
||||
}
|
||||
}
|
||||
|
||||
// If no more web clients watching, close the remote desktop
|
||||
// If no more web clients watching, close only the Web session dialog
|
||||
// MFC dialogs remain open
|
||||
if (watchingCount == 0) {
|
||||
ClearWebTriggered(device_id);
|
||||
m_pParentDlg->CloseRemoteDesktopByClientID(device_id);
|
||||
m_pParentDlg->CloseWebRemoteDesktopByClientID(device_id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1563,10 +1712,13 @@ void CWebService::RegisterScreenContext(uint64_t device_id, CONTEXT_OBJECT* ctx)
|
||||
}
|
||||
|
||||
void CWebService::UnregisterScreenContext(uint64_t device_id) {
|
||||
if (!m_bRunning) return;
|
||||
// Always clean up, even if WebService is stopping
|
||||
// This prevents stale pointers in m_ScreenContexts
|
||||
std::lock_guard<std::mutex> lock(m_ScreenContextsMutex);
|
||||
m_ScreenContexts.erase(device_id);
|
||||
Mprintf("[WebService] Unregistered screen context for device %llu\n", device_id);
|
||||
if (m_bRunning) {
|
||||
Mprintf("[WebService] Unregistered screen context for device %llu\n", device_id);
|
||||
}
|
||||
}
|
||||
|
||||
CONTEXT_OBJECT* CWebService::GetScreenContext(uint64_t device_id) {
|
||||
@@ -1666,6 +1818,26 @@ void CWebService::ClearWebTriggered(uint64_t device_id) {
|
||||
m_WebTriggeredDevices.erase(device_id);
|
||||
}
|
||||
|
||||
void CWebService::SetMfcTriggered(uint64_t device_id) {
|
||||
std::lock_guard<std::mutex> lock(m_MfcTriggeredMutex);
|
||||
m_MfcTriggeredDevices.insert(device_id);
|
||||
}
|
||||
|
||||
bool CWebService::IsMfcTriggered(uint64_t device_id) {
|
||||
std::lock_guard<std::mutex> lock(m_MfcTriggeredMutex);
|
||||
return m_MfcTriggeredDevices.find(device_id) != m_MfcTriggeredDevices.end();
|
||||
}
|
||||
|
||||
void CWebService::ClearMfcTriggered(uint64_t device_id) {
|
||||
std::lock_guard<std::mutex> lock(m_MfcTriggeredMutex);
|
||||
m_MfcTriggeredDevices.erase(device_id);
|
||||
}
|
||||
|
||||
bool CWebService::HasActiveSession(uint64_t device_id) {
|
||||
std::lock_guard<std::mutex> lock(m_ScreenContextsMutex);
|
||||
return m_ScreenContexts.find(device_id) != m_ScreenContexts.end();
|
||||
}
|
||||
|
||||
void CWebService::NotifyDeviceUpdate(uint64_t device_id, const std::string& rtt, const std::string& activeWindow) {
|
||||
if (!m_bRunning || m_bStopping) return;
|
||||
|
||||
|
||||
@@ -43,6 +43,7 @@ struct WebUser {
|
||||
std::string password_hash; // SHA256(password + salt)
|
||||
std::string salt;
|
||||
std::string role; // "admin" | "viewer"
|
||||
std::vector<std::string> allowed_groups; // Groups this user can view (empty = no access, admin = all)
|
||||
};
|
||||
|
||||
// Device info for web clients
|
||||
@@ -79,7 +80,8 @@ public:
|
||||
void SetAdminPassword(const std::string& password);
|
||||
|
||||
// User management
|
||||
bool CreateUser(const std::string& username, const std::string& password, const std::string& role);
|
||||
bool CreateUser(const std::string& username, const std::string& password, const std::string& role,
|
||||
const std::vector<std::string>& allowed_groups = {});
|
||||
bool DeleteUser(const std::string& username);
|
||||
std::vector<std::pair<std::string, std::string>> ListUsers(); // Returns [(username, role), ...]
|
||||
|
||||
@@ -144,7 +146,7 @@ private:
|
||||
|
||||
// JSON helpers
|
||||
std::string BuildJsonResponse(const std::string& cmd, bool ok, const std::string& msg = "");
|
||||
std::string BuildDeviceListJson();
|
||||
std::string BuildDeviceListJson(const std::string& username = "");
|
||||
|
||||
// Password verification
|
||||
bool VerifyPassword(const std::string& input, const WebUser& user);
|
||||
@@ -157,6 +159,7 @@ private:
|
||||
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);
|
||||
void HandleGetGroups(void* ws_ptr, const std::string& token);
|
||||
|
||||
// Send to WebSocket
|
||||
void SendText(void* ws_ptr, const std::string& text);
|
||||
@@ -224,6 +227,14 @@ public:
|
||||
bool IsWebTriggered(uint64_t device_id);
|
||||
void ClearWebTriggered(uint64_t device_id);
|
||||
|
||||
// MFC trigger management - MFC dialogs should always be visible
|
||||
void SetMfcTriggered(uint64_t device_id);
|
||||
bool IsMfcTriggered(uint64_t device_id);
|
||||
void ClearMfcTriggered(uint64_t device_id);
|
||||
|
||||
// Check if a remote desktop session already exists for device
|
||||
bool HasActiveSession(uint64_t device_id);
|
||||
|
||||
// Config accessors
|
||||
void SetHideWebSessions(bool hide) { m_bHideWebSessions = hide; }
|
||||
bool GetHideWebSessions() const { return m_bHideWebSessions; }
|
||||
@@ -240,6 +251,10 @@ private:
|
||||
// Screen context registry: device_id -> ScreenManager's CONTEXT_OBJECT
|
||||
std::map<uint64_t, CONTEXT_OBJECT*> m_ScreenContexts;
|
||||
std::mutex m_ScreenContextsMutex;
|
||||
|
||||
// MFC triggered devices: dialogs created by MFC should always be visible
|
||||
std::set<uint64_t> m_MfcTriggeredDevices;
|
||||
std::mutex m_MfcTriggeredMutex;
|
||||
};
|
||||
|
||||
// Global accessor
|
||||
|
||||
Reference in New Issue
Block a user