diff --git a/builder/dockerfile.centos_core.barebones.rpm.test b/builder/dockerfile.centos_core.barebones.rpm.test index 33098d3..b5e22e9 100644 --- a/builder/dockerfile.centos_core.barebones.rpm.test +++ b/builder/dockerfile.centos_core.barebones.rpm.test @@ -2,6 +2,7 @@ FROM centos:centos7 RUN yum install -y xterm RUN yum install -y vim less +RUN yum install -y redhat-lsb-core ARG KASMVNC_PACKAGE_DIR COPY $KASMVNC_PACKAGE_DIR/*.rpm /tmp @@ -9,10 +10,9 @@ RUN yum localinstall -y /tmp/*.rpm RUN useradd -m foo -USER foo:kasmvnc +USER foo:kasmvnc-cert RUN mkdir ~/.vnc && echo '/usr/bin/xterm &' >> ~/.vnc/xstartup && \ chmod +x ~/.vnc/xstartup -RUN echo bar | kasmvncpasswd -f > $HOME/.kasmpasswd && chmod 0600 $HOME/.kasmpasswd -ENTRYPOINT bash -c "vncserver :1 -interface 0.0.0.0 && vncserver -kill :1 && vncserver :1 -depth 24 -geometry 1280x1050 -websocketPort 8443 -cert /etc/pki/tls/private/kasmvnc.pem -sslOnly -FrameRate=24 -interface 0.0.0.0 -httpd /usr/share/kasmvnc/www && tail -f $HOME/.vnc/*.log " +ENTRYPOINT bash -c "echo -e \"$VNC_PW\n$VNC_PW\n\" | kasmvncpasswd -w -u \"$VNC_USER\" && vncserver :1 -interface 0.0.0.0 && vncserver -kill :1 && vncserver :1 -depth 24 -geometry 1280x1050 -websocketPort 8443 -cert /etc/pki/tls/private/kasmvnc.pem -sslOnly -FrameRate=24 -interface 0.0.0.0 -httpd /usr/share/kasmvnc/www && tail -f $HOME/.vnc/*.log " diff --git a/builder/dockerfile.fedora_thirtythree.barebones.rpm.test b/builder/dockerfile.fedora_thirtythree.barebones.rpm.test index d9ab4d7..b592c89 100644 --- a/builder/dockerfile.fedora_thirtythree.barebones.rpm.test +++ b/builder/dockerfile.fedora_thirtythree.barebones.rpm.test @@ -2,6 +2,7 @@ FROM fedora:33 RUN dnf install -y xterm RUN dnf install -y vim less +RUN yum install -y redhat-lsb-core ARG KASMVNC_PACKAGE_DIR COPY $KASMVNC_PACKAGE_DIR/*.rpm /tmp @@ -9,10 +10,9 @@ RUN dnf localinstall -y /tmp/*.rpm RUN useradd -m foo -USER foo:kasmvnc +USER foo:kasmvnc-cert RUN mkdir ~/.vnc && echo '/usr/bin/xterm &' >> ~/.vnc/xstartup && \ chmod +x ~/.vnc/xstartup -RUN echo bar | kasmvncpasswd -f > $HOME/.kasmpasswd && chmod 0600 $HOME/.kasmpasswd -ENTRYPOINT bash -c "vncserver :1 -interface 0.0.0.0 && vncserver -kill :1 && vncserver :1 -depth 24 -geometry 1280x1050 -websocketPort 8443 -cert /etc/pki/tls/private/kasmvnc.pem -sslOnly -FrameRate=24 -interface 0.0.0.0 -httpd /usr/share/kasmvnc/www && tail -f $HOME/.vnc/*.log " +ENTRYPOINT bash -c "echo -e \"$VNC_PW\n$VNC_PW\n\" | kasmvncpasswd -w -u \"$VNC_USER\" && vncserver :1 -interface 0.0.0.0 && vncserver -kill :1 && vncserver :1 -depth 24 -geometry 1280x1050 -websocketPort 8443 -cert /etc/pki/tls/private/kasmvnc.pem -sslOnly -FrameRate=24 -interface 0.0.0.0 -httpd /usr/share/kasmvnc/www && tail -f $HOME/.vnc/*.log " diff --git a/builder/dockerfile.ubuntu_focal.barebones.deb.test b/builder/dockerfile.ubuntu_focal.barebones.deb.test index 4449d77..26b0c14 100644 --- a/builder/dockerfile.ubuntu_focal.barebones.deb.test +++ b/builder/dockerfile.ubuntu_focal.barebones.deb.test @@ -11,6 +11,5 @@ USER foo RUN mkdir ~/.vnc && echo '/usr/bin/xterm &' >> ~/.vnc/xstartup && \ chmod +x ~/.vnc/xstartup -RUN echo bar | kasmvncpasswd -f > $HOME/.kasmpasswd && chmod 0600 $HOME/.kasmpasswd -ENTRYPOINT bash -c "vncserver :1 -interface 0.0.0.0 && vncserver -kill :1 && vncserver :1 -depth 24 -geometry 1280x1050 -websocketPort 8443 -cert /etc/ssl/certs/ssl-cert-snakeoil.pem -key /etc/ssl/private/ssl-cert-snakeoil.key -sslOnly -FrameRate=24 -interface 0.0.0.0 -httpd /usr/share/kasmvnc/www && tail -f $HOME/.vnc/*.log " +ENTRYPOINT bash -c "echo -e \"$VNC_PW\n$VNC_PW\n\" | kasmvncpasswd -w -u \"$VNC_USER\" && vncserver :1 -interface 0.0.0.0 && vncserver -kill :1 && vncserver :1 -depth 24 -geometry 1280x1050 -websocketPort 8443 -cert /etc/ssl/certs/ssl-cert-snakeoil.pem -key /etc/ssl/private/ssl-cert-snakeoil.key -sslOnly -FrameRate=24 -interface 0.0.0.0 -httpd /usr/share/kasmvnc/www && tail -f $HOME/.vnc/*.log " diff --git a/common/network/CMakeLists.txt b/common/network/CMakeLists.txt index 51a32a9..d63c696 100644 --- a/common/network/CMakeLists.txt +++ b/common/network/CMakeLists.txt @@ -1,6 +1,7 @@ include_directories(${CMAKE_SOURCE_DIR}/common ${CMAKE_SOURCE_DIR}/unix/kasmvncpasswd) set(NETWORK_SOURCES + GetAPIMessager.cxx Socket.cxx TcpSocket.cxx websocket.c diff --git a/common/network/GetAPI.h b/common/network/GetAPI.h new file mode 100644 index 0000000..c3eae8e --- /dev/null +++ b/common/network/GetAPI.h @@ -0,0 +1,75 @@ +/* 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_H__ +#define __NETWORK_GET_API_H__ + +#include +#include +#include +#include +#include +#include + +namespace network { + + class GetAPIMessager { + public: + GetAPIMessager(const char *passwdfile_); + + // from main thread + void mainUpdateScreen(rfb::PixelBuffer *pb); + + // from network threads + uint8_t *netGetScreenshot(uint16_t w, uint16_t h, + const uint8_t q, const bool dedup, + 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 netGiveControlTo(const char name[]); + + enum USER_ACTION { + //USER_ADD, - handled locally for interactivity + USER_REMOVE, + USER_GIVE_CONTROL, + }; + + struct action_data { + enum USER_ACTION action; + kasmpasswd_entry_t data; + }; + + pthread_mutex_t userMutex; + std::vector actionQueue; + + private: + const char *passwdfile; + + pthread_mutex_t screenMutex; + rfb::ManagedPixelBuffer screenPb; + uint16_t screenW, screenH; + uint64_t screenHash; + + std::vector cachedJpeg; + uint16_t cachedW, cachedH; + uint8_t cachedQ; + }; + +} + +#endif // __NETWORK_GET_API_H__ diff --git a/common/network/GetAPIMessager.cxx b/common/network/GetAPIMessager.cxx new file mode 100644 index 0000000..b06fabd --- /dev/null +++ b/common/network/GetAPIMessager.cxx @@ -0,0 +1,288 @@ +/* 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. + */ + +#define __STDC_FORMAT_MACROS + +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace network; +using namespace rfb; + +static LogWriter vlog("GetAPIMessager"); + +PixelBuffer *progressiveBilinearScale(const PixelBuffer *pb, + const uint16_t tgtw, const uint16_t tgth, + const float tgtdiff); + +struct TightJPEGConfiguration { + int quality; + int subsampling; +}; + +static const struct TightJPEGConfiguration conf[10] = { + { 15, subsample4X }, // 0 + { 29, subsample4X }, // 1 + { 41, subsample4X }, // 2 + { 42, subsample2X }, // 3 + { 62, subsample2X }, // 4 + { 77, subsample2X }, // 5 + { 79, subsampleNone }, // 6 + { 86, subsampleNone }, // 7 + { 92, subsampleNone }, // 8 + { 100, subsampleNone } // 9 +}; + +GetAPIMessager::GetAPIMessager(const char *passwdfile_): passwdfile(passwdfile_), + screenW(0), screenH(0), screenHash(0), + cachedW(0), cachedH(0), cachedQ(0) { + + pthread_mutex_init(&screenMutex, NULL); + pthread_mutex_init(&userMutex, NULL); +} + +// from main thread +void GetAPIMessager::mainUpdateScreen(rfb::PixelBuffer *pb) { + if (pthread_mutex_trylock(&screenMutex)) + return; + + int stride; + const rdr::U8 * const buf = pb->getBuffer(pb->getRect(), &stride); + + if (pb->width() != screenW || pb->height() != screenH) { + screenHash = 0; + screenW = pb->width(); + screenH = pb->height(); + screenPb.setPF(pb->getPF()); + screenPb.setSize(screenW, screenH); + + cachedW = cachedH = cachedQ = 0; + cachedJpeg.clear(); + } + + const uint64_t newHash = XXH64(buf, pb->area() * 4, 0); + if (newHash != screenHash) { + cachedW = cachedH = cachedQ = 0; + cachedJpeg.clear(); + + screenHash = newHash; + rdr::U8 *rw = screenPb.getBufferRW(screenPb.getRect(), &stride); + memcpy(rw, buf, screenW * screenH * 4); + screenPb.commitBufferRW(screenPb.getRect()); + } + + pthread_mutex_unlock(&screenMutex); +} + +// from network threads +uint8_t *GetAPIMessager::netGetScreenshot(uint16_t w, uint16_t h, + const uint8_t q, const bool dedup, + uint32_t &len, uint8_t *staging) { + + uint8_t *ret = NULL; + len = 0; + + if (w > screenW) + w = screenW; + if (h > screenH) + h = screenH; + + if (!screenW || !screenH) + vlog.error("Screenshot requested but no screenshot exists (screen hasn't been viewed)"); + + if (!w || !h || q > 9 || !staging) + return NULL; + + if (pthread_mutex_lock(&screenMutex)) + return NULL; + + if (w == cachedW && h == cachedH && q == cachedQ) { + if (dedup) { + // Return the hash of the unchanged image + sprintf((char *) staging, "%" PRIx64, screenHash); + ret = staging; + len = 16; + } else { + // Return the cached image + len = cachedJpeg.size(); + ret = staging; + memcpy(ret, &cachedJpeg[0], len); + + vlog.info("Returning cached screenshot"); + } + } else { + // Encode the new JPEG, cache it + JpegCompressor jc; + int quality, subsampling; + + quality = conf[q].quality; + subsampling = conf[q].subsampling; + + jc.clear(); + int stride; + + if (w != screenW || h != screenH) { + float xdiff = w / (float) screenW; + float ydiff = h / (float) screenH; + const float diff = xdiff < ydiff ? xdiff : ydiff; + + const uint16_t neww = screenW * diff; + const uint16_t newh = screenH * diff; + + const PixelBuffer *scaled = progressiveBilinearScale(&screenPb, neww, newh, diff); + const rdr::U8 * const buf = scaled->getBuffer(scaled->getRect(), &stride); + + jc.compress(buf, stride, scaled->getRect(), + scaled->getPF(), quality, subsampling); + + cachedJpeg.resize(jc.length()); + memcpy(&cachedJpeg[0], jc.data(), jc.length()); + + delete scaled; + + vlog.info("Returning scaled screenshot"); + } else { + const rdr::U8 * const buf = screenPb.getBuffer(screenPb.getRect(), &stride); + + jc.compress(buf, stride, screenPb.getRect(), + screenPb.getPF(), quality, subsampling); + + cachedJpeg.resize(jc.length()); + memcpy(&cachedJpeg[0], jc.data(), jc.length()); + + vlog.info("Returning normal screenshot"); + } + + cachedQ = q; + cachedW = w; + cachedH = h; + + len = cachedJpeg.size(); + ret = staging; + memcpy(ret, &cachedJpeg[0], len); + } + + pthread_mutex_unlock(&screenMutex); + + return ret; +} + +#define USERNAME_LEN sizeof(((struct kasmpasswd_entry_t *)0)->user) +#define PASSWORD_LEN sizeof(((struct kasmpasswd_entry_t *)0)->password) + +uint8_t GetAPIMessager::netAddUser(const char name[], const char pw[], const bool write) { + if (strlen(name) >= USERNAME_LEN) { + vlog.error("Username too long"); + return 0; + } + + if (strlen(pw) >= PASSWORD_LEN) { + vlog.error("Password too long"); + return 0; + } + + if (!passwdfile) + return 0; + + 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; + act.data.write = write; + + if (pthread_mutex_lock(&userMutex)) + return 0; + + // This needs to be handled locally for proper interactivity + // (consider adding users when nobody is connected). + // The mutex and atomic rename keep things in sync. + + 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); + goto out; + } + } + + s = set->num++; + set->entries = (struct kasmpasswd_entry_t *) realloc(set->entries, + set->num * sizeof(struct kasmpasswd_entry_t)); + set->entries[s] = act.data; + + writekasmpasswd(passwdfile, set); + vlog.info("User %s created", act.data.user); +out: + pthread_mutex_unlock(&userMutex); + + return 1; +} + +uint8_t GetAPIMessager::netRemoveUser(const char name[]) { + if (strlen(name) >= USERNAME_LEN) { + vlog.error("Username too long"); + return 0; + } + + action_data act; + act.action = USER_REMOVE; + + 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); + + pthread_mutex_unlock(&userMutex); + + return 1; +} + +uint8_t GetAPIMessager::netGiveControlTo(const char name[]) { + if (strlen(name) >= USERNAME_LEN) { + vlog.error("Username too long"); + 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); + + pthread_mutex_unlock(&userMutex); + + return 1; +} diff --git a/common/network/Socket.h b/common/network/Socket.h index bfda8a5..97cc4d0 100644 --- a/common/network/Socket.h +++ b/common/network/Socket.h @@ -74,6 +74,8 @@ namespace network { virtual ~ConnectionFilter() {} }; + class GetAPIMessager; + class SocketListener { public: SocketListener(int fd); @@ -93,6 +95,8 @@ namespace network { void setFilter(ConnectionFilter* f) {filter = f;} int getFd() {return fd;} + virtual GetAPIMessager *getMessager() { return NULL; } + protected: SocketListener(); diff --git a/common/network/TcpSocket.cxx b/common/network/TcpSocket.cxx index c6a7e14..4262be5 100644 --- a/common/network/TcpSocket.cxx +++ b/common/network/TcpSocket.cxx @@ -42,6 +42,7 @@ #include #include "websocket.h" +#include #include #include #include @@ -431,10 +432,37 @@ int TcpListener::getMyPort() { extern settings_t settings; +static uint8_t *screenshotCb(void *messager, uint16_t w, uint16_t h, const uint8_t q, + const uint8_t dedup, + uint32_t *len, uint8_t *staging) +{ + GetAPIMessager *msgr = (GetAPIMessager *) messager; + return msgr->netGetScreenshot(w, h, q, dedup, *len, staging); +} + +static uint8_t adduserCb(void *messager, const char name[], const char pw[], + const uint8_t write) +{ + GetAPIMessager *msgr = (GetAPIMessager *) messager; + return msgr->netAddUser(name, pw, write); +} + +static uint8_t removeCb(void *messager, const char name[]) +{ + GetAPIMessager *msgr = (GetAPIMessager *) messager; + return msgr->netRemoveUser(name); +} + +static uint8_t givecontrolCb(void *messager, const char name[]) +{ + GetAPIMessager *msgr = (GetAPIMessager *) messager; + return msgr->netGiveControlTo(name); +} + WebsocketListener::WebsocketListener(const struct sockaddr *listenaddr, socklen_t listenaddrlen, bool sslonly, const char *cert, const char *certkey, - const char *basicauth, + bool disablebasicauth, const char *httpdir) { int one = 1; @@ -504,7 +532,7 @@ WebsocketListener::WebsocketListener(const struct sockaddr *listenaddr, settings.passwdfile = strdup(wexp.we_wordv[0]); wordfree(&wexp); - settings.basicauth = basicauth; + settings.disablebasicauth = disablebasicauth; settings.cert = cert; settings.key = certkey; settings.ssl_only = sslonly; @@ -515,6 +543,12 @@ WebsocketListener::WebsocketListener(const struct sockaddr *listenaddr, settings.listen_sock = sock; + settings.messager = messager = new GetAPIMessager(settings.passwdfile); + settings.screenshotCb = screenshotCb; + settings.adduserCb = adduserCb; + settings.removeCb = removeCb; + settings.givecontrolCb = givecontrolCb; + pthread_t tid; pthread_create(&tid, NULL, start_server, NULL); } @@ -684,7 +718,7 @@ void network::createTcpListeners(std::list *listeners, void network::createWebsocketListeners(std::list *listeners, const struct addrinfo *ai, bool sslonly, const char *cert, const char *certkey, - const char *basicauth, + bool disablebasicauth, const char *httpdir) { const struct addrinfo *current; @@ -711,7 +745,7 @@ void network::createWebsocketListeners(std::list *listeners, try { new_listeners.push_back(new WebsocketListener(current->ai_addr, current->ai_addrlen, - sslonly, cert, certkey, basicauth, + sslonly, cert, certkey, disablebasicauth, httpdir)); } catch (SocketException& e) { // Ignore this if it is due to lack of address family support on @@ -740,7 +774,7 @@ void network::createWebsocketListeners(std::list *listeners, bool sslonly, const char *cert, const char *certkey, - const char *basicauth, + bool disablebasicauth, const char *httpdir) { if (addr && !strcmp(addr, "local")) { @@ -768,7 +802,7 @@ void network::createWebsocketListeners(std::list *listeners, ai[1].ai_addrlen = sizeof(sa[1].u.sin6); ai[1].ai_next = NULL; - createWebsocketListeners(listeners, ai, sslonly, cert, certkey, basicauth, httpdir); + createWebsocketListeners(listeners, ai, sslonly, cert, certkey, disablebasicauth, httpdir); } else { struct addrinfo *ai, hints; char service[16]; @@ -791,7 +825,7 @@ void network::createWebsocketListeners(std::list *listeners, gai_strerror(result)); try { - createWebsocketListeners(listeners, ai, sslonly, cert, certkey, basicauth, httpdir); + createWebsocketListeners(listeners, ai, sslonly, cert, certkey, disablebasicauth, httpdir); } catch(...) { freeaddrinfo(ai); throw; diff --git a/common/network/TcpSocket.h b/common/network/TcpSocket.h index 57a8629..3e2dc1e 100644 --- a/common/network/TcpSocket.h +++ b/common/network/TcpSocket.h @@ -91,7 +91,7 @@ namespace network { public: WebsocketListener(const struct sockaddr *listenaddr, socklen_t listenaddrlen, bool sslonly, const char *cert, const char *certkey, - const char *basicauth, + bool disablebasicauth, const char *httpdir); virtual int getMyPort(); @@ -100,8 +100,12 @@ namespace network { int internalSocket; + virtual GetAPIMessager *getMessager() { return messager; } + protected: virtual Socket* createSocket(int fd); + private: + GetAPIMessager *messager; }; void createLocalTcpListeners(std::list *listeners, @@ -112,7 +116,7 @@ namespace network { bool sslonly, const char *cert, const char *certkey, - const char *basicauth, + bool disablebasicauth, const char *httpdir); void createTcpListeners(std::list *listeners, const char *addr, @@ -124,7 +128,7 @@ namespace network { bool sslonly, const char *cert, const char *certkey, - const char *basicauth, + bool disablebasicauth, const char *httpdir); typedef struct vnc_sockaddr { diff --git a/common/network/websocket.c b/common/network/websocket.c index 21e22ee..f0d7f0b 100644 --- a/common/network/websocket.c +++ b/common/network/websocket.c @@ -9,6 +9,7 @@ */ #define _GNU_SOURCE +#include #include #include #include @@ -83,6 +84,55 @@ int resolve_host(struct in_addr *sin_addr, const char *hostname) return 0; } +static const char *parse_get(const char * const in, const char * const opt, unsigned *len) { + const char *start = in; + const char *end = strchrnul(start, '&'); + const unsigned optlen = strlen(opt); + *len = 0; + + while (1) { + if (!strncmp(start, opt, optlen)) { + const char *arg = strchr(start, '='); + if (!arg) + return ""; + arg++; + *len = end - arg; + return arg; + } + + if (!*end) + break; + + end++; + start = end; + end = strchrnul(start, '&'); + } + + return ""; +} + +static void percent_decode(const char *src, char *dst) { + while (1) { + if (!*src) + break; + if (*src != '%') { + *dst++ = *src++; + } else { + src++; + if (!src[0] || !src[1]) + break; + char hex[3]; + hex[0] = src[0]; + hex[1] = src[1]; + hex[2] = '\0'; + + src += 2; + *dst++ = strtol(hex, NULL, 16); + } + } + + *dst = '\0'; +} /* * SSL Wrapper Code @@ -814,6 +864,226 @@ nope: ws_send(ws_ctx, buf, strlen(buf)); } +static uint8_t ownerapi(ws_ctx_t *ws_ctx, const char *in) { + char buf[4096], path[4096], args[4096] = ""; + uint8_t ret = 0; // 0 = continue checking + + if (strncmp(in, "GET ", 4)) { + wserr("non-GET request, rejecting\n"); + return 0; + } + in += 4; + const char *end = strchr(in, ' '); + unsigned len = end - in; + + if (len < 1 || len > 1024 || strstr(in, "../")) { + wserr("Request too long (%u) or attempted dir traversal attack, rejecting\n", len); + return 0; + } + + end = memchr(in, '?', len); + if (end) { + len = end - in; + end++; + + const char *argend = strchr(end, ' '); + strncpy(args, end, argend - end); + args[argend - end] = '\0'; + } + + memcpy(path, in, len); + path[len] = '\0'; + + if (strstr(args, "password=")) { + wserr("Requested owner api '%s' with args (skipped, includes password)\n", path); + } else { + wserr("Requested owner api '%s' with args '%s'\n", path, args); + } + + #define entry(x) if (!strcmp(path, x)) + + const char *param; + + entry("/api/get_screenshot") { + uint8_t q = 7, dedup = 0; + uint16_t w = 4096, h = 4096; + + param = parse_get(args, "width", &len); + if (len && isdigit(param[0])) + w = atoi(param); + + param = parse_get(args, "height", &len); + if (len && isdigit(param[0])) + h = atoi(param); + + param = parse_get(args, "quality", &len); + if (len && isdigit(param[0])) + q = atoi(param); + + param = parse_get(args, "deduplicate", &len); + if (len && isalpha(param[0])) { + if (!strncmp(param, "true", len)) + dedup = 1; + } + + uint8_t *staging = malloc(1024 * 1024 * 8); + + settings.screenshotCb(settings.messager, w, h, q, dedup, &len, staging); + + if (len == 16) { + 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: %u\r\n" + "\r\n", len); + ws_send(ws_ctx, buf, strlen(buf)); + ws_send(ws_ctx, staging, len); + + wserr("Screenshot hadn't changed and dedup was requested, sent hash\n"); + ret = 1; + } else if (len) { + sprintf(buf, "HTTP/1.1 200 OK\r\n" + "Server: KasmVNC/4.0\r\n" + "Connection: close\r\n" + "Content-type: image/jpeg\r\n" + "Content-length: %u\r\n" + "\r\n", len); + ws_send(ws_ctx, buf, strlen(buf)); + ws_send(ws_ctx, staging, len); + + wserr("Sent screenshot %u bytes\n", len); + ret = 1; + } + + free(staging); + + if (!len) { + wserr("Invalid params to screenshot\n"); + goto nope; + } + } else entry("/api/create_user") { + char decname[1024] = "", decpw[1024] = ""; + uint8_t write = 0; + + param = parse_get(args, "name", &len); + if (len) { + memcpy(buf, param, len); + buf[len] = '\0'; + percent_decode(buf, decname); + } + + param = parse_get(args, "password", &len); + if (len) { + memcpy(buf, param, len); + buf[len] = '\0'; + percent_decode(buf, decpw); + + struct crypt_data cdata; + cdata.initialized = 0; + + const char *encrypted = crypt_r(decpw, "$5$kasm$", &cdata); + strcpy(decpw, encrypted); + } + + param = parse_get(args, "write", &len); + if (len && isalpha(param[0])) { + if (!strncmp(param, "true", len)) + write = 1; + } + + if (!decname[0] || !decpw[0]) + goto nope; + + if (!settings.adduserCb(settings.messager, decname, decpw, write)) { + wserr("Invalid params to create_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/remove_user") { + char decname[1024] = ""; + + param = parse_get(args, "name", &len); + if (len) { + memcpy(buf, param, len); + buf[len] = '\0'; + percent_decode(buf, decname); + } + + if (!decname[0]) + goto nope; + + if (!settings.removeCb(settings.messager, decname)) { + wserr("Invalid params to remove_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)); + + wserr("Passed remove_user request to main thread\n"); + ret = 1; + } else entry("/api/give_control") { + char decname[1024] = ""; + + param = parse_get(args, "name", &len); + if (len) { + memcpy(buf, param, len); + buf[len] = '\0'; + percent_decode(buf, decname); + } + + if (!decname[0]) + goto nope; + + if (!settings.givecontrolCb(settings.messager, decname)) { + wserr("Invalid params to give_control\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)); + + wserr("Passed give_control request to main thread\n"); + ret = 1; + } + + #undef entry + + return ret; +nope: + sprintf(buf, "HTTP/1.1 400 Bad Request\r\n" + "Server: KasmVNC/4.0\r\n" + "Connection: close\r\n" + "Content-type: text/plain\r\n" + "\r\n" + "400 Bad Request"); + ws_send(ws_ctx, buf, strlen(buf)); + return 1; +} + ws_ctx_t *do_handshake(int sock) { char handshake[4096], response[4096], sha1[29], trailer[17]; char *scheme, *pre; @@ -882,8 +1152,8 @@ ws_ctx_t *do_handshake(int sock) { usleep(10); } - const char *colon; - if ((colon = strchr(settings.basicauth, ':'))) { + unsigned char owner = 0; + if (!settings.disablebasicauth) { const char *hdr = strstr(handshake, "Authorization: Basic "); if (!hdr) { handler_emsg("BasicAuth required, but client didn't send any. 401 Unauth\n"); @@ -908,15 +1178,13 @@ ws_ctx_t *do_handshake(int sock) { tmp[len] = '\0'; len = ws_b64_pton(tmp, response, 256); - char authbuf[4096]; - strncpy(authbuf, settings.basicauth, 4096); - authbuf[4095] = '\0'; + char authbuf[4096] = ""; // Do we need to read it from the file? char *resppw = strchr(response, ':'); if (resppw && *resppw) resppw++; - if (!colon[1] && settings.passwdfile) { + if (settings.passwdfile) { if (resppw && *resppw && resppw - response < 32) { char pwbuf[4096]; struct kasmpasswd_t *set = readkasmpasswd(settings.passwdfile); @@ -938,6 +1206,9 @@ ws_ctx_t *do_handshake(int sock) { snprintf(authbuf, 4096, "%s:%s", set->entries[i].user, set->entries[i].password); authbuf[4095] = '\0'; + + if (set->entries[i].owner) + owner = 1; break; } } @@ -978,9 +1249,28 @@ ws_ctx_t *do_handshake(int sock) { if (!parse_handshake(ws_ctx, handshake)) { handler_emsg("Invalid WS request, maybe a HTTP one\n"); + if (strstr(handshake, "/api/")) { + handler_emsg("HTTP request under /api/\n"); + + if (owner) { + if (ownerapi(ws_ctx, handshake)) + goto done; + } else { + sprintf(response, "HTTP/1.1 401 Unauthorized\r\n" + "Server: KasmVNC/4.0\r\n" + "Connection: close\r\n" + "Content-type: text/plain\r\n" + "\r\n" + "401 Unauthorized"); + ws_send(ws_ctx, response, strlen(response)); + goto done; + } + } + if (settings.httpdir && settings.httpdir[0]) servefile(ws_ctx, handshake); +done: free_ws_ctx(ws_ctx); return NULL; } diff --git a/common/network/websocket.h b/common/network/websocket.h index 3d757f1..fd00987 100644 --- a/common/network/websocket.h +++ b/common/network/websocket.h @@ -1,4 +1,5 @@ #include +#include #define BUFSIZE 65536 #define DBUFSIZE (BUFSIZE * 3) / 4 - 20 @@ -70,10 +71,19 @@ typedef struct { unsigned int handler_id; const char *cert; const char *key; - const char *basicauth; + uint8_t disablebasicauth; const char *passwdfile; int ssl_only; const char *httpdir; + + void *messager; + uint8_t *(*screenshotCb)(void *messager, uint16_t w, uint16_t h, const uint8_t q, + const uint8_t dedup, + uint32_t *len, uint8_t *staging); + 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 (*givecontrolCb)(void *messager, const char name[]); } settings_t; #ifdef __cplusplus diff --git a/common/rfb/Configuration.cxx b/common/rfb/Configuration.cxx index ff03a6f..fdfc4d5 100644 --- a/common/rfb/Configuration.cxx +++ b/common/rfb/Configuration.cxx @@ -433,8 +433,7 @@ bool StringParameter::setParam(const char* v) { if (immutable) return true; if (!v) throw rfb::Exception("setParam() not allowed"); - if (strcasecmp(getName(), "BasicAuth")) // don't log the auth info - vlog.debug("set %s(String) to %s", getName(), v); + vlog.debug("set %s(String) to %s", getName(), v); CharArray oldValue(value); value = strDup(v); return value != 0; diff --git a/common/rfb/EncodeManager.cxx b/common/rfb/EncodeManager.cxx index f5f088c..fb9b8a6 100644 --- a/common/rfb/EncodeManager.cxx +++ b/common/rfb/EncodeManager.cxx @@ -966,7 +966,7 @@ static PixelBuffer *bilinearScale(const PixelBuffer *pb, const uint16_t w, const return newpb; } -static PixelBuffer *progressiveBilinearScale(const PixelBuffer *pb, +PixelBuffer *progressiveBilinearScale(const PixelBuffer *pb, const uint16_t tgtw, const uint16_t tgth, const float tgtdiff) { diff --git a/common/rfb/InputHandler.h b/common/rfb/InputHandler.h index 6c07284..806625f 100644 --- a/common/rfb/InputHandler.h +++ b/common/rfb/InputHandler.h @@ -36,7 +36,9 @@ namespace rfb { rdr::U32 __unused_attr keycode, bool __unused_attr down) { } virtual void pointerEvent(const Point& __unused_attr pos, - int __unused_attr buttonMask) { } + int __unused_attr buttonMask, + const bool __unused_attr skipClick, + const bool __unused_attr skipRelease) { } virtual void clientCutText(const char* __unused_attr str, int __unused_attr len) { } }; diff --git a/common/rfb/SMsgReader.cxx b/common/rfb/SMsgReader.cxx index 7fb3f2c..a71272a 100644 --- a/common/rfb/SMsgReader.cxx +++ b/common/rfb/SMsgReader.cxx @@ -217,7 +217,7 @@ void SMsgReader::readPointerEvent() int mask = is->readU8(); int x = is->readU16(); int y = is->readU16(); - handler->pointerEvent(Point(x, y), mask); + handler->pointerEvent(Point(x, y), mask, false, false); } diff --git a/common/rfb/ServerCore.cxx b/common/rfb/ServerCore.cxx index 900a21f..3b36af0 100644 --- a/common/rfb/ServerCore.cxx +++ b/common/rfb/ServerCore.cxx @@ -163,6 +163,19 @@ rfb::StringParameter rfb::Server::DLP_ClipLog ("DLP_Log", "Log clipboard/kbd actions. Accepts off, info or verbose", "off"); +rfb::StringParameter rfb::Server::DLP_Region +("DLP_Region", + "Black out anything outside this region", + ""); + +rfb::BoolParameter rfb::Server::DLP_RegionAllowClick +("DLP_RegionAllowClick", + "Allow clicks inside the blacked-out region", + false); +rfb::BoolParameter rfb::Server::DLP_RegionAllowRelease +("DLP_RegionAllowRelease", + "Allow click releases inside the blacked-out region", + true); rfb::StringParameter rfb::Server::maxVideoResolution ("MaxVideoResolution", diff --git a/common/rfb/ServerCore.h b/common/rfb/ServerCore.h index bd97265..280596f 100644 --- a/common/rfb/ServerCore.h +++ b/common/rfb/ServerCore.h @@ -49,6 +49,9 @@ namespace rfb { static IntParameter DLP_ClipDelay; static IntParameter DLP_KeyRateLimit; static StringParameter DLP_ClipLog; + static StringParameter DLP_Region; + static BoolParameter DLP_RegionAllowClick; + static BoolParameter DLP_RegionAllowRelease; static IntParameter jpegVideoQuality; static IntParameter webpVideoQuality; static StringParameter maxVideoResolution; diff --git a/common/rfb/VNCSConnectionST.cxx b/common/rfb/VNCSConnectionST.cxx index 467e8f9..31bd16e 100644 --- a/common/rfb/VNCSConnectionST.cxx +++ b/common/rfb/VNCSConnectionST.cxx @@ -48,7 +48,7 @@ static LogWriter vlog("VNCSConnST"); static Cursor emptyCursor(0, 0, Point(0, 0), NULL); -extern rfb::StringParameter basicauth; +extern rfb::BoolParameter disablebasicauth; VNCSConnectionST::VNCSConnectionST(VNCServerST* server_, network::Socket *s, bool reverse) @@ -637,7 +637,7 @@ void VNCSConnectionST::setPixelFormat(const PixelFormat& pf) setCursor(); } -void VNCSConnectionST::pointerEvent(const Point& pos, int buttonMask) +void VNCSConnectionST::pointerEvent(const Point& pos, int buttonMask, const bool skipClick, const bool skipRelease) { pointerEventTime = lastEventTime = time(0); server->lastUserInputTime = lastEventTime; @@ -649,7 +649,23 @@ void VNCSConnectionST::pointerEvent(const Point& pos, int buttonMask) server->pointerClient = this; else server->pointerClient = 0; - server->desktop->pointerEvent(pointerEventPos, buttonMask); + + bool skipclick = false, skiprelease = false; + if (server->DLPRegion.enabled) { + rdr::U16 x1, y1, x2, y2; + server->translateDLPRegion(x1, y1, x2, y2); + + if (pos.x < x1 || pos.x >= x2 || + pos.y < y1 || pos.y >= y2) { + + if (!Server::DLP_RegionAllowClick) + skipclick = true; + if (!Server::DLP_RegionAllowRelease) + skiprelease = true; + } + } + + server->desktop->pointerEvent(pointerEventPos, buttonMask, skipclick, skiprelease); } } @@ -1028,13 +1044,12 @@ bool VNCSConnectionST::isShiftPressed() bool VNCSConnectionST::getPerms(bool &write, bool &owner) const { bool found = false; - const char *colon = strchr(basicauth, ':'); - if (!colon || colon[1]) { - // We're running without basicauth, or with both user:pass on the command line + if (disablebasicauth) { + // We're running without basicauth write = true; return true; } - if (colon && !colon[1] && user[0]) { + if (user[0]) { struct kasmpasswd_t *set = readkasmpasswd(kasmpasswdpath); unsigned i; for (i = 0; i < set->num; i++) { diff --git a/common/rfb/VNCSConnectionST.h b/common/rfb/VNCSConnectionST.h index 3eff9ad..d1e1267 100644 --- a/common/rfb/VNCSConnectionST.h +++ b/common/rfb/VNCSConnectionST.h @@ -168,7 +168,7 @@ namespace rfb { virtual void queryConnection(const char* userName); virtual void clientInit(bool shared); virtual void setPixelFormat(const PixelFormat& pf); - virtual void pointerEvent(const Point& pos, int buttonMask); + virtual void pointerEvent(const Point& pos, int buttonMask, const bool skipClick, const bool skipRelease); virtual void keyEvent(rdr::U32 keysym, rdr::U32 keycode, bool down); virtual void clientCutText(const char* str, int len); virtual void framebufferUpdateRequest(const Rect& r, bool incremental); diff --git a/common/rfb/VNCServerST.cxx b/common/rfb/VNCServerST.cxx index 1ac8597..7bec158 100644 --- a/common/rfb/VNCServerST.cxx +++ b/common/rfb/VNCServerST.cxx @@ -51,6 +51,8 @@ #include #include +#include + #include #include #include @@ -79,23 +81,116 @@ EncCache VNCServerST::encCache; // static char kasmpasswdpath[4096]; -extern rfb::StringParameter basicauth; // -=- Constructors/Destructor +static void mixedPercentages() { + slog.error("Mixing percentages and absolute values in DLP_Region is not allowed"); + exit(1); +} + +static void parseRegionPart(const bool percents, rdr::U16 &pcdest, int &dest, + char **inptr) { + char *nextptr, *ptr; + ptr = *inptr; + int val = strtol(ptr, &nextptr, 10); + if (!*ptr || ptr == nextptr) { + slog.error("Invalid value for DLP_Region"); + exit(1); + } + ptr = nextptr; + if (*ptr == '%') { + if (!percents) + mixedPercentages(); + pcdest = val; + + if (val < 0 || val > 100) { + slog.error("Percent must be 0-100"); + exit(1); + } + + ptr++; + } else if (percents) { + mixedPercentages(); + } + dest = val; + + for (; *ptr && *ptr == ','; ptr++); + + *inptr = ptr; +} + VNCServerST::VNCServerST(const char* name_, SDesktop* desktop_) : blHosts(&blacklist), desktop(desktop_), desktopStarted(false), - blockCounter(0), pb(0), ledState(ledUnknown), + blockCounter(0), pb(0), blackedpb(0), ledState(ledUnknown), name(strDup(name_)), pointerClient(0), comparer(0), cursor(new Cursor(0, 0, Point(), NULL)), renderedCursorInvalid(false), queryConnectionHandler(0), keyRemapper(&KeyRemapper::defInstance), lastConnectionTime(0), disableclients(false), - frameTimer(this) + frameTimer(this), apimessager(NULL) { lastUserInputTime = lastDisconnectTime = time(0); slog.debug("creating single-threaded server %s", name.buf); + DLPRegion.enabled = DLPRegion.percents = false; + + if (Server::DLP_Region[0]) { + unsigned len = strlen(Server::DLP_Region); + unsigned i; + unsigned commas = 0; + int val; + char *ptr, *nextptr; + + for (i = 0; i < len; i++) { + if (Server::DLP_Region[i] == ',') + commas++; + } + + if (commas != 3) { + slog.error("DLP_Region must contain four values"); + exit(1); + } + + ptr = (char *) (const char *) Server::DLP_Region; + + val = strtol(ptr, &nextptr, 10); + if (!*ptr || ptr == nextptr) { + slog.error("Invalid value for DLP_Region"); + exit(1); + } + ptr = nextptr; + if (*ptr == '%') { + DLPRegion.percents = true; + DLPRegion.pcx1 = val; + ptr++; + } + DLPRegion.x1 = val; + + for (; *ptr && *ptr == ','; ptr++); + + parseRegionPart(DLPRegion.percents, DLPRegion.pcy1, DLPRegion.y1, + &ptr); + parseRegionPart(DLPRegion.percents, DLPRegion.pcx2, DLPRegion.x2, + &ptr); + parseRegionPart(DLPRegion.percents, DLPRegion.pcy2, DLPRegion.y2, + &ptr); + + // Validity checks + if (!DLPRegion.percents) { + if (DLPRegion.x1 > 0 && DLPRegion.x2 > 0 && DLPRegion.x2 <= DLPRegion.x1) { + slog.error("DLP_Region x2 must be > x1"); + exit(1); + } + if (DLPRegion.y1 > 0 && DLPRegion.y2 > 0 && DLPRegion.y2 <= DLPRegion.y1) { + slog.error("DLP_Region y2 must be > y1"); + exit(1); + } + } + + DLPRegion.enabled = 1; + } + kasmpasswdpath[0] = '\0'; wordexp_t wexp; if (!wordexp(rfb::Server::kasmPasswordFile, &wexp, WRDE_NOCMD)) @@ -639,6 +734,138 @@ int VNCServerST::msToNextUpdate() return frameTimer.getRemainingMs(); } +static void checkAPIMessages(network::GetAPIMessager *apimessager) +{ + if (pthread_mutex_lock(&apimessager->userMutex)) + return; + + const unsigned num = apimessager->actionQueue.size(); + unsigned i; + for (i = 0; i < num; i++) { + 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; + } + + if (set) { + free(set->entries); + free(set); + } + } + + apimessager->actionQueue.clear(); + pthread_mutex_unlock(&apimessager->userMutex); +} + +void VNCServerST::translateDLPRegion(rdr::U16 &x1, rdr::U16 &y1, rdr::U16 &x2, rdr::U16 &y2) const +{ + if (DLPRegion.percents) { + x1 = DLPRegion.pcx1 ? DLPRegion.pcx1 * pb->getRect().width() / 100 : 0; + y1 = DLPRegion.pcy1 ? DLPRegion.pcy1 * pb->getRect().height() / 100 : 0; + x2 = DLPRegion.pcx2 ? (100 - DLPRegion.pcx2) * pb->getRect().width() / 100 : pb->getRect().width(); + y2 = DLPRegion.pcy2 ? (100 - DLPRegion.pcy2) * pb->getRect().height() / 100 : pb->getRect().height(); + } else { + x1 = abs(DLPRegion.x1); + y1 = abs(DLPRegion.y1); + x2 = pb->getRect().width(); + y2 = pb->getRect().height(); + + if (DLPRegion.x2 < 0) + x2 += DLPRegion.x2; + else if (DLPRegion.x2 > 0) + x2 = DLPRegion.x2; + + if (DLPRegion.y2 < 0) + y2 += DLPRegion.y2; + else if (DLPRegion.y2 > 0) + y2 = DLPRegion.y2; + } + + if (y2 > pb->getRect().height()) + y2 = pb->getRect().height() - 1; + if (x2 > pb->getRect().width()) + x2 = pb->getRect().width() - 1; + + //slog.info("DLP_Region vals %u,%u %u,%u", x1, y1, x2, y2); +} + +void VNCServerST::blackOut() +{ + // Compute the region, since the resolution may have changed + rdr::U16 x1, y1, x2, y2; + + translateDLPRegion(x1, y1, x2, y2); + + if (blackedpb) + delete blackedpb; + blackedpb = new ManagedPixelBuffer(pb->getPF(), pb->getRect().width(), pb->getRect().height()); + + int stride; + const rdr::U8 *src = pb->getBuffer(pb->getRect(), &stride); + rdr::U8 *data = blackedpb->getBufferRW(pb->getRect(), &stride); + stride *= 4; + + memcpy(data, src, stride * pb->getRect().height()); + + rdr::U16 y; + const rdr::U16 w = pb->getRect().width(); + const rdr::U16 h = pb->getRect().height(); + for (y = 0; y < h; y++) { + if (y < y1 || y > y2) { + memset(data, 0, stride); + } else { + if (x1) + memset(data, 0, x1 * 4); + if (x2) + memset(&data[x2 * 4], 0, (w - x2) * 4); + } + + data += stride; + } +} + // writeUpdate() is called on a regular interval in order to see what // updates are pending and propagates them to the update tracker for // each client. It uses the ComparingUpdateTracker's compare() method @@ -656,6 +883,11 @@ void VNCServerST::writeUpdate() assert(blockCounter == 0); assert(desktopStarted); + if (DLPRegion.enabled) { + comparer->enable_copyrect(false); + blackOut(); + } + comparer->getUpdateInfo(&ui, pb->getRect()); toCheck = ui.changed.union_(ui.copied); @@ -709,6 +941,12 @@ void VNCServerST::writeUpdate() } } + if (apimessager) { + apimessager->mainUpdateScreen(pb); + + checkAPIMessages(apimessager); + } + for (ci = clients.begin(); ci != clients.end(); ci = ci_next) { ci_next = ci; ci_next++; diff --git a/common/rfb/VNCServerST.h b/common/rfb/VNCServerST.h index ef6e3e0..7872141 100644 --- a/common/rfb/VNCServerST.h +++ b/common/rfb/VNCServerST.h @@ -43,6 +43,7 @@ namespace rfb { class ListConnInfo; class PixelBuffer; class KeyRemapper; + class network::GetAPIMessager; class VNCServerST : public VNCServer, public Timer::Callback, @@ -94,7 +95,7 @@ namespace rfb { virtual void setPixelBuffer(PixelBuffer* pb, const ScreenSet& layout); virtual void setPixelBuffer(PixelBuffer* pb); virtual void setScreenLayout(const ScreenSet& layout); - virtual PixelBuffer* getPixelBuffer() const { return pb; } + virtual PixelBuffer* getPixelBuffer() const { if (DLPRegion.enabled && blackedpb) return blackedpb; else return pb; } virtual void serverCutText(const char* str, int len); virtual void add_changed(const Region ®ion); virtual void add_copied(const Region &dest, const Point &delta); @@ -186,6 +187,8 @@ namespace rfb { bool getDisable() { return disableclients;}; void setDisable(bool disable) { disableclients = disable;}; + void setAPIMessager(network::GetAPIMessager *msgr) { apimessager = msgr; } + protected: friend class VNCSConnectionST; @@ -206,6 +209,7 @@ namespace rfb { bool desktopStarted; int blockCounter; PixelBuffer* pb; + ManagedPixelBuffer *blackedpb; ScreenSet screenLayout; unsigned int ledState; @@ -232,6 +236,7 @@ namespace rfb { void stopFrameClock(); int msToNextUpdate(); void writeUpdate(); + void blackOut(); Region getPendingRegion(); const RenderedCursor* getRenderedCursor(); @@ -251,6 +256,17 @@ namespace rfb { Timer frameTimer; int inotifyfd; + + network::GetAPIMessager *apimessager; + + struct { + bool enabled; + int x1, y1, x2, y2; + bool percents; + rdr::U16 pcx1, pcy1, pcx2, pcy2; + } DLPRegion; + + void translateDLPRegion(rdr::U16 &x1, rdr::U16 &y1, rdr::U16 &x2, rdr::U16 &y2) const; }; }; diff --git a/unix/vncserver b/unix/vncserver index 04c758e..68b4ad9 100644 --- a/unix/vncserver +++ b/unix/vncserver @@ -465,8 +465,7 @@ sub LoadConfig { } # change username option to basicAuth and add colon as required by Xvnc, password will be taken from file if ($k = "username") { - $config{"basicauth"} = "$v:"; - $vncUserName = $v; + next; } else { $config{$k} = $v; } diff --git a/unix/xserver/hw/vnc/Input.c b/unix/xserver/hw/vnc/Input.c index 2519ad3..f8ac3fd 100644 --- a/unix/xserver/hw/vnc/Input.c +++ b/unix/xserver/hw/vnc/Input.c @@ -155,7 +155,8 @@ static void enqueueEvents(DeviceIntPtr dev, int n) } #endif /* XORG < 111 */ -void vncPointerButtonAction(int buttonMask) +void vncPointerButtonAction(int buttonMask, const unsigned char skipclick, + const unsigned char skiprelease) { int i; #if XORG < 111 @@ -169,6 +170,14 @@ void vncPointerButtonAction(int buttonMask) if ((buttonMask ^ oldButtonMask) & (1 << i)) { int action = (buttonMask & (1<getFd(), screenIndex, true, false); + if ((*i)->getMessager()) + server->setAPIMessager((*i)->getMessager()); } } @@ -416,11 +418,12 @@ void XserverDesktop::approveConnection(uint32_t opaqueId, bool accept, // SDesktop callbacks -void XserverDesktop::pointerEvent(const Point& pos, int buttonMask) +void XserverDesktop::pointerEvent(const Point& pos, int buttonMask, + const bool skipClick, const bool skipRelease) { vncPointerMove(pos.x + vncGetScreenX(screenIndex), pos.y + vncGetScreenY(screenIndex)); - vncPointerButtonAction(buttonMask); + vncPointerButtonAction(buttonMask, skipClick, skipRelease); } void XserverDesktop::clientCutText(const char* str, int len) diff --git a/unix/xserver/hw/vnc/XserverDesktop.h b/unix/xserver/hw/vnc/XserverDesktop.h index 014fcb5..ec9bf37 100644 --- a/unix/xserver/hw/vnc/XserverDesktop.h +++ b/unix/xserver/hw/vnc/XserverDesktop.h @@ -86,7 +86,8 @@ public: const char* rejectMsg=0); // rfb::SDesktop callbacks - virtual void pointerEvent(const rfb::Point& pos, int buttonMask); + virtual void pointerEvent(const rfb::Point& pos, int buttonMask, + const bool skipClick, const bool skipRelease); virtual void keyEvent(rdr::U32 keysym, rdr::U32 keycode, bool down); virtual void clientCutText(const char* str, int len); virtual unsigned int setScreenLayout(int fb_width, int fb_height, diff --git a/unix/xserver/hw/vnc/Xvnc.man b/unix/xserver/hw/vnc/Xvnc.man index f8ace17..809fc1c 100644 --- a/unix/xserver/hw/vnc/Xvnc.man +++ b/unix/xserver/hw/vnc/Xvnc.man @@ -280,6 +280,20 @@ Kasm exposes a few settings to the client the standard VNC does not. This param lets the server ignore those. . .TP +.B \-DLP_Region \fIx1,y1,x2,y2\fP +Black out anything outside this region. x1,y1 is the upper-left corner, +and x2,y2 the lower-left. In addition to absolute pixel values, percentages +are allowed, zero means "default", and a negative number means "border". +. +.TP +.B \-DLP_RegionAllowClick \fIbool\fP +Allow clicks inside the blacked-out region. +. +.TP +.B \-DLP_RegionAllowRelease \fIbool\fP +Allow click releases inside the blacked-out region. +. +.TP .B \-DLP_ClipSendMax \fIbytes\fP Limit clipboard bytes to send to clients in one transaction. Default 10,000. 0 disables the limit, use \fBSendCutText\fP to disable clipboard sending entirely. @@ -325,9 +339,9 @@ are in the same file, use \fB-cert\fP. Require SSL for websocket connections. Default off, non-SSL allowed. . .TP -.B \-basicAuth \fIuser:pass\fP -Username and password for websocket connections. Default empty, no authentication required. -If the password is empty, read it from the \fB-KasmPasswordFile\fP. +.B \-disableBasicAuth +Disable basic auth for websocket connections. Default enabled, details read from +the \fB-KasmPasswordFile\fP. . .TP .B \-SecurityTypes \fIsec-types\fP diff --git a/unix/xserver/hw/vnc/vncExtInit.cc b/unix/xserver/hw/vnc/vncExtInit.cc index 057ceb5..99e00dc 100644 --- a/unix/xserver/hw/vnc/vncExtInit.cc +++ b/unix/xserver/hw/vnc/vncExtInit.cc @@ -89,7 +89,7 @@ rfb::IntParameter websocketPort("websocketPort", "websocket port to listen for", rfb::StringParameter cert("cert", "SSL pem cert to use for websocket connections", ""); rfb::StringParameter certkey("key", "SSL pem key to use for websocket connections (if separate)", ""); rfb::BoolParameter sslonly("sslOnly", "Require SSL for websockets", false); -rfb::StringParameter basicauth("BasicAuth", "user:pass for HTTP basic auth for websockets", ""); +rfb::BoolParameter disablebasicauth("DisableBasicAuth", "Disable basic auth for websockets", false); rfb::StringParameter interface("interface", "listen on the specified network address", "all"); @@ -225,7 +225,7 @@ void vncExtensionInit(void) if (!noWebsocket) network::createWebsocketListeners(&listeners, websocketPort, localhostOnly ? "local" : addr, - sslonly, cert, certkey, basicauth, httpDir); + sslonly, cert, certkey, disablebasicauth, httpDir); else if (localhostOnly) network::createLocalTcpListeners(&listeners, port); else