Files
SimpleRemoter/server/2015Remote/WebService.cpp

2186 lines
76 KiB
C++
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#include "stdafx.h"
#include "2015Remote.h"
#include "resource.h" // IDR_WEB_XTERM_* (xterm.js/css 静态资源 ID)
#include "WebService.h"
#include "WebServiceAuth.h"
#include "2015RemoteDlg.h"
#include "Server.h" // For CONTEXT_OBJECT
#include "jsoncpp/json.h"
#include "WebPage.h"
#include "SimpleWebSocket.h"
#include "common/commands.h"
#include <filesystem>
#include <fstream>
#include <shlobj.h>
#include <set>
// Algorithm constants (same as ScreenSpyDlg.cpp)
#define ALGORITHM_H264 2
#pragma comment(lib, "ws2_32.lib")
// Load a Win32 BINARY resource by ID as std::string (raw bytes).
// Returns empty string on failure. The std::string is OK to hold binary data
// (we only treat it as bytes; size is from .length()).
static std::string LoadBinaryResourceAsString(int resourceId) {
HRSRC hRes = FindResourceA(NULL, MAKEINTRESOURCEA(resourceId), "BINARY");
if (!hRes) return {};
DWORD size = SizeofResource(NULL, hRes);
if (!size) return {};
HGLOBAL hData = LoadResource(NULL, hRes);
if (!hData) return {};
LPVOID p = LockResource(hData);
if (!p) return {};
return std::string(static_cast<const char*>(p), size);
}
// Challenge-response nonce storage (prevents replay attacks)
static std::map<void*, std::string> s_ClientNonces;
static std::mutex s_NonceMutex;
static std::atomic<bool> s_bShuttingDown{false}; // Prevents access during static destruction
// Generate random salt (16 hex chars) - thread-safe
static std::string GenerateSalt() {
static std::random_device rd;
static std::mt19937_64 gen(rd());
std::uniform_int_distribution<uint64_t> dis;
char buf[17];
snprintf(buf, sizeof(buf), "%016llX", dis(gen));
return std::string(buf);
}
// Generate random nonce (32 hex chars) - thread-safe
static std::string GenerateNonce() {
if (s_bShuttingDown) return "";
static std::random_device rd;
static std::mt19937_64 gen(rd());
std::uniform_int_distribution<uint64_t> dis;
char buf[33];
{
std::lock_guard<std::mutex> lock(s_NonceMutex);
sprintf_s(buf, "%016llx%016llx", dis(gen), dis(gen));
}
return buf;
}
// Store nonce for client
static void StoreNonce(void* ws_ptr, const std::string& nonce) {
if (s_bShuttingDown) return;
std::lock_guard<std::mutex> lock(s_NonceMutex);
s_ClientNonces[ws_ptr] = nonce;
}
// Get and clear nonce for client (one-time use)
static std::string ConsumeNonce(void* ws_ptr) {
if (s_bShuttingDown) return "";
std::lock_guard<std::mutex> lock(s_NonceMutex);
auto it = s_ClientNonces.find(ws_ptr);
if (it == s_ClientNonces.end()) return "";
std::string nonce = it->second;
s_ClientNonces.erase(it);
return nonce;
}
// Clear nonce when client disconnects
static void ClearNonce(void* ws_ptr) {
if (s_bShuttingDown) return;
std::lock_guard<std::mutex> lock(s_NonceMutex);
s_ClientNonces.erase(ws_ptr);
}
// Helper: Convert ANSI (GBK) string to UTF-8
static std::string AnsiToUtf8(const CString& str) {
if (str.IsEmpty()) return "";
#ifdef _UNICODE
// Unicode build: CString is already UTF-16, convert to UTF-8
int len = WideCharToMultiByte(CP_UTF8, 0, str, -1, NULL, 0, NULL, NULL);
if (len <= 0) return "";
std::string result(len - 1, '\0');
WideCharToMultiByte(CP_UTF8, 0, str, -1, &result[0], len, NULL, NULL);
return result;
#else
// MBCS build: CString is ANSI (GBK), need to convert to UTF-16 first, then to UTF-8
int wlen = MultiByteToWideChar(CP_ACP, 0, str, -1, NULL, 0);
if (wlen <= 0) return "";
std::wstring wstr(wlen - 1, L'\0');
MultiByteToWideChar(CP_ACP, 0, str, -1, &wstr[0], wlen);
int u8len = WideCharToMultiByte(CP_UTF8, 0, wstr.c_str(), -1, NULL, 0, NULL, NULL);
if (u8len <= 0) return "";
std::string result(u8len - 1, '\0');
WideCharToMultiByte(CP_UTF8, 0, wstr.c_str(), -1, &result[0], u8len, NULL, NULL);
return result;
#endif
}
//////////////////////////////////////////////////////////////////////////
// CWebService Implementation
//////////////////////////////////////////////////////////////////////////
CWebService& CWebService::Instance() {
static CWebService instance;
return instance;
}
CWebService::CWebService()
: m_bRunning(false)
, m_bStopping(false)
, m_pServer(nullptr)
, m_pParentDlg(nullptr)
, m_nMaxClientsPerDevice(10)
, m_nTokenExpireSeconds(86400) // 24 hours
, m_bHideWebSessions(true) // Hide web-triggered dialogs by default
{
// Secret key will be initialized in Start() via WSAuth::Init()
// Admin user will be configured via SetAdminPassword()
// Initialize payloads directory
char exe_path[MAX_PATH];
GetModuleFileNameA(NULL, exe_path, MAX_PATH);
std::filesystem::path exeDir = std::filesystem::path(exe_path).parent_path();
m_PayloadsDir = (exeDir / "Payloads").string();
std::error_code ec;
std::filesystem::create_directories(m_PayloadsDir, ec);
// Initialize config directory (same as YAMA.db location)
char appdata_path[MAX_PATH];
if (SUCCEEDED(SHGetFolderPathA(NULL, CSIDL_APPDATA, NULL, 0, appdata_path))) {
m_ConfigDir = std::string(appdata_path) + "\\" BRAND_DATA_FOLDER "\\";
} else {
m_ConfigDir = ".\\";
}
std::filesystem::create_directories(m_ConfigDir, ec);
}
void CWebService::SetAdminPassword(const std::string& password) {
std::lock_guard<std::mutex> lock(m_UsersMutex);
m_Users.clear();
// Admin user is built-in, always first
WebUser admin;
admin.username = "admin";
admin.salt = ""; // Not used with challenge-response auth
admin.password_hash = WSAuth::ComputeSHA256(password); // Simple SHA256
admin.role = "admin";
m_Users.push_back(admin);
Mprintf("[WebService] Admin password configured\n");
// Load additional users from file (non-admin users)
LoadUsers();
}
CWebService::~CWebService() {
Stop();
}
bool CWebService::Start(int port) {
if (m_bRunning) return true;
// Initialize authorization and get secret key
// Pass raw authorization string, verified internally by private library
WSAuthContext authCtx;
std::string authStr = THIS_CFG.GetStr("settings", "Authorization", "");
if (!WSAuth::Init(authCtx, authStr)) {
Mprintf("[WebService] Authorization check failed, service disabled\n");
return false;
}
m_SecretKey = authCtx.secretKey;
m_nTokenExpireSeconds = authCtx.tokenExpireSec;
m_nMaxClientsPerDevice = authCtx.maxClientsPerDevice;
m_bStopping = false;
m_ServerThread = std::thread(&CWebService::ServerThread, this, port);
// Wait for server to start
for (int i = 0; i < 50 && !m_bRunning; i++) {
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
// Start heartbeat thread
if (m_bRunning) {
m_HeartbeatThread = std::thread(&CWebService::HeartbeatThread, this);
}
return m_bRunning;
}
void CWebService::Stop() {
if (!m_bRunning) return;
// Set flags FIRST to prevent new operations from starting
m_bRunning = false;
m_bStopping = true;
s_bShuttingDown = true; // Prevent access to static variables
// Stop heartbeat thread first
if (m_HeartbeatThread.joinable()) {
m_HeartbeatThread.join();
}
if (m_ServerThread.joinable()) {
m_ServerThread.join();
}
// Clear all clients after server stopped
{
std::lock_guard<std::mutex> lock(m_ClientsMutex);
m_Clients.clear();
}
// Clear screen contexts to prevent dangling pointers
{
std::lock_guard<std::mutex> lock(m_ScreenContextsMutex);
m_ScreenContexts.clear();
}
// Clear device cache
{
std::lock_guard<std::mutex> lock(m_DeviceCacheMutex);
m_DeviceCache.clear();
}
m_pServer = nullptr;
m_pParentDlg = nullptr; // Clear to prevent access to destroyed dialog
}
bool CWebService::IsRunning() const {
return m_bRunning;
}
void CWebService::ServerThread(int port) {
ws::Server wsServer;
m_pServer = &wsServer;
// Serve static HTML page and file downloads
static std::string cachedHtml = GetWebPageHTML();
// Log loaded size; zero bytes means the RC BINARY resource is missing or
// server/web/index.html was not embedded — check 2015Remote.rc and rebuild.
Mprintf("[WebService] index.html loaded: %zu bytes\n", cachedHtml.size());
std::string payloadsDir = m_PayloadsDir; // Capture for lambda
// 静态资源缓存xterm.js / xterm.css / fit-addon。RC binary 资源加载一次缓存到内存,
// 避免每个浏览器请求都去 LockResource。Cache-Control 给浏览器侧缓存友好。
static std::string cachedXtermJs = LoadBinaryResourceAsString(IDR_WEB_XTERM_JS);
static std::string cachedXtermCss = LoadBinaryResourceAsString(IDR_WEB_XTERM_CSS);
static std::string cachedXtermFitJs = LoadBinaryResourceAsString(IDR_WEB_XTERM_FIT_JS);
auto buildStatic = [](const std::string& body, const std::string& mime) {
ws::HttpResponse r = ws::HttpResponse::OK(body, mime);
r.headers["Cache-Control"] = "public, max-age=86400"; // 1 day
return r;
};
wsServer.onHttp([payloadsDir, buildStatic](const std::string& path) -> ws::HttpResponse {
if (path == "/" || path == "/index.html") {
return ws::HttpResponse::OK(cachedHtml);
} else if (path == "/static/xterm.js") {
if (cachedXtermJs.empty()) return ws::HttpResponse::NotFound();
return buildStatic(cachedXtermJs, "application/javascript; charset=utf-8");
} else if (path == "/static/xterm.css") {
if (cachedXtermCss.empty()) return ws::HttpResponse::NotFound();
return buildStatic(cachedXtermCss, "text/css; charset=utf-8");
} else if (path == "/static/xterm-fit.js") {
if (cachedXtermFitJs.empty()) return ws::HttpResponse::NotFound();
return buildStatic(cachedXtermFitJs, "application/javascript; charset=utf-8");
} else if (path == "/health") {
return ws::HttpResponse::OK("{\"status\":\"ok\"}", "application/json");
} else if (path == "/manifest.json") {
// PWA manifest for iOS standalone app support
static const char* manifest = R"({
"name": "SimpleRemoter",
"short_name": "Remoter",
"start_url": "/",
"display": "standalone",
"orientation": "any",
"background_color": "#1a1a2e",
"theme_color": "#1a1a2e"
})";
return ws::HttpResponse::OK(manifest, "application/manifest+json");
} else if (path.rfind("/payloads/", 0) == 0) {
// File download: /payloads/filename
std::string filename = path.substr(10); // Remove "/payloads/"
// Security check: no path traversal or absolute paths
if (filename.empty() ||
filename.find("..") != std::string::npos || // Path traversal
filename[0] == '/' || filename[0] == '\\' || // Absolute path (Unix/Windows)
(filename.length() > 1 && filename[1] == ':')) // Windows drive letter (C:)
{
return ws::HttpResponse::Forbidden();
}
std::filesystem::path filepath = std::filesystem::path(payloadsDir) / filename;
std::error_code ec;
if (!std::filesystem::exists(filepath, ec) || !std::filesystem::is_regular_file(filepath, ec)) {
return ws::HttpResponse::NotFound();
}
return ws::HttpResponse::File(filepath.string(), filename);
}
return ws::HttpResponse::NotFound();
});
// WebSocket connect
wsServer.onConnect([this](std::shared_ptr<ws::Connection> conn) {
// Skip if server is stopping
if (m_bStopping) return;
void* ws_ptr = conn.get();
RegisterClient(ws_ptr, conn->clientIP());
// Generate and send challenge nonce
std::string nonce = GenerateNonce();
if (nonce.empty()) return; // Shutting down
StoreNonce(ws_ptr, nonce);
Json::Value challenge;
challenge["cmd"] = "challenge";
challenge["nonce"] = nonce;
Json::StreamWriterBuilder builder;
builder["indentation"] = "";
SendText(ws_ptr, Json::writeString(builder, challenge));
});
// WebSocket disconnect
wsServer.onDisconnect([this](std::shared_ptr<ws::Connection> conn) {
// Skip cleanup if server is stopping (destructor will clean up)
if (m_bStopping) return;
void* ws_ptr = conn.get();
ClearNonce(ws_ptr);
UnregisterClient(ws_ptr);
});
// WebSocket message
wsServer.onMessage([this](std::shared_ptr<ws::Connection> conn, const std::string& msg) {
// Skip if server is stopping
if (m_bStopping) return;
void* ws_ptr = conn.get();
// Update last activity time for heartbeat
{
std::lock_guard<std::mutex> lock(m_ClientsMutex);
auto it = m_Clients.find(ws_ptr);
if (it != m_Clients.end()) {
it->second.last_activity = (uint64_t)time(nullptr);
}
}
// Parse JSON signaling
Json::Value root;
Json::Reader reader;
if (reader.parse(msg, root)) {
std::string cmd = root.get("cmd", "").asString();
std::string token = root.get("token", "").asString();
if (cmd == "login") {
HandleLogin(ws_ptr, msg, conn->clientIP());
} else if (cmd == "get_devices") {
HandleGetDevices(ws_ptr, token);
} else if (cmd == "connect") {
// Support both string and number ID formats
std::string id_str = root.get("id", "").asString();
uint64_t device_id = id_str.empty() ? 0 : strtoull(id_str.c_str(), nullptr, 10);
HandleConnect(ws_ptr, token, device_id);
} else if (cmd == "disconnect") {
std::string disc_id_str = root.get("id", "").asString();
uint64_t disc_device_id = disc_id_str.empty() ? 0 : strtoull(disc_id_str.c_str(), nullptr, 10);
HandleDisconnect(ws_ptr, token, disc_device_id);
} else if (cmd == "ping") {
HandlePing(ws_ptr, token);
} else if (cmd == "mouse") {
HandleMouse(ws_ptr, msg);
} else if (cmd == "key") {
HandleKey(ws_ptr, msg);
} else if (cmd == "rdp_reset") {
HandleRdpReset(ws_ptr, token);
} else if (cmd == "get_salt") {
HandleGetSalt(ws_ptr, msg);
} else if (cmd == "create_user") {
HandleCreateUser(ws_ptr, msg);
} else if (cmd == "delete_user") {
HandleDeleteUser(ws_ptr, msg);
} else if (cmd == "list_users") {
HandleListUsers(ws_ptr, token);
} else if (cmd == "get_groups") {
HandleGetGroups(ws_ptr, token);
} else if (cmd == "term_open") {
HandleTermOpen(ws_ptr, msg);
} else if (cmd == "term_input") {
HandleTermInput(ws_ptr, msg);
} else if (cmd == "term_resize") {
HandleTermResize(ws_ptr, msg);
} else if (cmd == "term_close") {
HandleTermClose(ws_ptr, msg);
}
}
});
// Start listening
if (wsServer.start(port)) {
m_bRunning = true;
// Wait until stop is requested
while (!m_bStopping && wsServer.isRunning()) {
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
wsServer.stop();
}
m_bRunning = false;
}
void CWebService::HeartbeatThread() {
Mprintf("[WebService] Heartbeat thread started (interval=%ds, timeout=%ds)\n",
HEARTBEAT_INTERVAL_SEC, HEARTBEAT_TIMEOUT_SEC);
while (!m_bStopping) {
// Sleep in small increments to respond quickly to stop
for (int i = 0; i < HEARTBEAT_INTERVAL_SEC * 10 && !m_bStopping; i++) {
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
if (m_bStopping) break;
// Send ping to all WebSocket connections
// Copy pointer first to avoid race with Stop()
void* pServer = m_pServer;
if (pServer && !m_bStopping) {
auto* wsServer = static_cast<ws::Server*>(pServer);
if (wsServer->isRunning()) {
wsServer->pingAll();
}
}
// Check for timed out clients
uint64_t now = (uint64_t)time(nullptr);
std::vector<void*> timedOutClients;
{
std::lock_guard<std::mutex> lock(m_ClientsMutex);
for (const auto& [ws_ptr, client] : m_Clients) {
// Only check clients that are watching a device (streaming video)
if (client.watch_device_id > 0 && client.last_activity > 0) {
uint64_t elapsed = now - client.last_activity;
if (elapsed > HEARTBEAT_TIMEOUT_SEC) {
timedOutClients.push_back(ws_ptr);
Mprintf("[WebService] Client %s timed out (no activity for %llu seconds)\n",
client.client_ip.c_str(), elapsed);
}
}
}
}
// Disconnect timed out clients (outside lock to avoid deadlock)
for (void* ws_ptr : timedOutClients) {
UnregisterClient(ws_ptr);
}
}
Mprintf("[WebService] Heartbeat thread stopped\n");
}
//////////////////////////////////////////////////////////////////////////
// Signaling Handlers
//////////////////////////////////////////////////////////////////////////
void CWebService::HandleLogin(void* ws_ptr, const std::string& msg, const std::string& client_ip) {
Json::Value root;
Json::Reader reader;
if (!reader.parse(msg, root)) {
SendText(ws_ptr, BuildJsonResponse("login_result", false, "Invalid JSON"));
return;
}
std::string username = root.get("username", "").asString();
std::string response = root.get("response", "").asString();
std::string clientNonce = root.get("nonce", "").asString();
// Verify nonce matches (prevents replay)
std::string storedNonce = ConsumeNonce(ws_ptr);
if (storedNonce.empty() || storedNonce != clientNonce) {
SendText(ws_ptr, BuildJsonResponse("login_result", false, "Invalid or expired challenge"));
return;
}
// Check rate limit
if (!CheckRateLimit(client_ip)) {
SendText(ws_ptr, BuildJsonResponse("login_result", false, "Too many failed attempts. Try again later."));
return;
}
// Check if password is configured
if (m_Users.empty()) {
SendText(ws_ptr, BuildJsonResponse("login_result", false, "Server password not configured"));
return;
}
// Find user
WebUser* user = nullptr;
for (auto& u : m_Users) {
if (u.username == username) {
user = &u;
break;
}
}
if (!user) {
RecordFailedLogin(client_ip);
SendText(ws_ptr, BuildJsonResponse("login_result", false, "Invalid credentials"));
return;
}
// Verify challenge response: response = SHA256(passwordHash + nonce)
std::string expectedResponse = WSAuth::ComputeSHA256(user->password_hash + clientNonce);
if (response != expectedResponse) {
RecordFailedLogin(client_ip);
SendText(ws_ptr, BuildJsonResponse("login_result", false, "Invalid credentials"));
return;
}
// Success - generate token
RecordSuccessLogin(client_ip);
std::string token = GenerateToken(username, user->role);
// Update client state
{
std::lock_guard<std::mutex> lock(m_ClientsMutex);
auto it = m_Clients.find(ws_ptr);
if (it != m_Clients.end()) {
it->second.token = token;
it->second.username = username;
it->second.role = user->role;
}
}
// Build response
Json::Value res;
res["cmd"] = "login_result";
res["ok"] = true;
res["token"] = token;
res["role"] = user->role;
res["expires"] = m_nTokenExpireSeconds;
Json::StreamWriterBuilder builder;
builder["indentation"] = "";
SendText(ws_ptr, Json::writeString(builder, res));
}
void CWebService::HandleGetSalt(void* ws_ptr, const std::string& msg) {
Json::Value root;
Json::Reader reader;
if (!reader.parse(msg, root)) {
SendText(ws_ptr, BuildJsonResponse("salt", false, "Invalid JSON"));
return;
}
std::string username = root.get("username", "").asString();
if (username.empty()) {
SendText(ws_ptr, BuildJsonResponse("salt", false, "Username required"));
return;
}
// Find user and get salt
std::string salt = "";
bool userFound = false;
{
std::lock_guard<std::mutex> lock(m_UsersMutex);
for (const auto& u : m_Users) {
if (u.username == username) {
salt = u.salt;
userFound = true;
break;
}
}
}
// For security: if user doesn't exist, generate a fake deterministic salt
// This prevents username enumeration attacks
// Note: Admin has empty salt, so we must check userFound, not salt.empty()
if (!userFound) {
// Generate deterministic fake salt from username (won't match any real password)
salt = WSAuth::ComputeSHA256("fake_salt_prefix_" + username).substr(0, 16);
}
Json::Value res;
res["cmd"] = "salt";
res["ok"] = true;
res["salt"] = salt;
Json::StreamWriterBuilder builder;
builder["indentation"] = "";
SendText(ws_ptr, Json::writeString(builder, res));
}
void CWebService::HandleGetDevices(void* ws_ptr, const std::string& token) {
std::string username, role;
if (!ValidateToken(token, username, role)) {
SendText(ws_ptr, BuildJsonResponse("device_list", false, "Invalid token"));
return;
}
SendText(ws_ptr, BuildDeviceListJson(username));
}
void CWebService::HandleConnect(void* ws_ptr, const std::string& token, uint64_t device_id) {
std::string username, role;
if (!ValidateToken(token, username, role)) {
SendText(ws_ptr, BuildJsonResponse("connect_result", false, "Invalid token"));
return;
}
// Check device exists and is online
if (!m_pParentDlg) {
SendText(ws_ptr, BuildJsonResponse("connect_result", false, "Service not ready"));
return;
}
context* ctx = m_pParentDlg->FindHost(device_id);
if (!ctx) {
SendText(ws_ptr, BuildJsonResponse("connect_result", false, "Device not online"));
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) {
SendText(ws_ptr, BuildJsonResponse("connect_result", false, "Too many viewers"));
return;
}
// Update client state
{
std::lock_guard<std::mutex> lock(m_ClientsMutex);
auto it = m_Clients.find(ws_ptr);
if (it != m_Clients.end()) {
it->second.watch_device_id = device_id;
}
}
// Start remote desktop session if this is the first web viewer
if (current_count == 0) {
if (!StartRemoteDesktop(device_id)) {
SendText(ws_ptr, BuildJsonResponse("connect_result", false, "Failed to start remote desktop"));
return;
}
}
// Get screen dimensions from device info cache (may not be available yet)
int width = 0, height = 0;
{
std::lock_guard<std::mutex> lock(m_DeviceCacheMutex);
auto it = m_DeviceCache.find(device_id);
if (it != m_DeviceCache.end()) {
width = it->second->screen_width;
height = it->second->screen_height;
}
}
// Build success response
Json::Value res;
res["cmd"] = "connect_result";
res["ok"] = true;
// Only include dimensions if we have valid cached values
// Otherwise, client will wait for resolution_changed message
if (width > 0 && height > 0) {
res["width"] = width;
res["height"] = height;
}
res["algorithm"] = "h264";
Json::StreamWriterBuilder builder;
builder["indentation"] = "";
SendText(ws_ptr, Json::writeString(builder, res));
// Send cached keyframe if available
{
std::lock_guard<std::mutex> lock(m_DeviceCacheMutex);
auto it = m_DeviceCache.find(device_id);
if (it != m_DeviceCache.end() && !it->second->keyframe_cache.empty()) {
std::lock_guard<std::mutex> cache_lock(it->second->cache_mutex);
auto packet = BuildFramePacket(device_id, true,
it->second->keyframe_cache.data(),
it->second->keyframe_cache.size());
SendBinary(ws_ptr, packet.data(), packet.size());
}
}
}
void CWebService::HandleDisconnect(void* ws_ptr, const std::string& token, uint64_t requested_device_id) {
std::string username, role;
if (!ValidateToken(token, username, role)) {
SendText(ws_ptr, BuildJsonResponse("disconnect_result", false, "Invalid token"));
return;
}
// Get the device_id before clearing watch state
uint64_t device_id = 0;
{
std::lock_guard<std::mutex> lock(m_ClientsMutex);
auto it = m_Clients.find(ws_ptr);
if (it != m_Clients.end()) {
// Only disconnect if no specific device requested, or if it matches current device
// This prevents race condition when quickly switching devices
if (requested_device_id == 0 || it->second.watch_device_id == requested_device_id) {
device_id = it->second.watch_device_id;
it->second.watch_device_id = 0;
}
}
}
// Close remote desktop if this was the last viewer
if (device_id > 0) {
StopRemoteDesktop(device_id);
}
SendText(ws_ptr, BuildJsonResponse("disconnect_result", true));
}
void CWebService::HandlePing(void* ws_ptr, const std::string& token) {
Json::Value res;
res["cmd"] = "pong";
res["time"] = (Json::Int64)time(nullptr);
Json::StreamWriterBuilder builder;
builder["indentation"] = "";
SendText(ws_ptr, Json::writeString(builder, res));
}
void CWebService::HandleMouse(void* ws_ptr, const std::string& msg) {
// Parse JSON
Json::Value root;
Json::Reader reader;
if (!reader.parse(msg, root)) {
return;
}
std::string token = root.get("token", "").asString();
std::string type = root.get("type", "").asString();
int x = root.get("x", 0).asInt();
int y = root.get("y", 0).asInt();
int button = root.get("button", 0).asInt();
int delta = root.get("delta", 0).asInt();
// Validate token
std::string username, role;
if (!ValidateToken(token, username, role)) {
return;
}
// Get device being watched
uint64_t device_id = 0;
{
std::lock_guard<std::mutex> lock(m_ClientsMutex);
auto it = m_Clients.find(ws_ptr);
if (it != m_Clients.end()) {
device_id = it->second.watch_device_id;
it->second.last_activity = (uint64_t)time(nullptr);
}
}
if (device_id == 0) return;
// Get screen context (not main context!)
CONTEXT_OBJECT* ctx = GetScreenContext(device_id);
if (!ctx) {
return;
}
// Build MSG64 structure
MSG64 msg64;
memset(&msg64, 0, sizeof(MSG64));
msg64.pt.x = x;
msg64.pt.y = y;
msg64.lParam = MAKELPARAM(x, y);
msg64.time = GetTickCount();
// Map type and button to Windows message
if (type == "down") {
if (button == 0) {
msg64.message = WM_LBUTTONDOWN;
msg64.wParam = MK_LBUTTON;
} else if (button == 1) {
msg64.message = WM_MBUTTONDOWN;
msg64.wParam = MK_MBUTTON;
} else if (button == 2) {
msg64.message = WM_RBUTTONDOWN;
msg64.wParam = MK_RBUTTON;
}
} else if (type == "up") {
if (button == 0) {
msg64.message = WM_LBUTTONUP;
} else if (button == 1) {
msg64.message = WM_MBUTTONUP;
} else if (button == 2) {
msg64.message = WM_RBUTTONUP;
}
} else if (type == "move") {
msg64.message = WM_MOUSEMOVE;
} else if (type == "wheel") {
msg64.message = WM_MOUSEWHEEL;
// WM_MOUSEWHEEL: HIWORD(wParam) = wheel delta, LOWORD(wParam) = key flags
// Normalize: browser delta is usually ±100+, Windows expects ±120
short wheelDelta = (short)(delta > 0 ? -120 : (delta < 0 ? 120 : 0));
msg64.wParam = MAKEWPARAM(0, wheelDelta);
} else if (type == "dblclick") {
// dblclick is only needed for macOS clients
// Windows detects double-click from rapid mousedown/mouseup sequence
context* mainCtx = m_pParentDlg->FindHost(device_id);
if (!mainCtx) return;
CString clientType = mainCtx->GetAdditionalData(RES_CLIENT_TYPE);
// Check for both "MAC" (new) and "macOS" (legacy) for compatibility
if (clientType != GetClientType(CLIENT_TYPE_MACOS) && clientType != "macOS") {
return; // Skip dblclick for non-macOS clients
}
if (button == 0) {
msg64.message = WM_LBUTTONDBLCLK;
msg64.wParam = MK_LBUTTON;
} else if (button == 2) {
msg64.message = WM_RBUTTONDBLCLK;
msg64.wParam = MK_RBUTTON;
}
} else {
return; // Unknown type
}
// Send command to device
const int length = sizeof(MSG64) + 1;
BYTE szData[length + 4];
szData[0] = COMMAND_SCREEN_CONTROL;
memcpy(szData + 1, &msg64, sizeof(MSG64));
ctx->Send2Client(szData, length);
}
void CWebService::HandleKey(void* ws_ptr, const std::string& msg) {
// Parse JSON
Json::Value root;
Json::Reader reader;
if (!reader.parse(msg, root)) {
return;
}
std::string token = root.get("token", "").asString();
int keyCode = root.get("keyCode", 0).asInt();
bool isDown = root.get("down", false).asBool();
bool altKey = root.get("alt", false).asBool();
// Filter Windows keys (same as MFC version)
if (keyCode == VK_LWIN || keyCode == VK_RWIN) {
return;
}
// Validate token
std::string username, role;
if (!ValidateToken(token, username, role)) {
return;
}
// Get device being watched
uint64_t device_id = 0;
{
std::lock_guard<std::mutex> lock(m_ClientsMutex);
auto it = m_Clients.find(ws_ptr);
if (it != m_Clients.end()) {
device_id = it->second.watch_device_id;
it->second.last_activity = (uint64_t)time(nullptr);
}
}
if (device_id == 0) return;
// Get screen context
CONTEXT_OBJECT* ctx = GetScreenContext(device_id);
if (!ctx) {
return;
}
// Build MSG64 structure
MSG64 msg64;
memset(&msg64, 0, sizeof(MSG64));
// Use WM_SYSKEYDOWN/UP for Alt combinations (same as MFC version)
if (altKey) {
msg64.message = isDown ? WM_SYSKEYDOWN : WM_SYSKEYUP;
} else {
msg64.message = isDown ? WM_KEYDOWN : WM_KEYUP;
}
msg64.wParam = keyCode;
msg64.time = GetTickCount();
// Build lParam for keyboard message:
// bits 0-15: repeat count (1)
// bits 16-23: scan code
// bit 24: extended key flag
// bit 29: context code (1 if Alt is pressed)
// bit 30: previous key state (1 for keyup)
// bit 31: transition state (1 for keyup)
UINT scanCode = MapVirtualKey(keyCode, MAPVK_VK_TO_VSC);
// Extended keys: arrows, insert, delete, home, end, page up/down, numpad enter, etc.
bool isExtended = (keyCode >= VK_PRIOR && keyCode <= VK_DOWN) || // Page Up/Down, End, Home, Arrows
keyCode == VK_INSERT || keyCode == VK_DELETE ||
keyCode == VK_NUMLOCK || keyCode == VK_RCONTROL || keyCode == VK_RMENU ||
keyCode == VK_APPS;
LPARAM lParam = 1; // repeat count = 1
lParam |= (scanCode & 0xFF) << 16;
if (isExtended) lParam |= (1 << 24);
if (altKey) lParam |= (1 << 29); // context code for Alt
if (!isDown) lParam |= (3UL << 30); // bit 30 and 31 set for key up
msg64.lParam = lParam;
// Send command to device
const int length = sizeof(MSG64) + 1;
BYTE szData[length + 4];
szData[0] = COMMAND_SCREEN_CONTROL;
memcpy(szData + 1, &msg64, sizeof(MSG64));
ctx->Send2Client(szData, length);
}
void CWebService::HandleRdpReset(void* ws_ptr, const std::string& token) {
std::string username, role;
if (!ValidateToken(token, username, role)) {
SendText(ws_ptr, BuildJsonResponse("rdp_reset_result", false, "Invalid token"));
return;
}
// Get the device being watched by this client
uint64_t device_id = 0;
{
std::lock_guard<std::mutex> lock(m_ClientsMutex);
auto it = m_Clients.find(ws_ptr);
if (it != m_Clients.end()) {
device_id = it->second.watch_device_id;
}
}
if (device_id == 0) {
SendText(ws_ptr, BuildJsonResponse("rdp_reset_result", false, "No device connected"));
return;
}
// Get screen context (not main context!)
CONTEXT_OBJECT* ctx = GetScreenContext(device_id);
if (!ctx) {
Mprintf("[WebService] HandleRdpReset: No screen context for device %llu\n", device_id);
SendText(ws_ptr, BuildJsonResponse("rdp_reset_result", false, "No active screen session"));
return;
}
// Send CMD_RESTORE_CONSOLE command to client
BYTE bToken = CMD_RESTORE_CONSOLE;
if (ctx->Send2Client(&bToken, 1)) {
Mprintf("[WebService] Sent RDP reset command to device %llu\n", device_id);
SendText(ws_ptr, BuildJsonResponse("rdp_reset_result", true));
} else {
SendText(ws_ptr, BuildJsonResponse("rdp_reset_result", false, "Failed to send command"));
}
}
//////////////////////////////////////////////////////////////////////////
// User Management Handlers
//////////////////////////////////////////////////////////////////////////
void CWebService::HandleCreateUser(void* ws_ptr, const std::string& msg) {
Json::Value root;
Json::Reader reader;
if (!reader.parse(msg, root)) {
SendText(ws_ptr, BuildJsonResponse("create_user_result", false, "Invalid JSON"));
return;
}
std::string token = root.get("token", "").asString();
std::string username, role;
if (!ValidateToken(token, username, role)) {
SendText(ws_ptr, BuildJsonResponse("create_user_result", false, "Invalid token"));
return;
}
// Only admin can create users
if (role != "admin") {
SendText(ws_ptr, BuildJsonResponse("create_user_result", false, "Permission denied"));
return;
}
std::string newUsername = root.get("username", "").asString();
std::string newPassword = root.get("password", "").asString();
std::string newRole = root.get("role", "viewer").asString();
// 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, 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)"));
}
}
void CWebService::HandleDeleteUser(void* ws_ptr, const std::string& msg) {
Json::Value root;
Json::Reader reader;
if (!reader.parse(msg, root)) {
SendText(ws_ptr, BuildJsonResponse("delete_user_result", false, "Invalid JSON"));
return;
}
std::string token = root.get("token", "").asString();
std::string username, role;
if (!ValidateToken(token, username, role)) {
SendText(ws_ptr, BuildJsonResponse("delete_user_result", false, "Invalid token"));
return;
}
// Only admin can delete users
if (role != "admin") {
SendText(ws_ptr, BuildJsonResponse("delete_user_result", false, "Permission denied"));
return;
}
std::string targetUsername = root.get("username", "").asString();
if (DeleteUser(targetUsername)) {
SendText(ws_ptr, BuildJsonResponse("delete_user_result", true));
} else {
SendText(ws_ptr, BuildJsonResponse("delete_user_result", false, "Failed to delete user"));
}
}
void CWebService::HandleListUsers(void* ws_ptr, const std::string& token) {
std::string username, role;
if (!ValidateToken(token, username, role)) {
SendText(ws_ptr, BuildJsonResponse("list_users_result", false, "Invalid token"));
return;
}
// Only admin can list users
if (role != "admin") {
SendText(ws_ptr, BuildJsonResponse("list_users_result", false, "Permission denied"));
return;
}
Json::Value res;
res["cmd"] = "list_users_result";
res["ok"] = true;
Json::Value usersArray(Json::arrayValue);
{
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;
Json::StreamWriterBuilder builder;
builder["indentation"] = "";
std::string json = Json::writeString(builder, res);
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)
//////////////////////////////////////////////////////////////////////////
std::string CWebService::GenerateToken(const std::string& username, const std::string& role) {
return WSAuth::GenerateToken(username, role, m_nTokenExpireSeconds);
}
bool CWebService::ValidateToken(const std::string& token, std::string& username, std::string& role) {
return WSAuth::ValidateToken(token, username, role);
}
//////////////////////////////////////////////////////////////////////////
// Client Management
//////////////////////////////////////////////////////////////////////////
void CWebService::RegisterClient(void* ws_ptr, const std::string& client_ip) {
std::lock_guard<std::mutex> lock(m_ClientsMutex);
WebClient client;
client.client_ip = client_ip;
client.connected_at = (uint64_t)time(nullptr);
client.last_activity = client.connected_at; // Initialize for heartbeat
m_Clients[ws_ptr] = client;
}
void CWebService::UnregisterClient(void* ws_ptr) {
uint64_t device_id = 0;
// Get device_id and remove client
{
std::lock_guard<std::mutex> lock(m_ClientsMutex);
auto it = m_Clients.find(ws_ptr);
if (it != m_Clients.end()) {
device_id = it->second.watch_device_id;
m_Clients.erase(it);
}
}
// Close remote desktop if this was the last viewer
if (device_id > 0) {
StopRemoteDesktop(device_id);
}
// 关闭这个 web client 持有的所有终端会话MVP 一个用户一台主机一个,但兜底全扫描)
{
std::lock_guard<std::mutex> lk(m_TermMutex);
std::vector<uint64_t> to_close;
for (auto& kv : m_TermSessions) {
if (kv.second.ws_ptr == ws_ptr) to_close.push_back(kv.first);
}
for (uint64_t did : to_close) CloseTermSessionLocked(did);
}
}
WebClient* CWebService::FindClient(void* ws_ptr) {
auto it = m_Clients.find(ws_ptr);
return (it != m_Clients.end()) ? &it->second : nullptr;
}
int CWebService::GetWebClientCount(uint64_t device_id) {
std::lock_guard<std::mutex> lock(m_ClientsMutex);
int count = 0;
for (const auto& [ptr, client] : m_Clients) {
if (client.watch_device_id == device_id) {
count++;
}
}
return count;
}
//////////////////////////////////////////////////////////////////////////
// Rate Limiting
//////////////////////////////////////////////////////////////////////////
bool CWebService::CheckRateLimit(const std::string& ip) {
std::lock_guard<std::mutex> lock(m_LoginMutex);
auto it = m_LoginAttempts.find(ip);
if (it == m_LoginAttempts.end()) return true;
return time(nullptr) >= it->second.locked_until;
}
void CWebService::RecordFailedLogin(const std::string& ip) {
std::lock_guard<std::mutex> lock(m_LoginMutex);
auto& attempt = m_LoginAttempts[ip];
attempt.failed_count++;
if (attempt.failed_count >= 3) {
attempt.locked_until = time(nullptr) + 3600; // Lock for 1 hour
attempt.failed_count = 0;
Mprintf("[WebService] IP %s locked for 1 hour due to failed login attempts\n", ip.c_str());
}
}
void CWebService::RecordSuccessLogin(const std::string& ip) {
std::lock_guard<std::mutex> lock(m_LoginMutex);
m_LoginAttempts.erase(ip);
}
//////////////////////////////////////////////////////////////////////////
// Password Verification (delegated to WebServiceAuth module)
//////////////////////////////////////////////////////////////////////////
bool CWebService::VerifyPassword(const std::string& input, const WebUser& user) {
return WSAuth::VerifyPassword(input, user.password_hash, user.salt);
}
std::string CWebService::ComputeHash(const std::string& input) {
return WSAuth::ComputeSHA256(input);
}
//////////////////////////////////////////////////////////////////////////
// User Management
//////////////////////////////////////////////////////////////////////////
std::string CWebService::GetUsersFilePath() {
return m_ConfigDir + "users.json";
}
void CWebService::LoadUsers() {
// Note: m_UsersMutex should already be held by caller (SetAdminPassword)
// Load additional users from users.json (admin user is already in m_Users)
std::string path = GetUsersFilePath();
std::ifstream file(path);
if (!file.is_open()) {
Mprintf("[WebService] No users.json found, using admin only\n");
return;
}
try {
Json::Value root;
Json::CharReaderBuilder builder;
std::string errors;
if (!Json::parseFromStream(builder, file, &root, &errors)) {
Mprintf("[WebService] Failed to parse users.json: %s\n", errors.c_str());
return;
}
const Json::Value& users = root["users"];
int loaded = 0;
for (const auto& u : users) {
std::string username = u.get("username", "").asString();
// Skip admin user (it's built-in with master password)
if (username.empty() || username == "admin") continue;
WebUser user;
user.username = username;
user.password_hash = u.get("password_hash", "").asString();
user.salt = u.get("salt", "").asString();
user.role = u.get("role", "viewer").asString();
// 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++;
}
}
Mprintf("[WebService] Loaded %d additional users from users.json\n", loaded);
} catch (const std::exception& e) {
Mprintf("[WebService] Error loading users.json: %s\n", e.what());
}
}
void CWebService::SaveUsers() {
// Save non-admin users to users.json
std::lock_guard<std::mutex> lock(m_UsersMutex);
Json::Value root;
Json::Value users(Json::arrayValue);
for (const auto& u : m_Users) {
// Skip admin user (it uses master password, not stored in file)
if (u.username == "admin") continue;
Json::Value user;
user["username"] = u.username;
user["password_hash"] = u.password_hash;
user["salt"] = u.salt;
user["role"] = u.role;
// 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);
}
root["users"] = users;
std::string path = GetUsersFilePath();
std::ofstream file(path);
if (!file.is_open()) {
Mprintf("[WebService] Failed to open users.json for writing\n");
return;
}
Json::StreamWriterBuilder builder;
builder["indentation"] = " ";
std::unique_ptr<Json::StreamWriter> writer(builder.newStreamWriter());
writer->write(root, &file);
Mprintf("[WebService] Saved %d users to users.json\n", (int)users.size());
}
bool CWebService::CreateUser(const std::string& username, const std::string& password, const std::string& role,
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;
{
std::lock_guard<std::mutex> lock(m_UsersMutex);
// Check if user already exists
for (const auto& u : m_Users) {
if (u.username == username) return false;
}
// Generate salt and hash password with salt
WebUser user;
user.username = username;
user.salt = GenerateSalt();
user.password_hash = WSAuth::ComputeSHA256(password + user.salt);
user.role = role;
user.allowed_groups = allowed_groups;
m_Users.push_back(user);
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)
SaveUsers();
return true;
}
bool CWebService::DeleteUser(const std::string& username) {
if (username.empty() || username == "admin") return false;
bool deleted = false;
{
std::lock_guard<std::mutex> lock(m_UsersMutex);
for (auto it = m_Users.begin(); it != m_Users.end(); ++it) {
if (it->username == username) {
m_Users.erase(it);
Mprintf("[WebService] Deleted user: %s\n", username.c_str());
deleted = true;
break;
}
}
}
if (deleted) {
SaveUsers();
}
return deleted;
}
std::vector<std::pair<std::string, std::string>> CWebService::ListUsers() {
std::lock_guard<std::mutex> lock(m_UsersMutex);
std::vector<std::pair<std::string, std::string>> result;
for (const auto& u : m_Users) {
result.push_back({u.username, u.role});
}
return result;
}
//////////////////////////////////////////////////////////////////////////
// JSON Helpers
//////////////////////////////////////////////////////////////////////////
std::string CWebService::BuildJsonResponse(const std::string& cmd, bool ok, const std::string& msg) {
Json::Value res;
res["cmd"] = cmd;
res["ok"] = ok;
if (!msg.empty()) {
res["msg"] = msg;
}
Json::StreamWriterBuilder builder;
builder["indentation"] = "";
return Json::writeString(builder, res);
}
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());
CString name = ctx->GetClientData(ONLINELIST_COMPUTER_NAME);
device["name"] = AnsiToUtf8(name);
CString ip = ctx->GetClientData(ONLINELIST_IP);
device["ip"] = AnsiToUtf8(ip);
CString os = ctx->GetClientData(ONLINELIST_OS);
device["os"] = AnsiToUtf8(os);
CString location = ctx->GetClientData(ONLINELIST_LOCATION);
device["location"] = AnsiToUtf8(location);
CString rtt = ctx->GetClientData(ONLINELIST_PING);
device["rtt"] = AnsiToUtf8(rtt);
CString version = ctx->GetClientData(ONLINELIST_VERSION);
device["version"] = AnsiToUtf8(version);
// 活动窗口编码由客户端能力位决定:新客户端是 UTF-8老客户端是 CP_ACP默认 936
// 不能像其它字段那样无脑 AnsiToUtf8——会把新客户端的 UTF-8 字节再当 GBK 双重编码。
CString activeWindow = ctx->GetClientData(ONLINELIST_LOGINTIME);
std::string activeWindowU8;
if (!activeWindow.IsEmpty()) {
UINT cp = GetClientEncoding(ctx);
int wlen = MultiByteToWideChar(cp, 0, activeWindow, -1, NULL, 0);
if (wlen > 1) {
std::wstring w(wlen - 1, L'\0');
MultiByteToWideChar(cp, 0, activeWindow, -1, &w[0], wlen);
int u8len = WideCharToMultiByte(CP_UTF8, 0, w.c_str(), -1, NULL, 0, NULL, NULL);
if (u8len > 1) {
activeWindowU8.resize(u8len - 1);
WideCharToMultiByte(CP_UTF8, 0, w.c_str(), -1, &activeWindowU8[0], u8len, NULL, NULL);
}
}
}
device["activeWindow"] = activeWindowU8;
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);
if (!resolution.IsEmpty()) {
device["screen"] = AnsiToUtf8(resolution); // e.g. "2:3840x1080"
}
res["devices"].append(device);
}
LeaveCriticalSection(&m_pParentDlg->m_cs);
}
Json::StreamWriterBuilder builder;
builder["indentation"] = "";
return Json::writeString(builder, res);
}
//////////////////////////////////////////////////////////////////////////
// WebSocket Send Helpers
//////////////////////////////////////////////////////////////////////////
void CWebService::SendText(void* ws_ptr, const std::string& text) {
if (!ws_ptr || m_bStopping) return;
ws::Connection* conn = (ws::Connection*)ws_ptr;
if (!conn->isClosed()) {
conn->send(text);
}
}
void CWebService::SendBinary(void* ws_ptr, const uint8_t* data, size_t len) {
if (!ws_ptr || !data || len == 0 || m_bStopping) return;
ws::Connection* conn = (ws::Connection*)ws_ptr;
if (!conn->isClosed()) {
conn->sendBinary(data, len);
}
}
//////////////////////////////////////////////////////////////////////////
// Frame Broadcasting
//////////////////////////////////////////////////////////////////////////
std::vector<uint8_t> CWebService::BuildFramePacket(uint64_t device_id, bool is_keyframe,
const uint8_t* data, size_t len) {
// Packet format: [DeviceID:4][FrameType:1][DataLen:4][H264Data:N]
std::vector<uint8_t> packet;
packet.reserve(9 + len);
// DeviceID (4 bytes, little-endian, truncated to 32-bit for JS compatibility)
uint32_t id32 = (uint32_t)(device_id & 0xFFFFFFFF);
packet.push_back((id32 >> 0) & 0xFF);
packet.push_back((id32 >> 8) & 0xFF);
packet.push_back((id32 >> 16) & 0xFF);
packet.push_back((id32 >> 24) & 0xFF);
// FrameType (1 byte): 0=P, 1=IDR
packet.push_back(is_keyframe ? 1 : 0);
// DataLen (4 bytes, little-endian)
uint32_t data_len = (uint32_t)len;
packet.push_back((data_len >> 0) & 0xFF);
packet.push_back((data_len >> 8) & 0xFF);
packet.push_back((data_len >> 16) & 0xFF);
packet.push_back((data_len >> 24) & 0xFF);
// H264 Data
packet.insert(packet.end(), data, data + len);
return packet;
}
void CWebService::BroadcastFrame(uint64_t device_id, const uint8_t* data, size_t len, bool is_keyframe) {
if (!data || len == 0 || m_bStopping) return;
// Build packet once
auto packet = BuildFramePacket(device_id, is_keyframe, data, len);
// Broadcast to all watching clients
std::lock_guard<std::mutex> lock(m_ClientsMutex);
for (auto& [ws_ptr, client] : m_Clients) {
if (client.watch_device_id == device_id) {
SendBinary(ws_ptr, packet.data(), packet.size());
}
}
}
void CWebService::CacheKeyframe(uint64_t device_id, const uint8_t* data, size_t len) {
if (!data || len == 0 || m_bStopping) return;
std::lock_guard<std::mutex> lock(m_DeviceCacheMutex);
auto it = m_DeviceCache.find(device_id);
if (it == m_DeviceCache.end()) {
m_DeviceCache[device_id] = std::make_shared<WebDeviceInfo>();
it = m_DeviceCache.find(device_id);
}
std::lock_guard<std::mutex> cache_lock(it->second->cache_mutex);
it->second->keyframe_cache.assign(data, data + len);
}
void CWebService::BroadcastH264Frame(uint64_t device_id, const uint8_t* data, size_t len) {
// The data is already a complete packet: [DeviceID:4][FrameType:1][DataLen:4][H264Data:N]
if (!data || len < 9 || m_bStopping) return;
// Broadcast to all watching clients
std::lock_guard<std::mutex> lock(m_ClientsMutex);
for (auto& [ws_ptr, client] : m_Clients) {
if (client.watch_device_id == device_id) {
SendBinary(ws_ptr, data, len);
}
}
// Cache keyframe (check FrameType byte at offset 4)
if (data[4] == 1) { // IDR frame
CacheKeyframe(device_id, data, len);
}
}
void CWebService::NotifyResolutionChange(uint64_t device_id, int width, int height) {
if (m_bStopping) return;
// Update cache
{
std::lock_guard<std::mutex> lock(m_DeviceCacheMutex);
auto it = m_DeviceCache.find(device_id);
if (it == m_DeviceCache.end()) {
m_DeviceCache[device_id] = std::make_shared<WebDeviceInfo>();
it = m_DeviceCache.find(device_id);
}
it->second->screen_width = width;
it->second->screen_height = height;
}
// Notify watching clients
Json::Value res;
res["cmd"] = "resolution_changed";
res["id"] = device_id;
res["width"] = width;
res["height"] = height;
Json::StreamWriterBuilder builder;
builder["indentation"] = "";
std::string json = Json::writeString(builder, res);
std::lock_guard<std::mutex> lock(m_ClientsMutex);
for (auto& [ws_ptr, client] : m_Clients) {
if (client.watch_device_id == device_id) {
SendText(ws_ptr, json);
}
}
}
void CWebService::BroadcastCursor(uint64_t device_id, uint8_t cursor_index) {
if (m_bStopping) return;
// Build JSON message
Json::Value res;
res["cmd"] = "cursor";
res["index"] = cursor_index;
Json::StreamWriterBuilder builder;
builder["indentation"] = "";
std::string json = Json::writeString(builder, res);
// Send to all watching clients
std::lock_guard<std::mutex> lock(m_ClientsMutex);
for (auto& [ws_ptr, client] : m_Clients) {
if (client.watch_device_id == device_id) {
SendText(ws_ptr, json);
}
}
}
bool CWebService::StartRemoteDesktop(uint64_t device_id) {
if (!m_pParentDlg) return false;
context* ctx = m_pParentDlg->FindHost(device_id);
if (!ctx) return false;
// 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)
{
std::lock_guard<std::mutex> lock(m_WebTriggeredMutex);
m_WebTriggeredDevices.insert(device_id);
}
// Send COMMAND_SCREEN_SPY with H264 algorithm
// 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
bToken[2] = ALGORITHM_H264; // H264 algorithm
bToken[3] = 1; // Multi-screen: true
return ctx->Send2Client(bToken, sizeof(bToken)) != FALSE;
}
void CWebService::StopRemoteDesktop(uint64_t device_id) {
if (!m_pParentDlg) return;
// Check if any other web clients are watching this device
int watchingCount = 0;
{
std::lock_guard<std::mutex> lock(m_ClientsMutex);
for (const auto& [ws_ptr, client] : m_Clients) {
if (client.watch_device_id == device_id) {
watchingCount++;
}
}
}
// If no more web clients watching, close only the Web session dialog
// MFC dialogs remain open
if (watchingCount == 0) {
ClearWebTriggered(device_id);
m_pParentDlg->CloseWebRemoteDesktopByClientID(device_id);
}
}
//////////////////////////////////////////////////////////////////////////
// Web Terminal Session
//
// 数据流向:
// 浏览器 ── term_open ──► HandleTermOpen ── COMMAND_SHELL ──► 客户端
// 客户端 ── shell 子上下文 ──► MessageHandle TOKEN_TERMINAL_START
// ── RegisterTerminalContext ──► WebService
// 客户端 shell 输出 ── TOKEN_TERMINAL_DATA ──► MessageHandle ──► OnTerminalData
// ──► term_output 给浏览器
// 浏览器 keystrokes ── term_input ──► HandleTermInput ──► shell_ctx->Send2Client
//
// 一台主机最多一个 web 终端会话MVP
//////////////////////////////////////////////////////////////////////////
static std::string BuildTermJson(const std::string& cmd, std::initializer_list<std::pair<const char*, std::string>> kv) {
Json::Value v;
v["cmd"] = cmd;
for (auto& p : kv) v[p.first] = p.second;
Json::StreamWriterBuilder b; b["indentation"] = "";
return Json::writeString(b, v);
}
void CWebService::HandleTermOpen(void* ws_ptr, const std::string& msg) {
Json::Value root; Json::Reader rdr;
if (!rdr.parse(msg, root)) return;
std::string token = root.get("token", "").asString();
std::string username, role;
if (!ValidateToken(token, username, role)) {
SendText(ws_ptr, BuildJsonResponse("term_closed", false, "Invalid token"));
return;
}
std::string id_str = root.get("id", "").asString();
uint64_t device_id = id_str.empty() ? 0 : strtoull(id_str.c_str(), nullptr, 10);
if (!device_id || !m_pParentDlg) {
SendText(ws_ptr, BuildJsonResponse("term_closed", false, "Bad request"));
return;
}
context* ctx = m_pParentDlg->FindHost(device_id);
if (!ctx) {
SendText(ws_ptr, BuildJsonResponse("term_closed", false, "Device offline"));
return;
}
// Group permission check (admin 全部可见)
if (username != "admin") {
std::string g = ctx->GetGroupName(); if (g.empty()) g = "default";
bool ok = false;
{
std::lock_guard<std::mutex> lk(m_UsersMutex);
for (auto& u : m_Users) if (u.username == username) {
for (auto& ag : u.allowed_groups) if (ag == g) { ok = true; break; }
break;
}
}
if (!ok) {
SendText(ws_ptr, BuildJsonResponse("term_closed", false, "Permission denied"));
return;
}
}
// 占用MVP 阶段单设备单 web 终端
{
std::lock_guard<std::mutex> lk(m_TermMutex);
if (m_TermSessions.find(device_id) != m_TermSessions.end() ||
m_TermPending.find(device_id) != m_TermPending.end()) {
SendText(ws_ptr, BuildJsonResponse("term_closed", false,
"Terminal already open by another viewer"));
return;
}
WebTermSession s; s.ws_ptr = ws_ptr; s.device_id = device_id;
s.shell_ctx = nullptr; s.is_pty = false;
m_TermSessions[device_id] = s;
m_TermPending.insert(device_id);
}
// 触发客户端:发 COMMAND_SHELL
BYTE cmd = COMMAND_SHELL;
if (!ctx->Send2Client(&cmd, 1)) {
std::lock_guard<std::mutex> lk(m_TermMutex);
m_TermSessions.erase(device_id);
m_TermPending.erase(device_id);
SendText(ws_ptr, BuildJsonResponse("term_closed", false, "Send failed"));
return;
}
Mprintf("[WebService] term_open device=%llu user=%s\n", device_id, username.c_str());
}
void CWebService::HandleTermInput(void* ws_ptr, const std::string& msg) {
Json::Value root; Json::Reader rdr;
if (!rdr.parse(msg, root)) return;
std::string token = root.get("token", "").asString();
std::string username, role;
if (!ValidateToken(token, username, role)) return;
std::string id_str = root.get("id", "").asString();
uint64_t device_id = id_str.empty() ? 0 : strtoull(id_str.c_str(), nullptr, 10);
std::string data = root.get("data", "").asString();
if (!device_id || data.empty()) return;
CONTEXT_OBJECT* shellCtx = nullptr;
{
std::lock_guard<std::mutex> lk(m_TermMutex);
auto it = m_TermSessions.find(device_id);
if (it == m_TermSessions.end() || it->second.ws_ptr != ws_ptr) return;
shellCtx = it->second.shell_ctx;
}
if (!shellCtx) return; // shell 子上下文还没就绪
shellCtx->Send2Client((BYTE*)data.data(), (ULONG)data.size());
}
void CWebService::HandleTermResize(void* ws_ptr, const std::string& msg) {
Json::Value root; Json::Reader rdr;
if (!rdr.parse(msg, root)) return;
std::string token = root.get("token", "").asString();
std::string username, role;
if (!ValidateToken(token, username, role)) return;
std::string id_str = root.get("id", "").asString();
uint64_t device_id = id_str.empty() ? 0 : strtoull(id_str.c_str(), nullptr, 10);
int cols = root.get("cols", 0).asInt();
int rows = root.get("rows", 0).asInt();
if (!device_id || cols <= 0 || rows <= 0) return;
CONTEXT_OBJECT* shellCtx = nullptr;
bool isPty = false;
{
std::lock_guard<std::mutex> lk(m_TermMutex);
auto it = m_TermSessions.find(device_id);
if (it == m_TermSessions.end() || it->second.ws_ptr != ws_ptr) return;
shellCtx = it->second.shell_ctx;
isPty = it->second.is_pty;
}
if (!shellCtx || !isPty) return; // 老 cmd 模式不支持 resize
BYTE buf[5];
buf[0] = CMD_TERMINAL_RESIZE;
*(short*)(buf + 1) = (short)cols;
*(short*)(buf + 3) = (short)rows;
shellCtx->Send2Client(buf, 5);
}
void CWebService::HandleTermClose(void* ws_ptr, const std::string& msg) {
Json::Value root; Json::Reader rdr;
if (!rdr.parse(msg, root)) return;
std::string id_str = root.get("id", "").asString();
uint64_t device_id = id_str.empty() ? 0 : strtoull(id_str.c_str(), nullptr, 10);
if (!device_id) return;
std::lock_guard<std::mutex> lk(m_TermMutex);
auto it = m_TermSessions.find(device_id);
if (it == m_TermSessions.end() || it->second.ws_ptr != ws_ptr) return;
CloseTermSessionLocked(device_id);
}
void CWebService::CloseTermSessionLocked(uint64_t device_id) {
auto it = m_TermSessions.find(device_id);
if (it == m_TermSessions.end()) return;
CONTEXT_OBJECT* shellCtx = it->second.shell_ctx;
void* ws_ptr = it->second.ws_ptr;
m_TermSessions.erase(it);
m_TermPending.erase(device_id);
if (shellCtx) {
m_TermContextToDevice.erase(shellCtx);
// 触发客户端 shell 退出:直接断该子上下文
// (老 ShellDlg 是直接 Send TOKEN_BYE 之类,但断开更可靠)
shellCtx->CancelIO();
}
// 通知前端
SendText(ws_ptr, BuildJsonResponse("term_closed", true, "closed"));
Mprintf("[WebService] term_closed device=%llu\n", device_id);
}
bool CWebService::IsTermPending(uint64_t device_id) {
std::lock_guard<std::mutex> lk(m_TermMutex);
return m_TermPending.find(device_id) != m_TermPending.end();
}
void CWebService::RegisterTerminalContext(uint64_t device_id, CONTEXT_OBJECT* ctx, bool isPty) {
void* ws_ptr_to_notify = nullptr;
{
std::lock_guard<std::mutex> lk(m_TermMutex);
auto it = m_TermSessions.find(device_id);
if (it == m_TermSessions.end()) return; // 没有等的 web session 了
it->second.shell_ctx = ctx;
it->second.is_pty = isPty;
m_TermContextToDevice[ctx] = device_id;
m_TermPending.erase(device_id);
ws_ptr_to_notify = it->second.ws_ptr;
}
// 关键步骤:告知客户端"启动 shell 输出回流"。
// PTY 模式:客户端 PTYHandler 已 fork 了 shell 子进程,但读线程要靠 COMMAND_NEXT 才启动
// (参考 TerminalDlg::OnTerminalReady。漏发会导致 shell 在跑但输出永远不送回。
// PTY 还要先告知初始 cols/rows默认 80x24否则 shell 会按 PTY 默认尺寸渲染,
// vim 等 TUI 在浏览器侧的 fit 调整前会乱。后续浏览器 term_resize 会再调整。
if (isPty) {
BYTE resizeBuf[5];
resizeBuf[0] = CMD_TERMINAL_RESIZE;
*(short*)(resizeBuf + 1) = (short)80;
*(short*)(resizeBuf + 3) = (short)24;
ctx->Send2Client(resizeBuf, 5);
}
BYTE startCmd = COMMAND_NEXT;
ctx->Send2Client(&startCmd, 1);
// 通知前端 ready告知模式pty / legacy
if (ws_ptr_to_notify) {
SendText(ws_ptr_to_notify, BuildTermJson("term_ready", {{"mode", isPty ? "pty" : "legacy"}}));
}
Mprintf("[WebService] term_ready device=%llu mode=%s\n",
device_id, isPty ? "pty" : "legacy");
}
bool CWebService::IsTerminalContext(CONTEXT_OBJECT* ctx) {
std::lock_guard<std::mutex> lk(m_TermMutex);
return m_TermContextToDevice.find(ctx) != m_TermContextToDevice.end();
}
void CWebService::OnTerminalData(CONTEXT_OBJECT* ctx, const BYTE* data, ULONG len) {
void* ws_ptr = nullptr;
{
std::lock_guard<std::mutex> lk(m_TermMutex);
auto it = m_TermContextToDevice.find(ctx);
if (it == m_TermContextToDevice.end()) return;
auto sit = m_TermSessions.find(it->second);
if (sit == m_TermSessions.end()) return;
ws_ptr = sit->second.ws_ptr;
}
if (!ws_ptr || !data || !len) return;
// 用 binary frame 透传字节流(避免 JSON 二进制不可见字符 / 编码膨胀)。
// 帧格式:[4B magic 'TRM1'][N=payload]
// 4 字节 magic = 0x54 0x52 0x4D 0x31 —— 视频帧首 4 字节是 deviceIDuint32 LE
// 撞这个具体值 (0x314D5254) 的概率极低,浏览器侧据此安全分流。
std::vector<uint8_t> packet;
packet.reserve(len + 4);
packet.push_back('T'); packet.push_back('R'); packet.push_back('M'); packet.push_back('1');
packet.insert(packet.end(), data, data + len);
SendBinary(ws_ptr, packet.data(), packet.size());
}
void CWebService::OnTerminalClosed(CONTEXT_OBJECT* ctx) {
std::lock_guard<std::mutex> lk(m_TermMutex);
auto it = m_TermContextToDevice.find(ctx);
if (it == m_TermContextToDevice.end()) return;
uint64_t device_id = it->second;
CloseTermSessionLocked(device_id);
}
//////////////////////////////////////////////////////////////////////////
// Screen Context Registry (for mouse/keyboard control)
//////////////////////////////////////////////////////////////////////////
void CWebService::RegisterScreenContext(uint64_t device_id, CONTEXT_OBJECT* ctx) {
if (!m_bRunning) return;
std::lock_guard<std::mutex> lock(m_ScreenContextsMutex);
m_ScreenContexts[device_id] = ctx;
Mprintf("[WebService] Registered screen context for device %llu\n", device_id);
}
void CWebService::UnregisterScreenContext(uint64_t device_id) {
// 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);
if (m_bRunning) {
Mprintf("[WebService] Unregistered screen context for device %llu\n", device_id);
}
}
CONTEXT_OBJECT* CWebService::GetScreenContext(uint64_t device_id) {
std::lock_guard<std::mutex> lock(m_ScreenContextsMutex);
auto it = m_ScreenContexts.find(device_id);
return (it != m_ScreenContexts.end()) ? it->second : nullptr;
}
//////////////////////////////////////////////////////////////////////////
// Device Events
//////////////////////////////////////////////////////////////////////////
void CWebService::MarkDeviceOnline(uint64_t device_id) {
std::lock_guard<std::mutex> lock(m_DirtyDevicesMutex);
m_OnlineDevices.insert(device_id);
m_OfflineDevices.erase(device_id); // Cancel pending offline if any
}
void CWebService::MarkDeviceOffline(uint64_t device_id) {
std::lock_guard<std::mutex> lock(m_DirtyDevicesMutex);
m_OfflineDevices.insert(device_id);
m_OnlineDevices.erase(device_id); // Cancel pending online if any
}
void CWebService::FlushDeviceChanges() {
if (!m_bRunning || m_bStopping) return;
// Collect changes under lock
std::set<uint64_t> online, offline;
{
std::lock_guard<std::mutex> lock(m_DirtyDevicesMutex);
if (m_OnlineDevices.empty() && m_OfflineDevices.empty()) {
return; // No changes
}
online = std::move(m_OnlineDevices);
offline = std::move(m_OfflineDevices);
m_OnlineDevices.clear();
m_OfflineDevices.clear();
}
// Notify clients watching offline devices
if (!offline.empty()) {
std::lock_guard<std::mutex> lock(m_ClientsMutex);
for (auto& [ws_ptr, client] : m_Clients) {
if (offline.count(client.watch_device_id)) {
Json::Value res;
res["cmd"] = "device_offline";
res["id"] = std::to_string(client.watch_device_id);
Json::StreamWriterBuilder builder;
builder["indentation"] = "";
SendText(ws_ptr, Json::writeString(builder, res));
client.watch_device_id = 0;
}
}
}
// Clear offline devices from cache
{
std::lock_guard<std::mutex> lock(m_DeviceCacheMutex);
for (uint64_t id : offline) {
m_DeviceCache.erase(id);
}
}
// Build and broadcast devices_changed message
Json::Value msg;
msg["cmd"] = "devices_changed";
msg["online"] = Json::Value(Json::arrayValue);
msg["offline"] = Json::Value(Json::arrayValue);
for (uint64_t id : online) {
msg["online"].append(std::to_string(id));
}
for (uint64_t id : offline) {
msg["offline"].append(std::to_string(id));
}
Json::StreamWriterBuilder builder;
builder["indentation"] = "";
std::string json = Json::writeString(builder, msg);
// Broadcast to all authenticated clients
std::lock_guard<std::mutex> lock(m_ClientsMutex);
for (const auto& [ws_ptr, client] : m_Clients) {
if (!client.token.empty()) {
SendText(ws_ptr, json);
}
}
}
bool CWebService::IsWebTriggered(uint64_t device_id) {
std::lock_guard<std::mutex> lock(m_WebTriggeredMutex);
return m_WebTriggeredDevices.find(device_id) != m_WebTriggeredDevices.end();
}
void CWebService::ClearWebTriggered(uint64_t device_id) {
std::lock_guard<std::mutex> lock(m_WebTriggeredMutex);
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;
// Build update message
Json::Value msg;
msg["cmd"] = "device_update";
msg["id"] = std::to_string(device_id);
msg["rtt"] = rtt;
msg["activeWindow"] = activeWindow;
Json::StreamWriterBuilder builder;
builder["indentation"] = "";
std::string json_str = Json::writeString(builder, msg);
// Broadcast to all authenticated clients
std::lock_guard<std::mutex> lock(m_ClientsMutex);
for (const auto& [ws_ptr, client] : m_Clients) {
if (!client.token.empty()) {
SendText(ws_ptr, json_str);
}
}
}