diff --git a/common/network/CMakeLists.txt b/common/network/CMakeLists.txt index a767e73..51924d7 100644 --- a/common/network/CMakeLists.txt +++ b/common/network/CMakeLists.txt @@ -5,6 +5,7 @@ set(NETWORK_SOURCES Blacklist.cxx Socket.cxx TcpSocket.cxx + jsonescape.c websocket.c websockify.c ${CMAKE_SOURCE_DIR}/unix/kasmvncpasswd/kasmpasswd.c) diff --git a/common/network/GetAPI.h b/common/network/GetAPI.h index 0771640..e3efbc3 100644 --- a/common/network/GetAPI.h +++ b/common/network/GetAPI.h @@ -21,6 +21,7 @@ #include #include +#include #include #include #include @@ -54,15 +55,15 @@ namespace network { uint32_t &len, uint8_t *staging); uint8_t netAddUser(const char name[], const char pw[], const bool write); uint8_t netRemoveUser(const char name[]); + uint8_t netUpdateUser(const char name[], const uint64_t mask, const bool write, + const bool owner); uint8_t netGiveControlTo(const char name[]); + void netGetUsers(const char **ptr); void netGetBottleneckStats(char *buf, uint32_t len); void netGetFrameStats(char *buf, uint32_t len); uint8_t netServerFrameStatsReady(); enum USER_ACTION { - //USER_ADD, - handled locally for interactivity - USER_REMOVE, - USER_GIVE_CONTROL, WANT_FRAME_STATS_SERVERONLY, WANT_FRAME_STATS_ALL, WANT_FRAME_STATS_OWNER, diff --git a/common/network/GetAPIEnums.h b/common/network/GetAPIEnums.h new file mode 100644 index 0000000..2761fd6 --- /dev/null +++ b/common/network/GetAPIEnums.h @@ -0,0 +1,28 @@ +/* Copyright (C) 2021 Kasm + * + * This is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this software; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, + * USA. + */ + +#ifndef __NETWORK_GET_API_ENUMS_H__ +#define __NETWORK_GET_API_ENUMS_H__ + +// Enums that need accessibility from both C and C++. +enum USER_UPDATE_MASK { + USER_UPDATE_WRITE_MASK = 1 << 0, + USER_UPDATE_OWNER_MASK = 1 << 1, +}; + +#endif diff --git a/common/network/GetAPIMessager.cxx b/common/network/GetAPIMessager.cxx index d7abccd..d925873 100644 --- a/common/network/GetAPIMessager.cxx +++ b/common/network/GetAPIMessager.cxx @@ -20,6 +20,7 @@ #include #include +#include #include #include #include @@ -282,8 +283,6 @@ uint8_t GetAPIMessager::netAddUser(const char name[], const char pw[], const boo action_data act; - memcpy(act.data.user, name, USERNAME_LEN); - act.data.user[USERNAME_LEN - 1] = '\0'; memcpy(act.data.password, pw, PASSWORD_LEN); act.data.password[PASSWORD_LEN - 1] = '\0'; act.data.owner = 0; @@ -299,8 +298,8 @@ uint8_t GetAPIMessager::netAddUser(const char name[], const char pw[], const boo struct kasmpasswd_t *set = readkasmpasswd(passwdfile); unsigned s; for (s = 0; s < set->num; s++) { - if (!strcmp(set->entries[s].user, act.data.user)) { - vlog.error("Can't create user %s, already exists", act.data.user); + if (!strcmp(set->entries[s].user, name)) { + vlog.error("Can't create user %s, already exists", name); goto out; } } @@ -311,10 +310,13 @@ uint8_t GetAPIMessager::netAddUser(const char name[], const char pw[], const boo set->entries[s] = act.data; writekasmpasswd(passwdfile, set); - vlog.info("User %s created", act.data.user); + vlog.info("User %s created", name); out: pthread_mutex_unlock(&userMutex); + free(set->entries); + free(set); + return 1; } @@ -324,19 +326,90 @@ uint8_t GetAPIMessager::netRemoveUser(const char name[]) { return 0; } - action_data act; - act.action = USER_REMOVE; + if (pthread_mutex_lock(&userMutex)) + return 0; + + struct kasmpasswd_t *set = readkasmpasswd(passwdfile); + bool found = false; + unsigned s; + for (s = 0; s < set->num; s++) { + if (!strcmp(set->entries[s].user, name)) { + set->entries[s].user[0] = '\0'; + found = true; + break; + } + } + + if (found) { + writekasmpasswd(passwdfile, set); + vlog.info("User %s removed", name); + } else { + vlog.error("Tried to remove nonexistent user %s", name); + + pthread_mutex_unlock(&userMutex); + + free(set->entries); + free(set); + + return 0; + } + + pthread_mutex_unlock(&userMutex); + + free(set->entries); + free(set); + + return 1; +} + +uint8_t GetAPIMessager::netUpdateUser(const char name[], const uint64_t mask, + const bool write, const bool owner) { + if (strlen(name) >= USERNAME_LEN) { + vlog.error("Username too long"); + return 0; + } - memcpy(act.data.user, name, USERNAME_LEN); - act.data.user[USERNAME_LEN - 1] = '\0'; + if (!mask) { + vlog.error("Update_user without any updates?"); + return 0; + } if (pthread_mutex_lock(&userMutex)) return 0; - actionQueue.push_back(act); + struct kasmpasswd_t *set = readkasmpasswd(passwdfile); + bool found = false; + unsigned s; + for (s = 0; s < set->num; s++) { + if (!strcmp(set->entries[s].user, name)) { + if (mask & USER_UPDATE_WRITE_MASK) + set->entries[s].write = write; + if (mask & USER_UPDATE_OWNER_MASK) + set->entries[s].owner = owner; + found = true; + break; + } + } + + if (found) { + writekasmpasswd(passwdfile, set); + vlog.info("User %s permissions updated", name); + } else { + vlog.error("Tried to update nonexistent user %s", name); + + pthread_mutex_unlock(&userMutex); + + free(set->entries); + free(set); + + return 0; + } pthread_mutex_unlock(&userMutex); + free(set->entries); + free(set); + return 1; } @@ -346,22 +419,91 @@ uint8_t GetAPIMessager::netGiveControlTo(const char name[]) { return 0; } - action_data act; - act.action = USER_GIVE_CONTROL; - - memcpy(act.data.user, name, USERNAME_LEN); - act.data.user[USERNAME_LEN - 1] = '\0'; - if (pthread_mutex_lock(&userMutex)) return 0; - actionQueue.push_back(act); + struct kasmpasswd_t *set = readkasmpasswd(passwdfile); + bool found = false; + unsigned s; + for (s = 0; s < set->num; s++) { + if (!strcmp(set->entries[s].user, name)) { + set->entries[s].write = 1; + found = true; + } else { + set->entries[s].write = 0; + } + } + + if (found) { + writekasmpasswd(passwdfile, set); + vlog.info("User %s given control", name); + } else { + vlog.error("Tried to give control to nonexistent user %s", name); + + pthread_mutex_unlock(&userMutex); + + free(set->entries); + free(set); + + return 0; + } pthread_mutex_unlock(&userMutex); + free(set->entries); + free(set); + return 1; } +void GetAPIMessager::netGetUsers(const char **outptr) { +/* +[ + { "user": "username", "write": true, "owner": true }, + { "user": "username", "write": true, "owner": true } +] +*/ + char *buf; + char escapeduser[USERNAME_LEN * 2]; + + if (pthread_mutex_lock(&userMutex)) { + *outptr = (char *) calloc(1, 1); + return; + } + + struct kasmpasswd_t *set = readkasmpasswd(passwdfile); + + buf = (char *) calloc(set->num, 80); + FILE *f = fmemopen(buf, set->num * 80, "w"); + + fprintf(f, "[\n"); + + unsigned s; + for (s = 0; s < set->num; s++) { + JSON_escape(set->entries[s].user, escapeduser); + + fprintf(f, " { \"user\": \"%s\", \"write\": %s, \"owner\": %s }", + escapeduser, + set->entries[s].write ? "true" : "false", + set->entries[s].owner ? "true" : "false"); + + if (s == set->num - 1) + fprintf(f, "\n"); + else + fprintf(f, ",\n"); + } + + free(set->entries); + free(set); + + fprintf(f, "]\n"); + + fclose(f); + + pthread_mutex_unlock(&userMutex); + *outptr = buf; +} + void GetAPIMessager::netGetBottleneckStats(char *buf, uint32_t len) { /* { diff --git a/common/network/TcpSocket.cxx b/common/network/TcpSocket.cxx index f6f4a5a..7a08773 100644 --- a/common/network/TcpSocket.cxx +++ b/common/network/TcpSocket.cxx @@ -455,12 +455,25 @@ static uint8_t removeCb(void *messager, const char name[]) return msgr->netRemoveUser(name); } +static uint8_t updateUserCb(void *messager, const char name[], const uint64_t mask, + const uint8_t write, const uint8_t owner) +{ + GetAPIMessager *msgr = (GetAPIMessager *) messager; + return msgr->netUpdateUser(name, mask, write, owner); +} + static uint8_t givecontrolCb(void *messager, const char name[]) { GetAPIMessager *msgr = (GetAPIMessager *) messager; return msgr->netGiveControlTo(name); } +static void getUsersCb(void *messager, const char **ptr) +{ + GetAPIMessager *msgr = (GetAPIMessager *) messager; + msgr->netGetUsers(ptr); +} + static void bottleneckStatsCb(void *messager, char *buf, uint32_t len) { GetAPIMessager *msgr = (GetAPIMessager *) messager; @@ -613,7 +626,9 @@ WebsocketListener::WebsocketListener(const struct sockaddr *listenaddr, settings.screenshotCb = screenshotCb; settings.adduserCb = adduserCb; settings.removeCb = removeCb; + settings.updateUserCb = updateUserCb; settings.givecontrolCb = givecontrolCb; + settings.getUsersCb = getUsersCb; settings.bottleneckStatsCb = bottleneckStatsCb; settings.frameStatsCb = frameStatsCb; diff --git a/common/network/jsonescape.c b/common/network/jsonescape.c new file mode 100644 index 0000000..57ae87f --- /dev/null +++ b/common/network/jsonescape.c @@ -0,0 +1,81 @@ +/* Copyright (C) 2022 Kasm + * + * This is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this software; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, + * USA. + */ + +#include "jsonescape.h" + +void JSON_escape(const char *in, char *out) { + for (; *in; in++) { + if (in[0] == '\b') { + *out++ = '\\'; + *out++ = 'b'; + } else if (in[0] == '\f') { + *out++ = '\\'; + *out++ = 'f'; + } else if (in[0] == '\n') { + *out++ = '\\'; + *out++ = 'n'; + } else if (in[0] == '\r') { + *out++ = '\\'; + *out++ = 'r'; + } else if (in[0] == '\t') { + *out++ = '\\'; + *out++ = 't'; + } else if (in[0] == '"') { + *out++ = '\\'; + *out++ = '"'; + } else if (in[0] == '\\') { + *out++ = '\\'; + *out++ = '\\'; + } else { + *out++ = *in; + } + } + + *out = '\0'; +} + +void JSON_unescape(const char *in, char *out) { + for (; *in; in++) { + if (in[0] == '\\' && in[1] == 'b') { + *out++ = '\b'; + in++; + } else if (in[0] == '\\' && in[1] == 'f') { + *out++ = '\f'; + in++; + } else if (in[0] == '\\' && in[1] == 'n') { + *out++ = '\n'; + in++; + } else if (in[0] == '\\' && in[1] == 'r') { + *out++ = '\r'; + in++; + } else if (in[0] == '\\' && in[1] == 't') { + *out++ = '\t'; + in++; + } else if (in[0] == '\\' && in[1] == '"') { + *out++ = '"'; + in++; + } else if (in[0] == '\\' && in[1] == '\\') { + *out++ = '\\'; + in++; + } else { + *out++ = *in; + } + } + + *out = '\0'; +} diff --git a/common/network/jsonescape.h b/common/network/jsonescape.h new file mode 100644 index 0000000..c26c612 --- /dev/null +++ b/common/network/jsonescape.h @@ -0,0 +1,33 @@ +/* Copyright (C) 2022 Kasm + * + * This is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this software; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, + * USA. + */ + +#ifndef __NETWORK_JSON_ESCAPE_H__ +#define __NETWORK_JSON_ESCAPE_H__ + +#ifdef __cplusplus +extern "C" { +#endif + +void JSON_escape(const char *in, char *out); +void JSON_unescape(const char *in, char *out); + +#ifdef __cplusplus +} // extern C +#endif + +#endif diff --git a/common/network/websocket.c b/common/network/websocket.c index 01d246e..53bcc8f 100644 --- a/common/network/websocket.c +++ b/common/network/websocket.c @@ -1074,7 +1074,51 @@ static uint8_t ownerapi(ws_ctx_t *ws_ctx, const char *in) { "200 OK"); ws_send(ws_ctx, buf, strlen(buf)); - wserr("Passed remove_user request to main thread\n"); + ret = 1; + } else entry("/api/update_user") { + char decname[1024] = ""; + + param = parse_get(args, "name", &len); + if (len) { + memcpy(buf, param, len); + buf[len] = '\0'; + percent_decode(buf, decname, 0); + } + + if (!decname[0]) + goto nope; + + uint64_t mask = 0; + uint8_t mywrite = 0; + param = parse_get(args, "write", &len); + if (len && isalpha(param[0])) { + mask |= USER_UPDATE_WRITE_MASK; + if (!strncmp(param, "true", len)) + mywrite = 1; + } + + uint8_t myowner = 0; + param = parse_get(args, "owner", &len); + if (len && isalpha(param[0])) { + mask |= USER_UPDATE_OWNER_MASK; + if (!strncmp(param, "true", len)) + myowner = 1; + } + + if (!settings.updateUserCb(settings.messager, decname, mask, mywrite, myowner)) { + wserr("Invalid params to update_user\n"); + goto nope; + } + + 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: 6\r\n" + "\r\n" + "200 OK"); + ws_send(ws_ctx, buf, strlen(buf)); + ret = 1; } else entry("/api/give_control") { char decname[1024] = ""; @@ -1103,7 +1147,6 @@ static uint8_t ownerapi(ws_ctx_t *ws_ctx, const char *in) { "200 OK"); ws_send(ws_ctx, buf, strlen(buf)); - wserr("Passed give_control request to main thread\n"); ret = 1; } else entry("/api/get_bottleneck_stats") { char statbuf[4096]; @@ -1120,6 +1163,23 @@ static uint8_t ownerapi(ws_ctx_t *ws_ctx, const char *in) { wserr("Sent bottleneck stats to API caller\n"); ret = 1; + } else entry("/api/get_users") { + const char *ptr; + settings.getUsersCb(settings.messager, &ptr); + + 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" + "\r\n", strlen(ptr)); + ws_send(ws_ctx, buf, strlen(buf)); + ws_send(ws_ctx, ptr, strlen(ptr)); + + free((char *) ptr); + + wserr("Sent user 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 5cb8c0e..4cf5da1 100644 --- a/common/network/websocket.h +++ b/common/network/websocket.h @@ -1,5 +1,6 @@ #include #include +#include "GetAPIEnums.h" #define BUFSIZE 65536 #define DBUFSIZE (BUFSIZE * 3) / 4 - 20 @@ -83,6 +84,8 @@ typedef struct { uint8_t (*adduserCb)(void *messager, const char name[], const char pw[], const uint8_t write); uint8_t (*removeCb)(void *messager, const char name[]); + uint8_t (*updateUserCb)(void *messager, const char name[], const uint64_t mask, + const uint8_t write, const uint8_t owner); uint8_t (*givecontrolCb)(void *messager, const char name[]); void (*bottleneckStatsCb)(void *messager, char *buf, uint32_t len); void (*frameStatsCb)(void *messager, char *buf, uint32_t len); @@ -94,6 +97,7 @@ typedef struct { uint8_t (*ownerConnectedCb)(void *messager); uint8_t (*numActiveUsersCb)(void *messager); + void (*getUsersCb)(void *messager, const char **buf); uint8_t (*getClientFrameStatsNumCb)(void *messager); uint8_t (*serverFrameStatsReadyCb)(void *messager); } settings_t; diff --git a/common/rfb/VNCServerST.cxx b/common/rfb/VNCServerST.cxx index 7807feb..0511171 100644 --- a/common/rfb/VNCServerST.cxx +++ b/common/rfb/VNCServerST.cxx @@ -805,49 +805,8 @@ static void checkAPIMessages(network::GetAPIMessager *apimessager, slog.info("Main thread processing user API request %u/%u", i + 1, num); const network::GetAPIMessager::action_data &act = apimessager->actionQueue[i]; - struct kasmpasswd_t *set = NULL; - unsigned s; - bool found; switch (act.action) { - case network::GetAPIMessager::USER_REMOVE: - set = readkasmpasswd(kasmpasswdpath); - found = false; - for (s = 0; s < set->num; s++) { - if (!strcmp(set->entries[s].user, act.data.user)) { - set->entries[s].user[0] = '\0'; - found = true; - break; - } - } - - if (found) { - writekasmpasswd(kasmpasswdpath, set); - slog.info("User %s removed", act.data.user); - } else { - slog.error("Tried to remove nonexistent user %s", act.data.user); - } - break; - case network::GetAPIMessager::USER_GIVE_CONTROL: - set = readkasmpasswd(kasmpasswdpath); - found = false; - for (s = 0; s < set->num; s++) { - if (!strcmp(set->entries[s].user, act.data.user)) { - set->entries[s].write = 1; - found = true; - } else { - set->entries[s].write = 0; - } - } - - if (found) { - writekasmpasswd(kasmpasswdpath, set); - slog.info("User %s given control", act.data.user); - } else { - slog.error("Tried to give control to nonexistent user %s", act.data.user); - } - break; - case network::GetAPIMessager::WANT_FRAME_STATS_SERVERONLY: trackingFrameStats = act.action; break; @@ -862,11 +821,6 @@ static void checkAPIMessages(network::GetAPIMessager *apimessager, memcpy(trackingClient, act.data.password, 128); break; } - - if (set) { - free(set->entries); - free(set); - } } apimessager->actionQueue.clear();