From 695193ab3c324852b6ada4571a1c679d7f9ca089 Mon Sep 17 00:00:00 2001 From: dijunkun Date: Mon, 24 Aug 2026 09:46:51 +0800 Subject: [PATCH] [feat] display client version and platform --- src/admin/admin_controller.cpp | 2 + src/admin/web/admin.css | 4 ++ src/admin/web/admin.js | 18 ++++++++- src/device_db_manager/device_db_manager.cpp | 44 +++++++++++++++++++- src/device_db_manager/device_db_manager.h | 5 +++ src/signal_negotiation.cpp | 45 +++++++++++++++++++++ src/signal_negotiation.h | 1 + src/signal_server.cpp | 3 ++ 8 files changed, 119 insertions(+), 3 deletions(-) diff --git a/src/admin/admin_controller.cpp b/src/admin/admin_controller.cpp index 428da77..1c71488 100644 --- a/src/admin/admin_controller.cpp +++ b/src/admin/admin_controller.cpp @@ -610,6 +610,8 @@ AdminHttpResponse AdminController::HandleOverview( devices.push_back({{"id", device.device_id}, {"online", device.online}, {"kind", ClientKind(device.device_id)}, + {"client_version", device.client_version}, + {"client_platform", device.client_platform}, {"updated_at", device.updated_at}, {"last_online_at", device.online ? 0 : device.updated_at}, diff --git a/src/admin/web/admin.css b/src/admin/web/admin.css index 6253360..3296ce0 100644 --- a/src/admin/web/admin.css +++ b/src/admin/web/admin.css @@ -82,11 +82,15 @@ .sessions-table td:nth-child(1), .sessions-table td:nth-child(2) { word-break: break-all; } .device-id { font-weight: 600; word-break: break-all; } .subline { display: block; color: #667085; font-size: 12px; margin-top: 3px; } + .client-meta { align-items: center; display: flex; flex-wrap: wrap; gap: 6px; margin-top: 3px; } + .client-meta .subline { margin-top: 0; } .badge { border-radius: 999px; display: inline-flex; align-items: center; justify-content: center; font-size: 12px; font-weight: 700; line-height: 1; padding: 5px 8px; white-space: nowrap; } .badge.online { background: #dcfae6; color: #067647; } .badge.offline { background: #f2f4f7; color: #475467; } .badge.active { background: #fef0c7; color: #b54708; } .badge.web { background: #e0f2fe; color: #026aa2; } + .badge.platform { background: #ecfdf3; color: #027a48; } + .badge.version { background: #eef4ff; color: #3538cd; } .presence-table td:nth-child(2) { display: grid; gap: 4px; justify-items: start; } .presence-table td:nth-child(2) .badge { min-width: 58px; } .details-row td { background: #f9fafb; } diff --git a/src/admin/web/admin.js b/src/admin/web/admin.js index 97bdeac..4adf5d8 100644 --- a/src/admin/web/admin.js +++ b/src/admin/web/admin.js @@ -559,6 +559,13 @@ return Number(activeCount) > 0 ? formatDuration(value) : '-'; } + function platformLabel(platform) { + if (platform === 'windows') return 'Windows'; + if (platform === 'macos') return 'macOS'; + if (platform === 'linux') return 'Linux'; + return platform || ''; + } + function appendDetailItem(parent, label, value, className, dataset) { const item = document.createElement('div'); appendText(item, 'span', label); @@ -590,7 +597,16 @@ const clientCell = document.createElement('td'); labelCell(clientCell, 'Client'); appendText(clientCell, 'div', device.id, 'device-id'); - appendText(clientCell, 'span', device.kind === 'web' ? 'web client' : 'PC client', 'subline'); + const clientMeta = document.createElement('div'); + clientMeta.className = 'client-meta'; + appendText(clientMeta, 'span', device.kind === 'web' ? 'web client' : 'PC client', 'subline'); + if (device.kind !== 'web' && device.client_platform) { + appendBadge(clientMeta, platformLabel(device.client_platform), 'platform'); + } + if (device.kind !== 'web' && device.client_version) { + appendBadge(clientMeta, device.client_version, 'version'); + } + clientCell.appendChild(clientMeta); row.appendChild(clientCell); const statusCell = document.createElement('td'); diff --git a/src/device_db_manager/device_db_manager.cpp b/src/device_db_manager/device_db_manager.cpp index e06f0a8..c5bb8d2 100644 --- a/src/device_db_manager/device_db_manager.cpp +++ b/src/device_db_manager/device_db_manager.cpp @@ -426,6 +426,8 @@ void DeviceDBManager::InitDB() { EnsureTextColumn(db_, "device_presence", "geo_region"); EnsureTextColumn(db_, "device_presence", "geo_city"); EnsureTextColumn(db_, "device_presence", "geo_location"); + EnsureTextColumn(db_, "device_presence", "client_version"); + EnsureTextColumn(db_, "device_presence", "client_platform"); const char* sql_presence_backfill = "UPDATE device_presence SET online_since = updated_at " @@ -1040,6 +1042,38 @@ bool DeviceDBManager::UpdateDeviceNetworkInfo( return ok; } +bool DeviceDBManager::UpdateDeviceClientInfo( + const std::string& device_id, const std::string& client_version, + const std::string& client_platform) { + std::lock_guard lock(db_mutex_); + if (db_ == nullptr) { + LOG_ERROR("Database is not initialized in UpdateDeviceClientInfo."); + return false; + } + if (device_id.empty()) { + return false; + } + + const char* sql = + "INSERT INTO device_presence " + "(device_id, online, updated_at, client_version, client_platform) " + "VALUES (?, 0, CAST(strftime('%s','now') AS INTEGER), ?, ?) " + "ON CONFLICT(device_id) DO UPDATE SET " + "client_version=excluded.client_version, " + "client_platform=excluded.client_platform;"; + + sqlite3_stmt* stmt = nullptr; + if (sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr) != SQLITE_OK) { + return false; + } + sqlite3_bind_text(stmt, 1, device_id.c_str(), -1, SQLITE_TRANSIENT); + sqlite3_bind_text(stmt, 2, client_version.c_str(), -1, SQLITE_TRANSIENT); + sqlite3_bind_text(stmt, 3, client_platform.c_str(), -1, SQLITE_TRANSIENT); + const bool ok = sqlite3_step(stmt) == SQLITE_DONE; + sqlite3_finalize(stmt); + return ok; +} + bool DeviceDBManager::StartRemoteControlSession( const std::string& transmission_id, const std::string& host_id, const std::string& guest_id) { @@ -1668,7 +1702,8 @@ std::vector DeviceDBManager::ListOnlineDevices( "FROM remote_control_sessions " "WHERE normalized_host_id = device_presence.device_id), 0) " "AS total_controlled_seconds, " - "client_ip, geo_country, geo_region, geo_city, geo_location " + "client_ip, geo_country, geo_region, geo_city, geo_location, " + "client_version, client_platform " "FROM device_presence " "WHERE online = 1 " "AND device_id NOT LIKE 'web-%' " @@ -1707,6 +1742,8 @@ std::vector DeviceDBManager::ListOnlineDevices( info.region = ColumnText(stmt, 10); info.city = ColumnText(stmt, 11); info.location = ColumnText(stmt, 12); + info.client_version = ColumnText(stmt, 13); + info.client_platform = ColumnText(stmt, 14); result.push_back(info); } sqlite3_finalize(stmt); @@ -1771,7 +1808,8 @@ std::vector DeviceDBManager::ListDevicePresence( "FROM remote_control_sessions " "WHERE normalized_host_id = device_presence.device_id), '') " "AS active_controlled_by, " - "client_ip, geo_country, geo_region, geo_city, geo_location " + "client_ip, geo_country, geo_region, geo_city, geo_location, " + "client_version, client_platform " "FROM device_presence " "WHERE " + DevicePresenceFilterClause(filter, kind); @@ -1816,6 +1854,8 @@ std::vector DeviceDBManager::ListDevicePresence( info.region = ColumnText(stmt, 16); info.city = ColumnText(stmt, 17); info.location = ColumnText(stmt, 18); + info.client_version = ColumnText(stmt, 19); + info.client_platform = ColumnText(stmt, 20); result.push_back(info); } sqlite3_finalize(stmt); diff --git a/src/device_db_manager/device_db_manager.h b/src/device_db_manager/device_db_manager.h index f1b9f6a..e8563bd 100644 --- a/src/device_db_manager/device_db_manager.h +++ b/src/device_db_manager/device_db_manager.h @@ -42,6 +42,8 @@ struct OnlineDeviceInfo { std::string region; std::string city; std::string location; + std::string client_version; + std::string client_platform; }; struct ClientNetworkInfo { @@ -113,6 +115,9 @@ class DeviceDBManager { bool SetDeviceOnline(const std::string& device_id, bool online); bool UpdateDeviceNetworkInfo(const std::string& device_id, const ClientNetworkInfo& network_info); + bool UpdateDeviceClientInfo(const std::string& device_id, + const std::string& client_version, + const std::string& client_platform); bool RecordRuntimeHeartbeat(); bool StartRemoteControlSession(const std::string& transmission_id, const std::string& host_id, diff --git a/src/signal_negotiation.cpp b/src/signal_negotiation.cpp index 33db2be..2b3563c 100644 --- a/src/signal_negotiation.cpp +++ b/src/signal_negotiation.cpp @@ -18,6 +18,11 @@ bool GetStringField(const json& j, const char* key, std::string& value) { return true; } +bool ShouldTrackClientInfo(const std::string& user_id) { + return !user_id.empty() && user_id.rfind("web-", 0) != 0 && + user_id.rfind("C-", 0) != 0; +} + std::string PasswordFingerprint(const std::string& password) { unsigned char digest[SHA256_DIGEST_LENGTH]; SHA256(reinterpret_cast(password.data()), @@ -121,6 +126,10 @@ bool SignalNegotiation::login_user(websocketpp::connection_hdl hdl, transmission_manager_->BindHostToTransmission(ret_host_id, ret_host_id); if (success) { + if (ShouldTrackClientInfo(ret_host_id) && + !device_db_manager_->UpdateDeviceClientInfo(ret_host_id, "", "")) { + LOG_WARN("Failed to clear client information for [{}]", ret_host_id); + } json message = {{"type", "login"}, {"user_id", return_host_id}, {"status", "success"}}; @@ -151,6 +160,42 @@ bool SignalNegotiation::login_user(websocketpp::connection_hdl hdl, return true; } +bool SignalNegotiation::client_info(websocketpp::connection_hdl hdl, + const json& j) { + std::string version; + std::string platform; + if (!GetStringField(j, "version", version) || version.empty() || + version.size() > 64) { + LOG_ERROR("client_info missing or invalid field: version"); + return false; + } + if (!GetStringField(j, "platform", platform) || platform.empty() || + platform.size() > 32) { + LOG_ERROR("client_info missing or invalid field: platform"); + return false; + } + + const std::string user_id = transmission_manager_->GetUserId(hdl); + if (user_id.empty()) { + LOG_WARN("Ignore client_info from unauthenticated connection"); + return false; + } + + if (!ShouldTrackClientInfo(user_id)) { + return true; + } + + if (!device_db_manager_->UpdateDeviceClientInfo(user_id, version, + platform)) { + LOG_ERROR("Failed to store client information for [{}]", user_id); + return false; + } + + LOG_INFO("Client [{}] reports version [{}] on [{}]", user_id, version, + platform); + return true; +} + bool SignalNegotiation::leave_transmission(websocketpp::connection_hdl hdl, const json& j) { std::string transmission_id; diff --git a/src/signal_negotiation.h b/src/signal_negotiation.h index 6aee425..7659887 100644 --- a/src/signal_negotiation.h +++ b/src/signal_negotiation.h @@ -42,6 +42,7 @@ class SignalNegotiation { bool new_candidate_mid(websocketpp::connection_hdl hdl, const json& j); bool change_password(websocketpp::connection_hdl hdl, const json& j); bool turn_credentials(websocketpp::connection_hdl hdl, const json& j); + bool client_info(websocketpp::connection_hdl hdl, const json& j); void OnWebClientDisconnect(const std::string& user_id); private: diff --git a/src/signal_server.cpp b/src/signal_server.cpp index a0b826c..39e8e12 100644 --- a/src/signal_server.cpp +++ b/src/signal_server.cpp @@ -852,6 +852,9 @@ void SignalServer::OnMessage(websocketpp::connection_hdl hdl, case "turn_credentials"_H: signal_negotiation_->turn_credentials(hdl, j); break; + case "client_info"_H: + signal_negotiation_->client_info(hdl, j); + break; case "recent_connections_presence"_H: { std::string user_id; if (!j.contains("user_id") || !j["user_id"].is_string()) {