diff --git a/.gitmodules b/.gitmodules index cad5101..d350a11 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,7 +1,7 @@ [submodule "kasmweb"] path = kasmweb url = https://github.com/kasmtech/noVNC.git - branch = release/1.2.3 + branch = feature/VNC-196-shared-session-connect-msg [submodule "kasmvnc-functional-tests"] path = kasmvnc-functional-tests url = git@gitlab.com:kasm-technologies/internal/kasmvnc-functional-tests.git diff --git a/common/network/GetAPI.h b/common/network/GetAPI.h index 5b6c7ed..5a6a335 100644 --- a/common/network/GetAPI.h +++ b/common/network/GetAPI.h @@ -28,6 +28,7 @@ #include #include #include +#include namespace network { @@ -49,6 +50,8 @@ namespace network { uint32_t ping); void mainUpdateUserInfo(const uint8_t ownerConn, const uint8_t numUsers); + void mainUpdateSessionsInfo(std::string newSessionsInfo); + // from network threads uint8_t *netGetScreenshot(uint16_t w, uint16_t h, const uint8_t q, const bool dedup, @@ -61,6 +64,8 @@ namespace network { const bool read, const bool write, const bool owner); uint8_t netAddOrUpdateUser(const struct kasmpasswd_entry_t *entry); void netGetUsers(const char **ptr); + + const std::string_view netGetSessions(); void netGetBottleneckStats(char *buf, uint32_t len); void netGetFrameStats(char *buf, uint32_t len); void netResetFrameStatsCall(); @@ -142,6 +147,8 @@ namespace network { uint8_t ownerConnected; uint8_t activeUsers; pthread_mutex_t userInfoMutex; + std::mutex sessionInfoMutex; + std::string sessionsInfo; }; } diff --git a/common/network/GetAPIMessager.cxx b/common/network/GetAPIMessager.cxx index dfc739e..b11c878 100644 --- a/common/network/GetAPIMessager.cxx +++ b/common/network/GetAPIMessager.cxx @@ -28,6 +28,8 @@ #include #include #include +#include +#include using namespace network; using namespace rfb; @@ -55,7 +57,8 @@ static const struct TightJPEGConfiguration conf[10] = { GetAPIMessager::GetAPIMessager(const char *passwdfile_): passwdfile(passwdfile_), screenW(0), screenH(0), screenHash(0), cachedW(0), cachedH(0), cachedQ(0), - ownerConnected(0), activeUsers(0) { + ownerConnected(0), activeUsers(0), + sessionsInfo( "{\"users\":[]}"){ pthread_mutex_init(&screenMutex, NULL); pthread_mutex_init(&userMutex, NULL); @@ -171,6 +174,15 @@ void GetAPIMessager::mainUpdateUserInfo(const uint8_t ownerConn, const uint8_t n pthread_mutex_unlock(&userInfoMutex); } +void GetAPIMessager::mainUpdateSessionsInfo(std::string newSessionsInfo) +{ + std::unique_lock lock (sessionInfoMutex,std::defer_lock); + if (!lock.try_lock()) + return; + sessionsInfo = std::move(newSessionsInfo); + lock.unlock(); +} + // from network threads uint8_t *GetAPIMessager::netGetScreenshot(uint16_t w, uint16_t h, const uint8_t q, const bool dedup, @@ -514,6 +526,12 @@ void GetAPIMessager::netGetUsers(const char **outptr) { *outptr = buf; } + +const std::string_view GetAPIMessager::netGetSessions() +{ + return sessionsInfo; +} + void GetAPIMessager::netGetBottleneckStats(char *buf, uint32_t len) { /* { @@ -819,3 +837,4 @@ void GetAPIMessager::netClearClipboard() { pthread_mutex_unlock(&userMutex); } + diff --git a/common/network/TcpSocket.cxx b/common/network/TcpSocket.cxx index db13ec7..9663724 100644 --- a/common/network/TcpSocket.cxx +++ b/common/network/TcpSocket.cxx @@ -551,6 +551,18 @@ static void clearClipboardCb(void *messager) msgr->netClearClipboard(); } +static void getSessionsCb(void *messager, char **ptr) +{ + GetAPIMessager *msgr = (GetAPIMessager *) messager; + std::string_view sessionInfoView = msgr->netGetSessions(); + //Since this data is being returned to a c function using char array + //memmoery needs to be freeded by calling function + char *sessionInfo = (char *) calloc(sessionInfoView.size() + 1, sizeof(char)); + memcpy(sessionInfo, sessionInfoView.data(), sessionInfoView.size()); + sessionInfo[sessionInfoView.size()] = '\0'; + *ptr = sessionInfo; +} + #if OPENSSL_VERSION_NUMBER < 0x1010000f static pthread_mutex_t *sslmutex; @@ -700,6 +712,7 @@ WebsocketListener::WebsocketListener(const struct sockaddr *listenaddr, settings.serverFrameStatsReadyCb = serverFrameStatsReadyCb; settings.clearClipboardCb = clearClipboardCb; + settings.getSessionsCb = getSessionsCb; openssl_threads(); diff --git a/common/network/websocket.c b/common/network/websocket.c index fe3eb84..9140671 100644 --- a/common/network/websocket.c +++ b/common/network/websocket.c @@ -1511,7 +1511,8 @@ static uint8_t ownerapi(ws_ctx_t *ws_ctx, const char *in, const char * const use handler_msg("Sent bottleneck stats to API caller\n"); ret = 1; - } else entry("/api/get_users") { + } else entry("/api/get_users") + { const char *ptr; settings.getUsersCb(settings.messager, &ptr); @@ -1530,6 +1531,26 @@ static uint8_t ownerapi(ws_ctx_t *ws_ctx, const char *in, const char * const use handler_msg("Sent user list to API caller\n"); ret = 1; + } else entry("/api/get_sessions") { + + char *sessionData; + settings.getSessionsCb(settings.messager, &sessionData); + + sprintf(buf, "HTTP/1.1 200 OK\r\n" + "Server: KasmVNC/4.0\r\n" + "Connection: close\r\n" + "Content-type: text/plain\r\n" + "Content-length: %lu\r\n" + "%s" + "\r\n", strlen(sessionData), extra_headers ? extra_headers : ""); + ws_send(ws_ctx, buf, strlen(buf)); + ws_send(ws_ctx, sessionData, strlen(sessionData)); + weblog(200, wsthread_handler_id, 0, origip, ip, user, 1, origpath, strlen(buf) + strlen(sessionData)); + + free((char *) sessionData); + + handler_msg("Sent session list to API caller\n"); + ret = 1; } else entry("/api/get_frame_stats") { char statbuf[4096], decname[1024]; unsigned waitfor; diff --git a/common/network/websocket.h b/common/network/websocket.h index e9e8153..b193a17 100644 --- a/common/network/websocket.h +++ b/common/network/websocket.h @@ -108,6 +108,8 @@ typedef struct { uint8_t (*serverFrameStatsReadyCb)(void *messager); void (*clearClipboardCb)(void *messager); + + void (*getSessionsCb)(void *messager, char **buf); } settings_t; #ifdef __cplusplus diff --git a/common/rfb/SMsgWriter.cxx b/common/rfb/SMsgWriter.cxx index 11c59c7..eebb394 100644 --- a/common/rfb/SMsgWriter.cxx +++ b/common/rfb/SMsgWriter.cxx @@ -18,6 +18,7 @@ * USA. */ #include +#include #include #include #include @@ -776,3 +777,17 @@ void SMsgWriter::writeUnixRelay(const char *name, const rdr::U8 *buf, const unsi endMsg(); } + +void SMsgWriter::writeUserJoinedSession(const std::string& username) +{ + startMsg(msgTypeUserAddedToSession); + os->writeString(username.c_str()); + endMsg(); +} + +void SMsgWriter::writeUserLeftSession(const std::string& username) +{ + startMsg(msgTypeUserRemovedFromSession); + os->writeString(username.c_str()); + endMsg(); +} diff --git a/common/rfb/SMsgWriter.h b/common/rfb/SMsgWriter.h index ba07c62..3a424bf 100644 --- a/common/rfb/SMsgWriter.h +++ b/common/rfb/SMsgWriter.h @@ -23,6 +23,7 @@ #ifndef __RFB_SMSGWRITER_H__ #define __RFB_SMSGWRITER_H__ +#include #include #include #include @@ -132,6 +133,9 @@ namespace rfb { void writeSubscribeUnixRelay(const bool success, const char *msg); void writeUnixRelay(const char *name, const rdr::U8 *buf, const unsigned len); + void writeUserJoinedSession(const std::string& username); + void writeUserLeftSession(const std::string& username); + protected: void startMsg(int type); void endMsg(); diff --git a/common/rfb/VNCSConnectionST.cxx b/common/rfb/VNCSConnectionST.cxx index 96b0077..cf3af0b 100644 --- a/common/rfb/VNCSConnectionST.cxx +++ b/common/rfb/VNCSConnectionST.cxx @@ -16,7 +16,7 @@ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, * USA. */ - + #include #include @@ -66,7 +66,7 @@ VNCSConnectionST::VNCSConnectionST(VNCServerST* server_, network::Socket *s, needsPermCheck(false), pointerEventTime(0), clientHasCursor(false), accessRights(AccessDefault), startTime(time(0)), frameTracking(false), - udpFramesSinceFull(0), complainedAboutNoViewRights(false) + udpFramesSinceFull(0), complainedAboutNoViewRights(false), clientUsername("username_unavailable") { setStreams(&sock->inStream(), &sock->outStream()); peerEndpoint.buf = sock->getPeerEndpoint(); @@ -177,6 +177,19 @@ void VNCSConnectionST::close(const char* reason) if (authenticated()) { server->lastDisconnectTime = time(0); + + // First update the client state to CLOSING to ensure it's not included in user lists + setState(RFBSTATE_CLOSING); + + // Notify other clients about the user leaving + server->notifyUserAction(this, clientUsername, VNCServerST::Leave); + vlog.info("Notifying other clients that user '%s' left: %s", + clientUsername.c_str(), reason ? reason : "connection closed"); + + if (server->apimessager) + { + server->updateSessionUsersList(); + } } try { @@ -190,11 +203,12 @@ void VNCSConnectionST::close(const char* reason) vlog.error("Failed to flush remaining socket data on close: %s", e.str()); } - // Just shutdown the socket and mark our state as closing. Eventually the - // calling code will call VNCServerST's removeSocket() method causing us to - // be deleted. + // Just shutdown the socket and mark our state as closing if not already done. + // Eventually the calling code will call VNCServerST's removeSocket() method + // causing us to be deleted. sock->shutdown(); - setState(RFBSTATE_CLOSING); + if (state() != RFBSTATE_CLOSING) + setState(RFBSTATE_CLOSING); } @@ -631,6 +645,8 @@ void VNCSConnectionST::approveConnectionOrClose(bool accept, void VNCSConnectionST::authSuccess() { lastEventTime = time(0); + connectionTime = time(0); // Record when the user connected + vlog.info("User %s connected at %ld", clientUsername.c_str(), connectionTime); server->startDesktop(); @@ -649,11 +665,27 @@ void VNCSConnectionST::authSuccess() // - Mark the entire display as "dirty" updates.add_changed(server->pb->getRect()); - startTime = time(0); + startTime = time(nullptr); + + if (clientUsername.empty()) + { + setUsername(get_default_name(sock->getPeerAddress())); + } + vlog.info("Authentication successful for user: %s", clientUsername.c_str()); } void VNCSConnectionST::queryConnection(const char* userName) { + if (userName && strlen(userName) > 0) { + setUsername(userName); + vlog.info("Setting username for connection: %s", userName); + } else { + // Generate a default username based on connection info + setUsername(get_default_name(sock->getPeerAddress())); + + vlog.info("Generated username: %s", clientUsername.c_str()); + } + // - Authentication succeeded - clear from blacklist CharArray name; name.buf = sock->getPeerAddress(); server->blHosts->clearBlackmark(name.buf); @@ -708,6 +740,15 @@ void VNCSConnectionST::clientInit(bool shared) } } SConnection::clientInit(shared); + if (shared && authenticated()) { + server->notifyUserAction(this, clientUsername, VNCServerST::Join); + vlog.info("Notifying other clients that user '%s' joined the shared session", + clientUsername.c_str()); + } + + if (server->apimessager && authenticated()) { + server->updateSessionUsersList(); + } } void VNCSConnectionST::setPixelFormat(const PixelFormat& pf) diff --git a/common/rfb/VNCSConnectionST.h b/common/rfb/VNCSConnectionST.h index 9416b2d..6d8068c 100644 --- a/common/rfb/VNCSConnectionST.h +++ b/common/rfb/VNCSConnectionST.h @@ -219,6 +219,16 @@ namespace rfb { return server->sendWatermark; } + const std::string& getUsername() const { return clientUsername; } + void setUsername(const std::string& username) { + clientUsername = username.empty() ? "" : username; + } + + // Returns connection time + time_t getConnectionTime() const { return connectionTime; } + + // Returns access rights + AccessRights getAccessRights() const { return accessRights; } private: // SConnection callbacks @@ -226,6 +236,10 @@ namespace rfb { // none of these methods should call any of the above methods which may // delete the SConnectionST object. + // Connection timestamp when user was authenticated + time_t connectionTime; + + virtual void authSuccess(); virtual void queryConnection(const char* userName); virtual void clientInit(bool shared); @@ -348,6 +362,7 @@ namespace rfb { char unixRelaySubscriptions[MAX_UNIX_RELAYS][MAX_UNIX_RELAY_NAME_LEN]; bool complainedAboutNoViewRights; + std::string clientUsername; }; } #endif diff --git a/common/rfb/VNCServerST.cxx b/common/rfb/VNCServerST.cxx index b29997d..8ea70d7 100644 --- a/common/rfb/VNCServerST.cxx +++ b/common/rfb/VNCServerST.cxx @@ -65,6 +65,7 @@ #include #include #include +#include #include @@ -773,6 +774,27 @@ void VNCServerST::stopDesktop() } } +std::vector VNCServerST::getSessionUsers() { + std::vector users; + + for ( auto client : clients) { + if (!client->authenticated()) { + continue; + } + users.push_back(SessionInfo(client->getUsername(),client->getConnectionTime())); + } + return users; +} + +void VNCServerST::updateSessionUsersList() +{ + auto sessionUsers = getSessionUsers(); + if (!sessionUsers.empty()) { + std::string sessionUsersJson = formatUsersToJson(sessionUsers); + apimessager->mainUpdateSessionsInfo(sessionUsersJson); + } +} + int VNCServerST::authClientCount() { int count = 0; std::list::iterator ci; @@ -1158,9 +1180,6 @@ void VNCServerST::writeUpdate() } } -// checkUpdate() is called by clients to see if it is safe to read from -// the framebuffer at this time. - Region VNCServerST::getPendingRegion() { UpdateInfo ui; @@ -1232,6 +1251,15 @@ void VNCServerST::notifyScreenLayoutChange(VNCSConnectionST* requester) } } +bool VNCServerST::checkClientOwnerships() { + std::list::iterator i; + for (i = clients.begin(); i != clients.end(); i++) { + if ((*i)->is_owner()) + return true; + } + return false; +} + bool VNCServerST::getComparerState() { if (rfb::Server::compareFB == 0) @@ -1290,3 +1318,40 @@ void VNCServerST::sendUnixRelayData(const char name[], } } } + +void VNCServerST::notifyUserAction(const VNCSConnectionST* newConnection, std::string& username, const UserActionType actionType) +{ + if (username.empty()) { + username = "username_unavailable"; + } + + std::string actionTypeStr = actionType == Join ? "joined" : "left"; + int notificationsSent = 0; + + std::string msgNotification = "Sent user " + actionTypeStr + " notification to client"; + std::string errNotification = "Failed to send user " + actionTypeStr + " notification to client: "; + std::string logNotification = "User " + username + " " + actionTypeStr + " - sent notifications to "; + + for (auto client : clients ) { + // Don't notify the connection that just joined, and only notify authenticated connections + if (client != newConnection && client->authenticated() && + client->state() == SConnection::RFBSTATE_NORMAL) { + try { + if (actionType == Join) { + client->writer()->writeUserJoinedSession(username); + } + else { + client->writer()->writeUserLeftSession(username); + } + notificationsSent++; + + slog.debug(msgNotification.c_str()); + } catch (rdr::Exception& e) { + errNotification.append( e.str()); + slog.error(errNotification.c_str()); + } + } + } + logNotification.append( std::to_string(notificationsSent) + " clients"); + slog.info(logNotification.c_str()); +} diff --git a/common/rfb/VNCServerST.h b/common/rfb/VNCServerST.h index fee2517..2df210b 100644 --- a/common/rfb/VNCServerST.h +++ b/common/rfb/VNCServerST.h @@ -35,6 +35,7 @@ #include #include #include +#include namespace rfb { @@ -200,6 +201,9 @@ namespace rfb { void refreshClients(); void sendUnixRelayData(const char name[], const unsigned char *buf, const unsigned len); + enum UserActionType {Join, Leave}; + void notifyUserAction(const VNCSConnectionST* newConnection, std::string& user_name, const UserActionType action_type); + protected: friend class VNCSConnectionST; @@ -253,9 +257,13 @@ namespace rfb { Region getPendingRegion(); const RenderedCursor* getRenderedCursor(); + std::vector getSessionUsers(); + void updateSessionUsersList(); + void notifyScreenLayoutChange(VNCSConnectionST *requester); bool getComparerState(); + bool checkClientOwnerships(); void updateWatermark(); diff --git a/common/rfb/msgTypes.h b/common/rfb/msgTypes.h index 836e4d8..9098b33 100644 --- a/common/rfb/msgTypes.h +++ b/common/rfb/msgTypes.h @@ -37,6 +37,9 @@ namespace rfb { const int msgTypeUnixRelay = 183; const int msgTypeServerFence = 248; + const int msgTypeUserAddedToSession = 253; + const int msgTypeUserRemovedFromSession = 254; + // client to server diff --git a/common/rfb/util.cxx b/common/rfb/util.cxx index a0f5cac..5a21962 100644 --- a/common/rfb/util.cxx +++ b/common/rfb/util.cxx @@ -630,4 +630,57 @@ namespace rfb { sizeof(iecPrefixes)/sizeof(*iecPrefixes), precision); } + + std::string get_default_name(const std::string& str) { + std::string default_name =str; + //Remove IP and other network info since only username needed + auto atPos = default_name.find('@'); + if (atPos != std::string::npos) { + default_name.erase(atPos); + } + + if (default_name.empty()) { + default_name = "username_unavailable"; + } + else { + // Replace special characters + for (size_t i = 0; i < default_name.length(); i++) { + if (default_name[i] == '.' || default_name[i] == ':') { + default_name[i] = '_'; + } + } + } + + return default_name; + + } + std::string formatUsersToJson(const std::vector & users) + { + std::string usersList = "["; + bool firstUser = true; + for (const auto&[userName, connectionTime] : users) + { + std::string username =userName; + time_t connTime = connectionTime; + char timeStr[32]; + strftime(timeStr, sizeof(timeStr), "%Y-%m-%d %H:%M:%S", gmtime(&connTime)); + + if (!firstUser) { + usersList.append( ","); + } + firstUser = false; + + std::string userEntry = "{\"username\":\""; + userEntry.append(username); + userEntry.append( "\", \"connected_since\":\""); + userEntry.append(timeStr); + userEntry.append("\"}"); + + usersList.append(userEntry); + } + usersList += "]"; + usersList = "{\"users\": " + usersList + "} "; + return usersList; + } }; + diff --git a/common/rfb/util.h b/common/rfb/util.h index 18d38ac..9687da9 100644 --- a/common/rfb/util.h +++ b/common/rfb/util.h @@ -31,6 +31,8 @@ #include #include #include +#include +#include struct timeval; @@ -67,6 +69,17 @@ namespace rfb { CharArray& operator=(const CharArray&); }; + struct SessionInfo { + std::string userName; + time_t connectionTime; + SessionInfo(const std::string& name, const time_t& time) + { + userName = name; + connectionTime = time; + } + + }; + char* strDup(const char* s); void strFree(char* s); void strFree(wchar_t* s); @@ -145,6 +158,10 @@ namespace rfb { char *buffer, size_t maxlen, int precision=6); size_t iecPrefix(long long value, const char *unit, char *buffer, size_t maxlen, int precision=6); + + std::string get_default_name(const std::string& str); + std::string formatUsersToJson(const std::vector & users); + } // Some platforms (e.g. Windows) include max() and min() macros in their diff --git a/kasmweb b/kasmweb index 5c46b2e..df061dc 160000 --- a/kasmweb +++ b/kasmweb @@ -1 +1 @@ -Subproject commit 5c46b2e13ab1dd7232b28f017fd7e49ca740f5a4 +Subproject commit df061dc3d5c1f492108822cf569e7b42f087850a