mirror of
https://github.com/kasmtech/KasmVNC.git
synced 2026-01-23 02:14:29 +00:00
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:
commit
9b338a061c
16 changed files with 297 additions and 14 deletions
2
.gitmodules
vendored
2
.gitmodules
vendored
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
2
kasmweb
2
kasmweb
|
|
@ -1 +1 @@
|
|||
Subproject commit 5c46b2e13ab1dd7232b28f017fd7e49ca740f5a4
|
||||
Subproject commit df061dc3d5c1f492108822cf569e7b42f087850a
|
||||
Loading…
Add table
Add a link
Reference in a new issue