From 655b1934a433696ad34700d7729f5d637b11b956 Mon Sep 17 00:00:00 2001 From: yuanyuanxiang <962914132@qq.com> Date: Fri, 24 Apr 2026 12:19:15 +0200 Subject: [PATCH] Feature: Implement user management feature with role support --- server/2015Remote/WebPage.h | 271 ++++++++++++++++++++++++- server/2015Remote/WebService.cpp | 333 +++++++++++++++++++++++++++++++ server/2015Remote/WebService.h | 16 ++ 3 files changed, 617 insertions(+), 3 deletions(-) diff --git a/server/2015Remote/WebPage.h b/server/2015Remote/WebPage.h index ded4f56..014760a 100644 --- a/server/2015Remote/WebPage.h +++ b/server/2015Remote/WebPage.h @@ -275,6 +275,118 @@ inline std::string GetWebPageHTML() { margin-left: 10px; } .logout-btn:hover { transform: translateY(-1px); box-shadow: 0 4px 12px rgba(192, 57, 43, 0.4); } + .users-btn { + padding: 10px 20px; + border: none; + border-radius: 8px; + background: linear-gradient(135deg, #9b59b6 0%, #8e44ad 100%); + color: #fff; + font-size: 14px; + font-weight: 500; + cursor: pointer; + transition: all 0.3s; + margin-left: 10px; + display: none; + } + .users-btn.visible { display: inline-block; } + .users-btn:hover { transform: translateY(-1px); box-shadow: 0 4px 12px rgba(142, 68, 173, 0.4); } + /* User Management Modal */ + .modal-overlay { + display: none; + position: fixed; + top: 0; left: 0; right: 0; bottom: 0; + background: rgba(0,0,0,0.7); + z-index: 1000; + align-items: center; + justify-content: center; + } + .modal-overlay.active { display: flex; } + .modal-content { + background: rgba(22, 33, 62, 0.98); + border-radius: 16px; + padding: 24px; + width: 90%; + max-width: 500px; + max-height: 80vh; + overflow-y: auto; + box-shadow: 0 8px 32px rgba(0,0,0,0.5); + border: 1px solid rgba(233, 69, 96, 0.2); + } + .modal-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 20px; + padding-bottom: 16px; + border-bottom: 1px solid rgba(255,255,255,0.1); + } + .modal-header h3 { color: #e94560; margin: 0; } + .modal-close { + background: none; + border: none; + color: #888; + font-size: 24px; + cursor: pointer; + padding: 0; + line-height: 1; + } + .modal-close:hover { color: #e94560; } + .user-form { margin-bottom: 24px; } + .user-form h4 { color: #ccc; margin-bottom: 12px; font-size: 14px; } + .user-form input, .user-form select { + width: 100%; + padding: 10px 12px; + margin-bottom: 12px; + border: 1px solid rgba(255,255,255,0.1); + border-radius: 8px; + background: rgba(15, 52, 96, 0.8); + color: #fff; + font-size: 14px; + } + .user-form input:focus, .user-form select:focus { + outline: none; + border-color: #e94560; + } + .user-form button { + width: 100%; + padding: 12px; + border: none; + border-radius: 8px; + background: linear-gradient(135deg, #27ae60 0%, #2ecc71 100%); + color: #fff; + font-weight: 600; + cursor: pointer; + transition: all 0.3s; + } + .user-form button:hover { transform: translateY(-1px); } + .user-list h4 { color: #ccc; margin-bottom: 12px; font-size: 14px; } + .user-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 12px; + background: rgba(15, 52, 96, 0.5); + border-radius: 8px; + margin-bottom: 8px; + } + .user-item .user-info { flex: 1; } + .user-item .username { color: #fff; font-weight: 500; } + .user-item .role { color: #888; font-size: 12px; } + .user-item .role.admin { color: #e94560; } + .user-item .delete-btn { + background: rgba(231, 76, 60, 0.2); + border: 1px solid rgba(231, 76, 60, 0.5); + color: #e74c3c; + padding: 6px 12px; + border-radius: 6px; + cursor: pointer; + font-size: 12px; + } + .user-item .delete-btn:hover { background: rgba(231, 76, 60, 0.4); } + .user-item .delete-btn:disabled { opacity: 0.5; cursor: not-allowed; } + .user-msg { padding: 10px; border-radius: 6px; margin-bottom: 12px; font-size: 13px; } + .user-msg.success { background: rgba(39, 174, 96, 0.2); color: #2ecc71; } + .user-msg.error { background: rgba(231, 76, 60, 0.2); color: #e74c3c; } )HTML"; // Part 3: Device card styles @@ -814,6 +926,7 @@ inline std::string GetWebPageHTML() { + @@ -871,6 +984,31 @@ inline std::string GetWebPageHTML() {
100%
+ + + )HTML"; // Part 7: JavaScript - State and WebSocket @@ -1030,11 +1168,28 @@ inline std::string GetWebPageHTML() { challengeNonce = msg.nonce || ''; console.log('Received challenge nonce'); break; + case 'salt': + if (msg.ok) { + completeLogin(msg.salt || ''); + } else { + pendingLogin = null; // Clear pending state on error + document.getElementById('login-error').textContent = msg.msg || 'Failed to get salt'; + } + break; case 'login_result': if (msg.ok) { token = msg.token; + currentUserRole = msg.role || 'viewer'; sessionStorage.setItem('token', token); + sessionStorage.setItem('role', currentUserRole); document.getElementById('login-error').textContent = ''; + // Show Users button for admin only + const usersBtn = document.getElementById('users-btn'); + if (currentUserRole === 'admin') { + usersBtn.classList.add('visible'); + } else { + usersBtn.classList.remove('visible'); + } showPage('devices-page'); getDevices(); } else { @@ -1096,6 +1251,29 @@ inline std::string GetWebPageHTML() { getDevices(); } break; + case 'create_user_result': + if (msg.ok) { + showUserMsg('User created successfully', false); + document.getElementById('new-username').value = ''; + document.getElementById('new-password').value = ''; + listUsers(); + } else { + showUserMsg(msg.msg || 'Failed to create user', true); + } + break; + case 'delete_user_result': + if (msg.ok) { + showUserMsg('User deleted', false); + listUsers(); + } else { + showUserMsg(msg.msg || 'Failed to delete user', true); + } + break; + case 'list_users_result': + if (msg.ok) { + renderUsersList(msg.users); + } + break; } } )HTML"; @@ -1383,6 +1561,9 @@ inline std::string GetWebPageHTML() { } } + // Pending login state for salt-based auth + let pendingLogin = null; + async function login() { const username = document.getElementById('username').value; const password = document.getElementById('password').value; @@ -1390,8 +1571,18 @@ inline std::string GetWebPageHTML() { if (!ws || ws.readyState !== WebSocket.OPEN) { document.getElementById('login-error').textContent = 'Not connected'; return; } if (!challengeNonce) { document.getElementById('login-error').textContent = 'No challenge received'; return; } - // Compute password hash (same as server stores) - passwordHash = await sha256(password); + // Store pending login info and request salt first + pendingLogin = { username, password }; + ws.send(JSON.stringify({ cmd: 'get_salt', username })); + } + + async function completeLogin(salt) { + if (!pendingLogin) return; + const { username, password } = pendingLogin; + pendingLogin = null; + + // Compute password hash with salt: SHA256(password + salt) + passwordHash = await sha256(password + salt); // Compute response: SHA256(passwordHash + nonce) const response = await sha256(passwordHash + challengeNonce); ws.send(JSON.stringify({ cmd: 'login', username, response, nonce: challengeNonce })); @@ -1409,6 +1600,75 @@ inline std::string GetWebPageHTML() { sessionStorage.removeItem('token'); devices = []; showPage('login-page'); + // Hide users button + document.getElementById('users-btn').classList.remove('visible'); + } + + // User Management Functions + let currentUserRole = 'viewer'; + + function openUsersModal() { + document.getElementById('users-modal').classList.add('active'); + document.getElementById('user-msg').innerHTML = ''; + listUsers(); + } + + function closeUsersModal() { + document.getElementById('users-modal').classList.remove('active'); + } + + function showUserMsg(msg, isError) { + const el = document.getElementById('user-msg'); + el.className = 'user-msg ' + (isError ? 'error' : 'success'); + el.textContent = msg; + setTimeout(() => { el.innerHTML = ''; }, 3000); + } + + function createUser() { + const username = document.getElementById('new-username').value.trim(); + const password = document.getElementById('new-password').value; + const role = document.getElementById('new-role').value; + + if (!username || !password) { + showUserMsg('Username and password are required', true); + return; + } + + if (ws && ws.readyState === WebSocket.OPEN && token) { + ws.send(JSON.stringify({ cmd: 'create_user', token, username, password, role })); + } + } + + function deleteUser(username) { + if (!confirm('Delete user "' + username + '"?')) return; + if (ws && ws.readyState === WebSocket.OPEN && token) { + ws.send(JSON.stringify({ cmd: 'delete_user', token, username })); + } + } + + function listUsers() { + if (ws && ws.readyState === WebSocket.OPEN && token) { + ws.send(JSON.stringify({ cmd: 'list_users', token })); + } + } + + function renderUsersList(users) { + const container = document.getElementById('users-list'); + if (!users || users.length === 0) { + container.innerHTML = '
No users
'; + return; + } + container.innerHTML = users.map(u => { + const isAdmin = u.role === 'admin'; + const canDelete = u.username !== 'admin'; // Cannot delete built-in admin + return '
' + + '
' + + '
' + escapeHtml(u.username) + '
' + + '
' + u.role + '
' + + '
' + + (canDelete ? '' : '') + + '
'; + }).join(''); } function getDevices() { @@ -2642,8 +2902,13 @@ inline std::string GetWebPageHTML() { bindKeyboardBtnEvents(document.getElementById('qc-keyboard')); bindKeyboardBtnEvents(document.getElementById('btn-keyboard')); bindKeyboardBtnEvents(document.getElementById('btn-keyboard-bar')); - // Restore token from sessionStorage + // Restore token and role from sessionStorage token = sessionStorage.getItem('token'); + currentUserRole = sessionStorage.getItem('role') || 'viewer'; + // Show Users button for admin only (will be updated after login verification) + if (token && currentUserRole === 'admin') { + document.getElementById('users-btn').classList.add('visible'); + } connectWebSocket(); }; diff --git a/server/2015Remote/WebService.cpp b/server/2015Remote/WebService.cpp index c9de53f..89e3ecc 100644 --- a/server/2015Remote/WebService.cpp +++ b/server/2015Remote/WebService.cpp @@ -9,6 +9,8 @@ #include "SimpleWebSocket.h" #include "common/commands.h" #include +#include +#include // Algorithm constants (same as ScreenSpyDlg.cpp) #define ALGORITHM_H264 2 @@ -20,6 +22,16 @@ static std::map s_ClientNonces; static std::mutex s_NonceMutex; static std::atomic 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 dis; + char buf[17]; + snprintf(buf, sizeof(buf), "%016llX", dis(gen)); + return std::string(buf); +} + // Generate random nonce (32 hex chars) - thread-safe static std::string GenerateNonce() { if (s_bShuttingDown) return ""; @@ -112,11 +124,22 @@ CWebService::CWebService() m_PayloadsDir = (exeDir / "Payloads").string(); std::error_code ec; std::filesystem::create_directories(m_PayloadsDir, ec); + + // Initialize config directory (same as YAMA.db location) + char appdata_path[MAX_PATH]; + if (SUCCEEDED(SHGetFolderPathA(NULL, CSIDL_APPDATA, NULL, 0, appdata_path))) { + m_ConfigDir = std::string(appdata_path) + "\\" BRAND_DATA_FOLDER "\\"; + } else { + m_ConfigDir = ".\\"; + } + std::filesystem::create_directories(m_ConfigDir, ec); } void CWebService::SetAdminPassword(const std::string& password) { + std::lock_guard lock(m_UsersMutex); m_Users.clear(); + // Admin user is built-in, always first WebUser admin; admin.username = "admin"; admin.salt = ""; // Not used with challenge-response auth @@ -125,6 +148,9 @@ void CWebService::SetAdminPassword(const std::string& password) { m_Users.push_back(admin); Mprintf("[WebService] Admin password configured\n"); + + // Load additional users from file (non-admin users) + LoadUsers(); } CWebService::~CWebService() { @@ -329,6 +355,14 @@ void CWebService::ServerThread(int port) { HandleKey(ws_ptr, msg); } else if (cmd == "rdp_reset") { HandleRdpReset(ws_ptr, token); + } else if (cmd == "get_salt") { + HandleGetSalt(ws_ptr, msg); + } else if (cmd == "create_user") { + HandleCreateUser(ws_ptr, msg); + } else if (cmd == "delete_user") { + HandleDeleteUser(ws_ptr, msg); + } else if (cmd == "list_users") { + HandleListUsers(ws_ptr, token); } } }); @@ -480,6 +514,51 @@ void CWebService::HandleLogin(void* ws_ptr, const std::string& msg, const std::s SendText(ws_ptr, Json::writeString(builder, res)); } +void CWebService::HandleGetSalt(void* ws_ptr, const std::string& msg) { + Json::Value root; + Json::Reader reader; + if (!reader.parse(msg, root)) { + SendText(ws_ptr, BuildJsonResponse("salt", false, "Invalid JSON")); + return; + } + + std::string username = root.get("username", "").asString(); + if (username.empty()) { + SendText(ws_ptr, BuildJsonResponse("salt", false, "Username required")); + return; + } + + // Find user and get salt + std::string salt = ""; + bool userFound = false; + { + std::lock_guard 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)) { @@ -837,6 +916,111 @@ void CWebService::HandleRdpReset(void* ws_ptr, const std::string& token) { } } +////////////////////////////////////////////////////////////////////////// +// User Management Handlers +////////////////////////////////////////////////////////////////////////// + +void CWebService::HandleCreateUser(void* ws_ptr, const std::string& msg) { + Json::Value root; + Json::Reader reader; + if (!reader.parse(msg, root)) { + SendText(ws_ptr, BuildJsonResponse("create_user_result", false, "Invalid JSON")); + return; + } + + std::string token = root.get("token", "").asString(); + std::string username, role; + if (!ValidateToken(token, username, role)) { + SendText(ws_ptr, BuildJsonResponse("create_user_result", false, "Invalid token")); + return; + } + + // Only admin can create users + if (role != "admin") { + SendText(ws_ptr, BuildJsonResponse("create_user_result", false, "Permission denied")); + return; + } + + std::string newUsername = root.get("username", "").asString(); + std::string newPassword = root.get("password", "").asString(); + std::string newRole = root.get("role", "viewer").asString(); + + if (newUsername.empty() || newPassword.empty()) { + SendText(ws_ptr, BuildJsonResponse("create_user_result", false, "Username and password required")); + return; + } + + if (CreateUser(newUsername, newPassword, newRole)) { + SendText(ws_ptr, BuildJsonResponse("create_user_result", true)); + } else { + SendText(ws_ptr, BuildJsonResponse("create_user_result", false, "Failed to create user (may already exist)")); + } +} + +void CWebService::HandleDeleteUser(void* ws_ptr, const std::string& msg) { + Json::Value root; + Json::Reader reader; + if (!reader.parse(msg, root)) { + SendText(ws_ptr, BuildJsonResponse("delete_user_result", false, "Invalid JSON")); + return; + } + + std::string token = root.get("token", "").asString(); + std::string username, role; + if (!ValidateToken(token, username, role)) { + SendText(ws_ptr, BuildJsonResponse("delete_user_result", false, "Invalid token")); + return; + } + + // Only admin can delete users + if (role != "admin") { + SendText(ws_ptr, BuildJsonResponse("delete_user_result", false, "Permission denied")); + return; + } + + std::string targetUsername = root.get("username", "").asString(); + + if (DeleteUser(targetUsername)) { + SendText(ws_ptr, BuildJsonResponse("delete_user_result", true)); + } else { + SendText(ws_ptr, BuildJsonResponse("delete_user_result", false, "Failed to delete user")); + } +} + +void CWebService::HandleListUsers(void* ws_ptr, const std::string& token) { + std::string username, role; + if (!ValidateToken(token, username, role)) { + SendText(ws_ptr, BuildJsonResponse("list_users_result", false, "Invalid token")); + return; + } + + // Only admin can list users + if (role != "admin") { + SendText(ws_ptr, BuildJsonResponse("list_users_result", false, "Permission denied")); + return; + } + + auto users = ListUsers(); + + Json::Value res; + res["cmd"] = "list_users_result"; + res["ok"] = true; + + Json::Value usersArray(Json::arrayValue); + for (const auto& u : users) { + Json::Value user; + user["username"] = u.first; + user["role"] = u.second; + usersArray.append(user); + } + res["users"] = usersArray; + + Json::StreamWriterBuilder builder; + builder["indentation"] = ""; + std::string json = Json::writeString(builder, res); + SendText(ws_ptr, json); +} + ////////////////////////////////////////////////////////////////////////// // Token Management (delegated to WebServiceAuth module) ////////////////////////////////////////////////////////////////////////// @@ -936,6 +1120,155 @@ std::string CWebService::ComputeHash(const std::string& input) { return WSAuth::ComputeSHA256(input); } +////////////////////////////////////////////////////////////////////////// +// User Management +////////////////////////////////////////////////////////////////////////// + +std::string CWebService::GetUsersFilePath() { + return m_ConfigDir + "users.json"; +} + +void CWebService::LoadUsers() { + // Note: m_UsersMutex should already be held by caller (SetAdminPassword) + // Load additional users from users.json (admin user is already in m_Users) + + std::string path = GetUsersFilePath(); + std::ifstream file(path); + if (!file.is_open()) { + Mprintf("[WebService] No users.json found, using admin only\n"); + return; + } + + try { + Json::Value root; + Json::CharReaderBuilder builder; + std::string errors; + if (!Json::parseFromStream(builder, file, &root, &errors)) { + Mprintf("[WebService] Failed to parse users.json: %s\n", errors.c_str()); + return; + } + + const Json::Value& users = root["users"]; + int loaded = 0; + for (const auto& u : users) { + std::string username = u.get("username", "").asString(); + // Skip admin user (it's built-in with master password) + if (username.empty() || username == "admin") continue; + + WebUser user; + user.username = username; + user.password_hash = u.get("password_hash", "").asString(); + user.salt = u.get("salt", "").asString(); + user.role = u.get("role", "viewer").asString(); + + if (!user.password_hash.empty()) { + m_Users.push_back(user); + loaded++; + } + } + Mprintf("[WebService] Loaded %d additional users from users.json\n", loaded); + } catch (const std::exception& e) { + Mprintf("[WebService] Error loading users.json: %s\n", e.what()); + } +} + +void CWebService::SaveUsers() { + // Save non-admin users to users.json + std::lock_guard lock(m_UsersMutex); + + Json::Value root; + Json::Value users(Json::arrayValue); + + for (const auto& u : m_Users) { + // Skip admin user (it uses master password, not stored in file) + if (u.username == "admin") continue; + + Json::Value user; + user["username"] = u.username; + user["password_hash"] = u.password_hash; + user["salt"] = u.salt; + user["role"] = u.role; + users.append(user); + } + + root["users"] = users; + + std::string path = GetUsersFilePath(); + std::ofstream file(path); + if (!file.is_open()) { + Mprintf("[WebService] Failed to open users.json for writing\n"); + return; + } + + Json::StreamWriterBuilder builder; + builder["indentation"] = " "; + std::unique_ptr writer(builder.newStreamWriter()); + writer->write(root, &file); + + Mprintf("[WebService] Saved %d users to users.json\n", (int)users.size()); +} + +bool CWebService::CreateUser(const std::string& username, const std::string& password, const std::string& role) { + if (username.empty() || password.empty()) return false; + if (username == "admin") return false; // Cannot create user named "admin" + if (role != "admin" && role != "viewer") return false; + + { + std::lock_guard lock(m_UsersMutex); + + // Check if user already exists + for (const auto& u : m_Users) { + if (u.username == username) return false; + } + + // Generate salt and hash password with salt + WebUser user; + user.username = username; + user.salt = GenerateSalt(); + user.password_hash = WSAuth::ComputeSHA256(password + user.salt); + user.role = role; + + m_Users.push_back(user); + Mprintf("[WebService] Created user: %s (role: %s)\n", username.c_str(), role.c_str()); + } + + // Save to file (outside lock scope since SaveUsers acquires its own lock) + SaveUsers(); + return true; +} + +bool CWebService::DeleteUser(const std::string& username) { + if (username.empty() || username == "admin") return false; + + bool deleted = false; + { + std::lock_guard 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> CWebService::ListUsers() { + std::lock_guard lock(m_UsersMutex); + std::vector> result; + for (const auto& u : m_Users) { + result.push_back({u.username, u.role}); + } + return result; +} + ////////////////////////////////////////////////////////////////////////// // JSON Helpers ////////////////////////////////////////////////////////////////////////// diff --git a/server/2015Remote/WebService.h b/server/2015Remote/WebService.h index fc5509d..c558bb0 100644 --- a/server/2015Remote/WebService.h +++ b/server/2015Remote/WebService.h @@ -78,6 +78,11 @@ public: // Set admin password (use master password) void SetAdminPassword(const std::string& password); + // User management + bool CreateUser(const std::string& username, const std::string& password, const std::string& role); + bool DeleteUser(const std::string& username); + std::vector> ListUsers(); // Returns [(username, role), ...] + // Device management (called from main app) void MarkDeviceOnline(uint64_t device_id); void MarkDeviceOffline(uint64_t device_id); @@ -111,6 +116,7 @@ private: // Signaling handlers void HandleLogin(void* ws_ptr, const std::string& msg, const std::string& client_ip); + void HandleGetSalt(void* ws_ptr, const std::string& msg); void HandleGetDevices(void* ws_ptr, const std::string& token); void HandleConnect(void* ws_ptr, const std::string& token, uint64_t device_id); void HandleDisconnect(void* ws_ptr, const std::string& token, uint64_t requested_device_id = 0); @@ -141,6 +147,14 @@ private: bool VerifyPassword(const std::string& input, const WebUser& user); std::string ComputeHash(const std::string& input); + // User management helpers + std::string GetUsersFilePath(); + void LoadUsers(); + void SaveUsers(); + void HandleCreateUser(void* ws_ptr, const std::string& msg); + void HandleDeleteUser(void* ws_ptr, const std::string& msg); + void HandleListUsers(void* ws_ptr, const std::string& token); + // Send to WebSocket void SendText(void* ws_ptr, const std::string& text); void SendBinary(void* ws_ptr, const uint8_t* data, size_t len); @@ -181,6 +195,7 @@ private: // User accounts (loaded from config) std::vector m_Users; + std::mutex m_UsersMutex; // Token secret key (generated on startup) std::string m_SecretKey; @@ -190,6 +205,7 @@ private: int m_nTokenExpireSeconds; bool m_bHideWebSessions; // Whether to hide web-triggered dialogs (default: true) std::string m_PayloadsDir; // Directory for file downloads (Payloads/) + std::string m_ConfigDir; // Directory for config files (users.json, etc.) // Web-triggered sessions (should be hidden) std::set m_WebTriggeredDevices;