Merge branch 'feature/VNC-196-shared-session-connect-msg' into 'master'

Resolve VNC-196 "Feature/shared session connect msg"

Closes VNC-196

See merge request kasm-technologies/internal/KasmVNC!193
This commit is contained in:
Matthew McClaskey 2025-07-30 18:24:53 +00:00
commit 9b338a061c
16 changed files with 297 additions and 14 deletions

2
.gitmodules vendored
View file

@ -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

View file

@ -28,6 +28,7 @@
#include <map>
#include <string>
#include <vector>
#include <mutex>
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;
};
}

View file

@ -28,6 +28,8 @@
#include <rfb/xxhash.h>
#include <stdio.h>
#include <stdlib.h>
#include <string>
#include <utility>
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<std::mutex> 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);
}

View file

@ -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();

View file

@ -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;

View file

@ -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

View file

@ -18,6 +18,7 @@
* USA.
*/
#include <stdio.h>
#include <string>
#include <rdr/OutStream.h>
#include <rdr/MemOutStream.h>
#include <rdr/ZlibOutStream.h>
@ -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();
}

View file

@ -23,6 +23,7 @@
#ifndef __RFB_SMSGWRITER_H__
#define __RFB_SMSGWRITER_H__
#include <string>
#include <rdr/types.h>
#include <rfb/encodings.h>
#include <rfb/ScreenSet.h>
@ -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();

View file

@ -16,7 +16,7 @@
* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307,
* USA.
*/
#include <network/GetAPI.h>
#include <network/TcpSocket.h>
@ -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)

View file

@ -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

View file

@ -65,6 +65,7 @@
#include <rfb/Watermark.h>
#include <rfb/util.h>
#include <rfb/ledStates.h>
#include <rfb/SMsgWriter.h>
#include <rdr/types.h>
@ -773,6 +774,27 @@ void VNCServerST::stopDesktop()
}
}
std::vector<SessionInfo> VNCServerST::getSessionUsers() {
std::vector<SessionInfo> 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<VNCSConnectionST*>::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<VNCSConnectionST*>::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());
}

View file

@ -35,6 +35,7 @@
#include <rfb/Timer.h>
#include <network/Socket.h>
#include <rfb/ScreenSet.h>
#include <string>
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<SessionInfo> getSessionUsers();
void updateSessionUsersList();
void notifyScreenLayoutChange(VNCSConnectionST *requester);
bool getComparerState();
bool checkClientOwnerships();
void updateWatermark();

View file

@ -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

View file

@ -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<SessionInfo> & 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;
}
};

View file

@ -31,6 +31,8 @@
#include <climits>
#include <cstring>
#include <chrono>
#include <string>
#include <vector>
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<SessionInfo> & users);
}
// Some platforms (e.g. Windows) include max() and min() macros in their

@ -1 +1 @@
Subproject commit 5c46b2e13ab1dd7232b28f017fd7e49ca740f5a4
Subproject commit df061dc3d5c1f492108822cf569e7b42f087850a