diff --git a/builder/build-and-test-deb b/builder/build-and-test-deb new file mode 100755 index 0000000..3728692 --- /dev/null +++ b/builder/build-and-test-deb @@ -0,0 +1,16 @@ +#!/bin/bash + +set -e + +os="$1" +codename="$2" + +if [[ -z "$os" ]] || [[ -z "$codename" ]]; then + echo "Usage: $0 " >&2 + exit 1 +fi + +cd "$(dirname "$0")" + +./build-tarball "$os" "$codename" && ./build-deb "$os" "$codename" && \ + ./test-deb "$os" "$codename" diff --git a/builder/build-and-test-rpm b/builder/build-and-test-rpm new file mode 100755 index 0000000..e6d85b0 --- /dev/null +++ b/builder/build-and-test-rpm @@ -0,0 +1,16 @@ +#!/bin/bash + +set -e + +os="$1" +codename="$2" + +if [[ -z "$os" ]] || [[ -z "$codename" ]]; then + echo "Usage: $0 " >&2 + exit 1 +fi + +cd "$(dirname "$0")" + +./build-tarball "$os" "$codename" && ./build-rpm "$os" "$codename" && \ + ./test-rpm "$os" "$codename" diff --git a/builder/startup/vnc_startup.sh b/builder/startup/vnc_startup.sh index b2c3c59..93c6079 100755 --- a/builder/startup/vnc_startup.sh +++ b/builder/startup/vnc_startup.sh @@ -34,13 +34,31 @@ detect_cert_location() { fi } +add_vnc_user() { + local username="$1" + local password="$2" + local permission_option="$3" + + echo "Adding user $username" + echo -e "$password\n$password" | kasmvncpasswd $permission_option \ + -u "$username" $HOME/.kasmpasswd +} + ## resolve_vnc_connection VNC_IP=$(hostname -i) # first entry is control, second is view (if only one is valid for both) mkdir -p "$HOME/.vnc" PASSWD_PATH="$HOME/.vnc/passwd" -echo "$VNC_PW" | kasmvncpasswd -f > $HOME/.kasmpasswd +# echo -e "$VNC_PW\n$VNC_PW" | kasmvncpasswd -w -u $VNC_USER $HOME/.kasmpasswd +add_vnc_user "$VNC_USER" "$VNC_PW" "-w" +add_vnc_user "$VNC_USER-ro" "$VNC_PW" +add_vnc_user "$VNC_USER-owner" "$VNC_PW" "-o" +add_vnc_user "$VNC_USER-to-delete" "$VNC_PW" + +kasmvncpasswd -n -u "$VNC_USER-owner" -w $HOME/.kasmpasswd +kasmvncpasswd -d -u "$VNC_USER-to-delete" $HOME/.kasmpasswd + chmod 0600 $HOME/.kasmpasswd openssl req -x509 -nodes -days 3650 -newkey rsa:2048 -keyout $HOME/.vnc/self.pem -out $HOME/.vnc/self.pem -subj "/C=US/ST=VA/L=None/O=None/OU=DoFu/CN=kasm/emailAddress=none@none.none" @@ -69,9 +87,10 @@ vncserver -kill $DISPLAY &> $HOME/.vnc/vnc_startup.log \ detect_www_dir detect_cert_location +[ -n "$KASMVNC_VERBOSE_LOGGING" ] && verbose_logging_option="-log *:stderr:100" echo -e "start vncserver with param: VNC_COL_DEPTH=$VNC_COL_DEPTH, VNC_RESOLUTION=$VNC_RESOLUTION\n..." -vncserver $DISPLAY -depth $VNC_COL_DEPTH -geometry $VNC_RESOLUTION -FrameRate=$MAX_FRAME_RATE -websocketPort $VNC_PORT $cert_option -sslOnly -interface 0.0.0.0 $VNCOPTIONS $package_www_dir_option #&> $STARTUPDIR/no_vnc_startup.log +vncserver $DISPLAY -depth $VNC_COL_DEPTH -geometry $VNC_RESOLUTION -FrameRate=$MAX_FRAME_RATE -websocketPort $VNC_PORT $cert_option -sslOnly -interface 0.0.0.0 $VNCOPTIONS $package_www_dir_option $verbose_logging_option #&> $STARTUPDIR/no_vnc_startup.log PID_SUN=$! diff --git a/builder/test-deb b/builder/test-deb index dacd3d0..9114a3e 100755 --- a/builder/test-deb +++ b/builder/test-deb @@ -8,5 +8,7 @@ cd "$(dirname "$0")" docker build --build-arg KASMVNC_PACKAGE_DIR="build/${os_codename}" \ -t kasmvnctester_${os}:$os_codename \ -f dockerfile.${os}_${os_codename}.deb.test . -echo docker run -it -p 443:8443 --rm -e "VNC_USER=foo" -e "VNC_PW=bar" \ +docker run -it -p 443:8443 --rm \ + -e KASMVNC_VERBOSE_LOGGING=$KASMVNC_VERBOSE_LOGGING \ + -e "VNC_USER=foo" -e "VNC_PW=foobar" \ kasmvnctester_${os}:$os_codename diff --git a/builder/test-deb-barebones b/builder/test-deb-barebones index 37f9e1b..cb94966 100755 --- a/builder/test-deb-barebones +++ b/builder/test-deb-barebones @@ -11,4 +11,5 @@ docker build --build-arg KASMVNC_PACKAGE_DIR="build/${os_codename}" \ -f dockerfile.${os}_${os_codename}.barebones.deb.test . echo echo "You will be asked to set password. User name is docker." -docker run -it -p 443:8443 --rm kasmvnctester_barebones_${os}:$os_codename +docker run -it -p 443:8443 --rm -e "VNC_USER=foo" -e "VNC_PW=foobar" \ + kasmvnctester_barebones_${os}:$os_codename diff --git a/builder/test-rpm b/builder/test-rpm index b352962..719b65f 100755 --- a/builder/test-rpm +++ b/builder/test-rpm @@ -10,5 +10,8 @@ docker build --build-arg \ KASMVNC_PACKAGE_DIR="build/${os}_${os_codename}" \ -t kasmvnctester_${os}:$os_codename \ -f dockerfile.${os}_${os_codename}.rpm.test . -echo docker run -it -p 443:8443 --rm -e "VNC_USER=foo" -e "VNC_PW=bar" \ + +docker run -it -p 443:8443 --rm \ + -e KASMVNC_VERBOSE_LOGGING=$KASMVNC_VERBOSE_LOGGING \ + -e "VNC_USER=foo" -e "VNC_PW=foobar" \ kasmvnctester_${os}:$os_codename diff --git a/builder/test-rpm-barebones b/builder/test-rpm-barebones index 68ec718..7ad42cf 100755 --- a/builder/test-rpm-barebones +++ b/builder/test-rpm-barebones @@ -9,5 +9,5 @@ os_codename="${2:-core}" docker build --build-arg KASMVNC_PACKAGE_DIR="build/${os}_${os_codename}" \ -t kasmvnctester_barebones_${os}:$os_codename \ -f dockerfile.${os}_${os_codename}.barebones.rpm.test . -docker run -it -p 443:8443 --rm -e "VNC_USER=foo" -e "VNC_PW=bar" \ +docker run -it -p 443:8443 --rm -e "VNC_USER=foo" -e "VNC_PW=foobar" \ kasmvnctester_barebones_${os}:$os_codename diff --git a/common/network/CMakeLists.txt b/common/network/CMakeLists.txt index 485682b..51a32a9 100644 --- a/common/network/CMakeLists.txt +++ b/common/network/CMakeLists.txt @@ -1,10 +1,11 @@ -include_directories(${CMAKE_SOURCE_DIR}/common) +include_directories(${CMAKE_SOURCE_DIR}/common ${CMAKE_SOURCE_DIR}/unix/kasmvncpasswd) set(NETWORK_SOURCES Socket.cxx TcpSocket.cxx websocket.c - websockify.c) + websockify.c + ${CMAKE_SOURCE_DIR}/unix/kasmvncpasswd/kasmpasswd.c) if(NOT WIN32) set(NETWORK_SOURCES ${NETWORK_SOURCES} UnixSocket.cxx) diff --git a/common/network/TcpSocket.cxx b/common/network/TcpSocket.cxx index de667d0..c6a7e14 100644 --- a/common/network/TcpSocket.cxx +++ b/common/network/TcpSocket.cxx @@ -124,11 +124,20 @@ WebSocket::WebSocket(int sock) : Socket(sock) } char* WebSocket::getPeerAddress() { - return rfb::strDup("websocket"); + struct sockaddr_un addr; + socklen_t len = sizeof(struct sockaddr_un); + if (getpeername(getFd(), (struct sockaddr *) &addr, &len) != 0) { + vlog.error("unable to get peer name for socket"); + return rfb::strDup("websocket"); + } + return rfb::strDup(addr.sun_path + 1); } char* WebSocket::getPeerEndpoint() { - return rfb::strDup("websocket"); + char buf[1024]; + sprintf(buf, "%s::websocket", getPeerAddress()); + + return rfb::strDup(buf); } // -=- TcpSocket diff --git a/common/network/websocket.c b/common/network/websocket.c index ccf492d..21e22ee 100644 --- a/common/network/websocket.c +++ b/common/network/websocket.c @@ -30,6 +30,7 @@ #include /* md5 hash */ #include /* sha1 hash */ #include "websocket.h" +#include "kasmpasswd.h" /* * Global state @@ -59,28 +60,28 @@ void fatal(char *msg) exit(1); } -/* resolve host with also IP address parsing */ -int resolve_host(struct in_addr *sin_addr, const char *hostname) -{ - if (!inet_aton(hostname, sin_addr)) { - struct addrinfo *ai, *cur; - struct addrinfo hints; - memset(&hints, 0, sizeof(hints)); - hints.ai_family = AF_INET; - if (getaddrinfo(hostname, NULL, &hints, &ai)) - return -1; - for (cur = ai; cur; cur = cur->ai_next) { - if (cur->ai_family == AF_INET) { - *sin_addr = ((struct sockaddr_in *)cur->ai_addr)->sin_addr; - freeaddrinfo(ai); - return 0; - } - } - freeaddrinfo(ai); - return -1; - } - return 0; -} +/* resolve host with also IP address parsing */ +int resolve_host(struct in_addr *sin_addr, const char *hostname) +{ + if (!inet_aton(hostname, sin_addr)) { + struct addrinfo *ai, *cur; + struct addrinfo hints; + memset(&hints, 0, sizeof(hints)); + hints.ai_family = AF_INET; + if (getaddrinfo(hostname, NULL, &hints, &ai)) + return -1; + for (cur = ai; cur; cur = cur->ai_next) { + if (cur->ai_family == AF_INET) { + *sin_addr = ((struct sockaddr_in *)cur->ai_addr)->sin_addr; + freeaddrinfo(ai); + return 0; + } + } + freeaddrinfo(ai); + return -1; + } + return 0; +} /* @@ -107,7 +108,7 @@ ssize_t ws_send(ws_ctx_t *ctx, const void *buf, size_t len) { ws_ctx_t *alloc_ws_ctx() { ws_ctx_t *ctx; - if (! (ctx = malloc(sizeof(ws_ctx_t))) ) + if (! (ctx = calloc(sizeof(ws_ctx_t), 1)) ) { fatal("malloc()"); } if (! (ctx->cin_buf = malloc(BUFSIZE)) ) @@ -308,7 +309,7 @@ int decode_hixie(char *src, size_t srclength, *left = srclength; if (srclength == 2 && - (src[0] == '\xff') && + (src[0] == '\xff') && (src[1] == '\x00')) { // client sent orderly close frame *opcode = 0x8; // Close frame @@ -326,7 +327,7 @@ int decode_hixie(char *src, size_t srclength, return len; } retlen += len; - start = end + 2; // Skip '\xff' end and '\x00' start + start = end + 2; // Skip '\xff' end and '\x00' start framecount++; } while (end < (src+srclength-1)); if (framecount > 1) { @@ -399,7 +400,7 @@ int decode_hybi(unsigned char *src, size_t srclength, int i = 0, len, framecount = 0; size_t remaining = 0; unsigned int target_offset = 0, hdr_length = 0, payload_length = 0; - + *left = srclength; frame = src; @@ -499,7 +500,7 @@ int decode_hybi(unsigned char *src, size_t srclength, snprintf(cntstr, 3, "%d", framecount); traffic(cntstr); } - + *left = remaining; return target_offset; } @@ -543,7 +544,7 @@ int parse_handshake(ws_ctx_t *ws_ctx, char *handshake) { end = strstr(start, "\r\n"); strncpy(headers->origin, start, end-start); headers->origin[end-start] = '\0'; - + start = strstr(handshake, "\r\nSec-WebSocket-Version: "); if (start) { // HyBi/RFC 6455 @@ -560,14 +561,14 @@ int parse_handshake(ws_ctx_t *ws_ctx, char *handshake) { end = strstr(start, "\r\n"); strncpy(headers->key1, start, end-start); headers->key1[end-start] = '\0'; - + start = strstr(handshake, "\r\nConnection: "); if (!start) { return 0; } start += 14; end = strstr(start, "\r\n"); strncpy(headers->connection, start, end-start); headers->connection[end-start] = '\0'; - + start = strstr(handshake, "\r\nSec-WebSocket-Protocol: "); if (!start) { return 0; } start += 26; @@ -592,7 +593,7 @@ int parse_handshake(ws_ctx_t *ws_ctx, char *handshake) { end = strstr(start, "\r\n"); strncpy(headers->key1, start, end-start); headers->key1[end-start] = '\0'; - + start = strstr(handshake, "\r\nSec-WebSocket-Key2: "); if (!start) { return 0; } start += 22; @@ -916,34 +917,46 @@ ws_ctx_t *do_handshake(int sock) { if (resppw && *resppw) resppw++; if (!colon[1] && settings.passwdfile) { - if (resppw && *resppw) { + if (resppw && *resppw && resppw - response < 32) { char pwbuf[4096]; - FILE *f = fopen(settings.passwdfile, "r"); - if (f) { - handler_emsg("BasicAuth reading password from %s\n", settings.passwdfile); - const unsigned len = fread(pwbuf, 1, 4096, f); - fclose(f); - pwbuf[4095] = '\0'; - if (len < 4096) - pwbuf[len] = '\0'; - - snprintf(authbuf, 4096, "%s%s", settings.basicauth, pwbuf); - authbuf[4095] = '\0'; - - struct crypt_data cdata; - cdata.initialized = 0; - - const char *encrypted = crypt_r(resppw, "$5$kasm$", &cdata); - *resppw = '\0'; - - snprintf(pwbuf, 4096, "%s%s", response, encrypted); - pwbuf[4095] = '\0'; - strcpy(response, pwbuf); - } else { - fprintf(stderr, " websocket %d: Error: BasicAuth configured to read password from file %s, but the file doesn't exist\n", + struct kasmpasswd_t *set = readkasmpasswd(settings.passwdfile); + if (!set->num) { + fprintf(stderr, " websocket %d: Error: BasicAuth configured to read password from file %s, but the file doesn't exist or has no valid users\n", wsthread_handler_id, settings.passwdfile); + } else { + unsigned i; + char inuser[32]; + unsigned char found = 0; + memcpy(inuser, response, resppw - response - 1); + inuser[resppw - response - 1] = '\0'; + + for (i = 0; i < set->num; i++) { + if (!strcmp(set->entries[i].user, inuser)) { + found = 1; + strcpy(ws_ctx->user, inuser); + snprintf(authbuf, 4096, "%s:%s", set->entries[i].user, + set->entries[i].password); + authbuf[4095] = '\0'; + break; + } + } + + if (!found) + handler_emsg("BasicAuth user %s not found\n", inuser); } + free(set->entries); + free(set); + + struct crypt_data cdata; + cdata.initialized = 0; + + const char *encrypted = crypt_r(resppw, "$5$kasm$", &cdata); + *resppw = '\0'; + + snprintf(pwbuf, 4096, "%s%s", response, encrypted); + pwbuf[4095] = '\0'; + strcpy(response, pwbuf); } else { // Client tried an empty password, just fail them response[0] = '\0'; @@ -1001,7 +1014,7 @@ ws_ctx_t *do_handshake(int sock) { snprintf(response, sizeof(response), SERVER_HANDSHAKE_HIXIE, pre, headers->origin, pre, scheme, headers->host, headers->path, pre, "base64", trailer); } - + //handler_msg("response: %s\n", response); ws_send(ws_ctx, response, strlen(response)); @@ -1018,7 +1031,6 @@ void *subthread(void *ptr) { const int csock = pass->csock; wsthread_handler_id = pass->id; - free((void *) pass); ws_ctx_t *ws_ctx; @@ -1028,11 +1040,14 @@ void *subthread(void *ptr) { goto out; // Child process exits } + memcpy(ws_ctx->ip, pass->ip, sizeof(pass->ip)); + proxy_handler(ws_ctx); if (pipe_error) { handler_emsg("Closing due to SIGPIPE\n"); } out: + free((void *) pass); if (ws_ctx) { ws_socket_free(ws_ctx); @@ -1068,12 +1083,13 @@ void *start_server(void *unused) { error("ERROR on accept"); continue; } + struct wspass_t *pass = calloc(1, sizeof(struct wspass_t)); + inet_ntop(cli_addr.sin_family, &cli_addr.sin_addr, pass->ip, sizeof(pass->ip)); fprintf(stderr, " websocket %d: got client connection from %s\n", settings.handler_id, - inet_ntoa(cli_addr.sin_addr)); + pass->ip); pthread_t tid; - struct wspass_t *pass = calloc(1, sizeof(struct wspass_t)); pass->id = settings.handler_id; pass->csock = csock; pthread_create(&tid, NULL, subthread, pass); diff --git a/common/network/websocket.h b/common/network/websocket.h index 18a9b0d..3d757f1 100644 --- a/common/network/websocket.h +++ b/common/network/websocket.h @@ -53,11 +53,15 @@ typedef struct { char *cout_buf; char *tin_buf; char *tout_buf; + + char user[32]; + char ip[64]; } ws_ctx_t; struct wspass_t { int csock; unsigned id; + char ip[64]; }; typedef struct { diff --git a/common/network/websockify.c b/common/network/websockify.c index fe2f018..aaabb9a 100644 --- a/common/network/websockify.c +++ b/common/network/websockify.c @@ -227,7 +227,13 @@ void proxy_handler(ws_ctx_t *ws_ctx) { strcpy(addr.sun_path, ".KasmVNCSock"); addr.sun_path[0] = '\0'; + struct sockaddr_un myaddr; + myaddr.sun_family = AF_UNIX; + sprintf(myaddr.sun_path, ".%s@%s", ws_ctx->user, ws_ctx->ip); + myaddr.sun_path[0] = '\0'; + int tsock = socket(AF_UNIX, SOCK_STREAM, 0); + bind(tsock, (struct sockaddr *) &myaddr, sizeof(struct sockaddr_un)); handler_msg("connecting to VNC target\n"); diff --git a/common/rfb/CMakeLists.txt b/common/rfb/CMakeLists.txt index 67adcf9..38d3472 100644 --- a/common/rfb/CMakeLists.txt +++ b/common/rfb/CMakeLists.txt @@ -1,4 +1,5 @@ -include_directories(${CMAKE_SOURCE_DIR}/common ${JPEG_INCLUDE_DIR}) +include_directories(${CMAKE_SOURCE_DIR}/common ${JPEG_INCLUDE_DIR} + ${CMAKE_SOURCE_DIR}/unix/kasmvncpasswd) set(RFB_SOURCES Blacklist.cxx diff --git a/common/rfb/ConnParams.cxx b/common/rfb/ConnParams.cxx index 56f86c6..7942779 100644 --- a/common/rfb/ConnParams.cxx +++ b/common/rfb/ConnParams.cxx @@ -26,6 +26,7 @@ #include #include #include +#include #include using namespace rfb; @@ -43,7 +44,7 @@ ConnParams::ConnParams() supportsContinuousUpdates(false), compressLevel(2), qualityLevel(-1), fineQualityLevel(-1), subsampling(subsampleUndefined), name_(0), verStrPos(0), - ledState_(ledUnknown) + ledState_(ledUnknown), shandler(NULL) { memset(kasmPassed, 0, KASM_NUM_SETTINGS); setName(""); @@ -124,6 +125,8 @@ void ConnParams::setEncodings(int nEncodings, const rdr::S32* encodings) encodings_.clear(); encodings_.insert(encodingRaw); + bool canChangeSettings = !shandler || shandler->canChangeKasmSettings(); + for (int i = nEncodings-1; i >= 0; i--) { switch (encodings[i]) { case encodingCopyRect: @@ -184,11 +187,11 @@ void ConnParams::setEncodings(int nEncodings, const rdr::S32* encodings) subsampling = subsample16X; break; case pseudoEncodingPreferBandwidth: - if (!rfb::Server::ignoreClientSettingsKasm) + if (!rfb::Server::ignoreClientSettingsKasm && canChangeSettings) Server::preferBandwidth.setParam(); break; case pseudoEncodingMaxVideoResolution: - if (!rfb::Server::ignoreClientSettingsKasm) + if (!rfb::Server::ignoreClientSettingsKasm && canChangeSettings) kasmPassed[KASM_MAX_VIDEO_RESOLUTION] = true; break; } @@ -205,7 +208,7 @@ void ConnParams::setEncodings(int nEncodings, const rdr::S32* encodings) encodings[i] <= pseudoEncodingFineQualityLevel100) fineQualityLevel = encodings[i] - pseudoEncodingFineQualityLevel0; - if (!rfb::Server::ignoreClientSettingsKasm) { + if (!rfb::Server::ignoreClientSettingsKasm && canChangeSettings) { if (encodings[i] >= pseudoEncodingJpegVideoQualityLevel0 && encodings[i] <= pseudoEncodingJpegVideoQualityLevel9) Server::jpegVideoQuality.setParam(encodings[i] - pseudoEncodingJpegVideoQualityLevel0); diff --git a/common/rfb/ConnParams.h b/common/rfb/ConnParams.h index 448c410..7439943 100644 --- a/common/rfb/ConnParams.h +++ b/common/rfb/ConnParams.h @@ -42,6 +42,8 @@ namespace rfb { const int subsample8X = 4; const int subsample16X = 5; + class SMsgHandler; + class ConnParams { public: ConnParams(); @@ -74,6 +76,8 @@ namespace rfb { const PixelFormat& pf() const { return pf_; } void setPF(const PixelFormat& pf); + void setSHandler(SMsgHandler *s) { shandler = s; } + const char* name() const { return name_; } void setName(const char* name); @@ -136,6 +140,7 @@ namespace rfb { char verStr[13]; int verStrPos; unsigned int ledState_; + SMsgHandler *shandler; }; } #endif diff --git a/common/rfb/SMsgHandler.cxx b/common/rfb/SMsgHandler.cxx index c38458c..78ece78 100644 --- a/common/rfb/SMsgHandler.cxx +++ b/common/rfb/SMsgHandler.cxx @@ -24,6 +24,7 @@ using namespace rfb; SMsgHandler::SMsgHandler() { + cp.setSHandler(this); } SMsgHandler::~SMsgHandler() diff --git a/common/rfb/SMsgHandler.h b/common/rfb/SMsgHandler.h index 6d5f5a1..a169621 100644 --- a/common/rfb/SMsgHandler.h +++ b/common/rfb/SMsgHandler.h @@ -56,6 +56,8 @@ namespace rfb { virtual void sendStats() = 0; + virtual bool canChangeKasmSettings() const = 0; + // InputHandler interface // The InputHandler methods will be called for the corresponding messages. diff --git a/common/rfb/SMsgReader.cxx b/common/rfb/SMsgReader.cxx index 7550331..7fb3f2c 100644 --- a/common/rfb/SMsgReader.cxx +++ b/common/rfb/SMsgReader.cxx @@ -151,7 +151,7 @@ void SMsgReader::readSetMaxVideoResolution() width = is->readU16(); height = is->readU16(); - if (!rfb::Server::ignoreClientSettingsKasm) { + if (!rfb::Server::ignoreClientSettingsKasm && handler->canChangeKasmSettings()) { sprintf(tmp, "%ux%u", width, height); rfb::Server::maxVideoResolution.setParam(tmp); } diff --git a/common/rfb/VNCSConnectionST.cxx b/common/rfb/VNCSConnectionST.cxx index fdcf984..467e8f9 100644 --- a/common/rfb/VNCSConnectionST.cxx +++ b/common/rfb/VNCSConnectionST.cxx @@ -38,6 +38,9 @@ #include #include #include +#include + +#include "kasmpasswd.h" using namespace rfb; @@ -45,6 +48,8 @@ static LogWriter vlog("VNCSConnST"); static Cursor emptyCursor(0, 0, Point(0, 0), NULL); +extern rfb::StringParameter basicauth; + VNCSConnectionST::VNCSConnectionST(VNCServerST* server_, network::Socket *s, bool reverse) : sock(s), reverseConnection(reverse), @@ -54,7 +59,7 @@ VNCSConnectionST::VNCSConnectionST(VNCServerST* server_, network::Socket *s, losslessTimer(this), kbdLogTimer(this), server(server_), updates(false), updateRenderedCursor(false), removeRenderedCursor(false), continuousUpdates(false), encodeManager(this, &server_->encCache), - pointerEventTime(0), + needsPermCheck(false), pointerEventTime(0), clientHasCursor(false), accessRights(AccessDefault), startTime(time(0)) { @@ -65,6 +70,25 @@ VNCSConnectionST::VNCSConnectionST(VNCServerST* server_, network::Socket *s, memset(bstats_total, 0, sizeof(bstats_total)); gettimeofday(&connStart, NULL); + // Check their permissions, if applicable + kasmpasswdpath[0] = '\0'; + wordexp_t wexp; + if (!wordexp(rfb::Server::kasmPasswordFile, &wexp, WRDE_NOCMD)) + strncpy(kasmpasswdpath, wexp.we_wordv[0], 4096); + kasmpasswdpath[4095] = '\0'; + wordfree(&wexp); + + user[0] = '\0'; + const char *at = strchr(peerEndpoint.buf, '@'); + if (at && at - peerEndpoint.buf > 1 && at - peerEndpoint.buf < 32) { + memcpy(user, peerEndpoint.buf, at - peerEndpoint.buf); + user[at - peerEndpoint.buf] = '\0'; + } + + bool write, owner; + if (!getPerms(write, owner) || !write) + accessRights = (accessRights & ~(AccessPtrEvents | AccessKeyEvents | AccessSetDesktopSize)); + // Configure the socket setSocketTimeouts(); lastEventTime = time(0); @@ -1001,6 +1025,34 @@ bool VNCSConnectionST::isShiftPressed() return false; } +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 + write = true; + return true; + } + if (colon && !colon[1] && user[0]) { + struct kasmpasswd_t *set = readkasmpasswd(kasmpasswdpath); + unsigned i; + for (i = 0; i < set->num; i++) { + if (!strcmp(set->entries[i].user, user)) { + write = set->entries[i].write; + owner = set->entries[i].owner; + found = true; + break; + } + } + + free(set->entries); + free(set); + } + + return found; +} + void VNCSConnectionST::writeRTTPing() { char type; @@ -1081,6 +1133,22 @@ void VNCSConnectionST::writeFramebufferUpdate() if (isCongested()) return; + // Check for permission changes? + if (needsPermCheck) { + needsPermCheck = false; + + bool write, owner, ret; + ret = getPerms(write, owner); + if (!ret) { + close("User was deleted"); + return; + } else if (!write) { + accessRights = (accessRights & ~(AccessPtrEvents | AccessKeyEvents | AccessSetDesktopSize)); + } else { + accessRights |= AccessPtrEvents | AccessKeyEvents | AccessSetDesktopSize; + } + } + // Updates often consists of many small writes, and in continuous // mode, we will also have small fence messages around the update. We // need to aggregate these in order to not clog up TCP's congestion diff --git a/common/rfb/VNCSConnectionST.h b/common/rfb/VNCSConnectionST.h index 76350ef..3eff9ad 100644 --- a/common/rfb/VNCSConnectionST.h +++ b/common/rfb/VNCSConnectionST.h @@ -102,6 +102,10 @@ namespace rfb { // or because the current cursor position has not been set by this client. bool needRenderedCursor(); + void recheckPerms() { + needsPermCheck = true; + } + network::Socket* getSock() { return sock; } void add_changed(const Region& region) { updates.add_changed(region); } void add_changed_all() { updates.add_changed(server->pb->getRect()); } @@ -179,12 +183,23 @@ namespace rfb { virtual void supportsLEDState(); virtual void sendStats(); + virtual bool canChangeKasmSettings() const { + return (accessRights & (AccessPtrEvents | AccessKeyEvents)) == + (AccessPtrEvents | AccessKeyEvents); + } // setAccessRights() allows a security package to limit the access rights // of a VNCSConnectioST to the server. These access rights are applied // such that the actual rights granted are the minimum of the server's // default access settings and the connection's access settings. - virtual void setAccessRights(AccessRights ar) {accessRights=ar;} + virtual void setAccessRights(AccessRights ar) { + accessRights = ar; + + bool write, owner; + if (!getPerms(write, owner) || !write) + accessRights = (accessRights & ~(AccessPtrEvents | AccessKeyEvents)); + needsPermCheck = false; + } // Timer callbacks virtual bool handleTimeout(Timer* t); @@ -193,6 +208,8 @@ namespace rfb { bool isShiftPressed(); + bool getPerms(bool &write, bool &owner) const; + // Congestion control void writeRTTPing(); bool isCongested(); @@ -249,6 +266,10 @@ namespace rfb { rdr::U64 bstats_total[BS_NUM]; struct timeval connStart; + char user[32]; + char kasmpasswdpath[4096]; + bool needsPermCheck; + time_t lastEventTime; time_t pointerEventTime; Point pointerEventPos; diff --git a/common/rfb/VNCServerST.cxx b/common/rfb/VNCServerST.cxx index 0577ec0..1ac8597 100644 --- a/common/rfb/VNCServerST.cxx +++ b/common/rfb/VNCServerST.cxx @@ -63,6 +63,11 @@ #include +#include +#include +#include +#include + using namespace rfb; static LogWriter slog("VNCServerST"); @@ -73,6 +78,9 @@ EncCache VNCServerST::encCache; // -=- VNCServerST Implementation // +static char kasmpasswdpath[4096]; +extern rfb::StringParameter basicauth; + // -=- Constructors/Destructor VNCServerST::VNCServerST(const char* name_, SDesktop* desktop_) @@ -87,6 +95,26 @@ VNCServerST::VNCServerST(const char* name_, SDesktop* desktop_) { lastUserInputTime = lastDisconnectTime = time(0); slog.debug("creating single-threaded server %s", name.buf); + + kasmpasswdpath[0] = '\0'; + wordexp_t wexp; + if (!wordexp(rfb::Server::kasmPasswordFile, &wexp, WRDE_NOCMD)) + strncpy(kasmpasswdpath, wexp.we_wordv[0], 4096); + kasmpasswdpath[4095] = '\0'; + wordfree(&wexp); + + if (kasmpasswdpath[0] && access(kasmpasswdpath, R_OK) == 0) { + // Set up a watch on the password file + inotifyfd = inotify_init(); + if (inotifyfd < 0) + slog.error("Failed to init inotify"); + + int flags = fcntl(inotifyfd, F_GETFL, 0); + fcntl(inotifyfd, F_SETFL, flags | O_NONBLOCK); + + if (inotify_add_watch(inotifyfd, kasmpasswdpath, IN_CLOSE_WRITE | IN_DELETE_SELF) < 0) + slog.error("Failed to set watch"); + } } VNCServerST::~VNCServerST() @@ -659,8 +687,34 @@ void VNCServerST::writeUpdate() encCache.clear(); encCache.enabled = clients.size() > 1; + // Check if the password file was updated + bool permcheck = false; + if (inotifyfd >= 0) { + char buf[256]; + int ret = read(inotifyfd, buf, 256); + int pos = 0; + while (ret > 0) { + const struct inotify_event * const ev = (struct inotify_event *) &buf[pos]; + + if (ev->mask & IN_IGNORED) { + // file was deleted, set new watch + if (inotify_add_watch(inotifyfd, kasmpasswdpath, IN_CLOSE_WRITE | IN_DELETE_SELF) < 0) + slog.error("Failed to set watch"); + } + + permcheck = true; + + ret -= sizeof(struct inotify_event) - ev->len; + pos += sizeof(struct inotify_event) - ev->len; + } + } + for (ci = clients.begin(); ci != clients.end(); ci = ci_next) { ci_next = ci; ci_next++; + + if (permcheck) + (*ci)->recheckPerms(); + (*ci)->add_copied(ui.copied, ui.copy_delta); (*ci)->add_copypassed(ui.copypassed); (*ci)->add_changed(ui.changed); diff --git a/common/rfb/VNCServerST.h b/common/rfb/VNCServerST.h index 7eaf896..ef6e3e0 100644 --- a/common/rfb/VNCServerST.h +++ b/common/rfb/VNCServerST.h @@ -249,6 +249,8 @@ namespace rfb { bool disableclients; Timer frameTimer; + + int inotifyfd; }; }; diff --git a/kasmweb/app/ui.js b/kasmweb/app/ui.js index 952f109..1e2d7a6 100644 --- a/kasmweb/app/ui.js +++ b/kasmweb/app/ui.js @@ -172,7 +172,6 @@ const UI = { UI.initSetting('port', port); UI.initSetting('encrypt', (window.location.protocol === "https:")); UI.initSetting('view_clip', false); - UI.initSetting('resize', 'remote'); UI.initSetting('shared', true); UI.initSetting('view_only', false); UI.initSetting('show_dot', false); @@ -181,15 +180,26 @@ const UI = { UI.initSetting('reconnect', false); UI.initSetting('reconnect_delay', 5000); UI.initSetting('idle_disconnect', 20); - UI.initSetting('video_quality', 3); - UI.initSetting('clipboard_up', true); - UI.initSetting('clipboard_down', true); - UI.initSetting('clipboard_seamless', true); UI.initSetting('prefer_local_cursor', true); - UI.initSetting('enable_webp', true); UI.initSetting('toggle_control_panel', false); UI.initSetting('enable_perf_stats', false); - + + if (WebUtil.isInsideKasmVDI()) { + UI.initSetting('video_quality', 1); + UI.initSetting('clipboard_up', false); + UI.initSetting('clipboard_down', false); + UI.initSetting('clipboard_seamless', false); + UI.initSetting('enable_webp', false); + UI.initSetting('resize', 'off'); + } else { + UI.initSetting('video_quality', 3); + UI.initSetting('clipboard_up', true); + UI.initSetting('clipboard_down', true); + UI.initSetting('clipboard_seamless', true); + UI.initSetting('enable_webp', true); + UI.initSetting('resize', 'remote'); + } + UI.setupSettingLabels(); }, // Adds a link to the label elements on the corresponding input elements @@ -416,6 +426,10 @@ const UI = { document.documentElement.classList.remove("noVNC_reconnecting"); const transition_elem = document.getElementById("noVNC_transition_text"); + if (WebUtil.isInsideKasmVDI()) + { + parent.postMessage({ action: 'connection_state', value: state}, '*' ); + } switch (state) { case 'init': break; @@ -1254,7 +1268,7 @@ const UI = { UI.rfb.addEventListener("securityfailure", UI.securityFailed); UI.rfb.addEventListener("capabilities", UI.updatePowerButton); UI.rfb.addEventListener("clipboard", UI.clipboardReceive); - UI.rfb.addEventListener("bottleneck_stats", UI.bottleneckStatsRecieve); + UI.rfb.addEventListener("bottleneck_stats", UI.bottleneckStatsRecieve); document.addEventListener('mouseenter', UI.enterVNC); document.addEventListener('mouseleave', UI.leaveVNC); @@ -1295,7 +1309,9 @@ const UI = { window.attachEvent('onload', WindowLoad); window.attachEvent('message', UI.receiveMessage); } - UI.rfb.addEventListener("clipboard", UI.clipboardRx); + if (UI.rfb.clipboardDown){ + UI.rfb.addEventListener("clipboard", UI.clipboardRx); + } UI.rfb.addEventListener("disconnect", UI.disconnectedRx); document.getElementById('noVNC_control_bar_anchor').setAttribute('style', 'display: none'); document.getElementById('noVNC_connect_dlg').innerHTML = ''; @@ -1459,7 +1475,9 @@ const UI = { if (event.data && event.data.action) { switch (event.data.action) { case 'clipboardsnd': - UI.rfb.clipboardPasteFrom(event.data.value); + if (UI.rfb.clipboardUp) { + UI.rfb.clipboardPasteFrom(event.data.value); + } break; case 'setvideoquality': UI.rfb.videoQuality = event.data.value; diff --git a/kasmweb/core/rfb.js b/kasmweb/core/rfb.js index 407d451..91c11b0 100644 --- a/kasmweb/core/rfb.js +++ b/kasmweb/core/rfb.js @@ -1068,14 +1068,7 @@ export default class RFB extends EventTargetMixin { _negotiate_std_vnc_auth() { if (this._sock.rQwait("auth challenge", 16)) { return false; } - /* Empty passwords are allowed in VNC and since we use HTTPS basic auth, wich is superior, lets allow the bypass of the vnc password - if (!this._rfb_credentials.password) { - this.dispatchEvent(new CustomEvent( - "credentialsrequired", - { detail: { types: ["password"] } })); - return false; - } - */ + // KasmVNC uses basic Auth, clear the VNC password, which is not used this._rfb_credentials.password = ""; // TODO(directxman12): make genDES not require an Array @@ -1353,7 +1346,7 @@ export default class RFB extends EventTargetMixin { */ if (!this.enableWebP) return false; - // It's not possible to check for webp synchronously, and hacking promises + // It's not possible to check for webp synchronously, and hacking promises // into everything would be too time-consuming. So test for FF and Chrome. var uagent = navigator.userAgent.toLowerCase(); var match = uagent.match(/firefox\/([0-9]+)\./); diff --git a/kasmweb/vnc.html b/kasmweb/vnc.html index 676c447..bce252b 100644 --- a/kasmweb/vnc.html +++ b/kasmweb/vnc.html @@ -71,9 +71,19 @@ loader.src = "vendor/browser-es-module-loader/dist/browser-es-module-loader.js"; document.head.appendChild(loader); }); - window.addEventListener("load", function() { - document.getElementById("noVNC_connect_button").click(); - }); + + let isInsideKasmVDI = false; + try { + isInsideKasmVDI = (window.self !== window.top); + } catch (e) { + isInsideKasmVDI = true; + } + + if (!isInsideKasmVDI) { + window.addEventListener("load", function() { + document.getElementById("noVNC_connect_button").click(); + }); + } diff --git a/unix/kasmvncpasswd/CMakeLists.txt b/unix/kasmvncpasswd/CMakeLists.txt index 2edb0d7..600ee34 100644 --- a/unix/kasmvncpasswd/CMakeLists.txt +++ b/unix/kasmvncpasswd/CMakeLists.txt @@ -1,7 +1,8 @@ include_directories(${CMAKE_SOURCE_DIR}/common) add_executable(kasmvncpasswd - kasmvncpasswd.c) + kasmvncpasswd.c + kasmpasswd.c) target_link_libraries(kasmvncpasswd crypt) diff --git a/unix/kasmvncpasswd/kasmpasswd.c b/unix/kasmvncpasswd/kasmpasswd.c new file mode 100644 index 0000000..bee2870 --- /dev/null +++ b/unix/kasmvncpasswd/kasmpasswd.c @@ -0,0 +1,117 @@ +#include +#include +#include +#include +#include +#include + +#include "kasmpasswd.h" + +struct kasmpasswd_t *readkasmpasswd(const char path[]) { + + struct kasmpasswd_t *set = calloc(sizeof(struct kasmpasswd_t), 1); + FILE *f = fopen(path, "r"); + if (!f) + return set; + + // Count lines + unsigned lines = 0; + char buf[4096]; + + while (fgets(buf, 4096, f)) { + lines++; + } + + rewind(f); + + set->entries = calloc(sizeof(struct kasmpasswd_entry_t), lines); + + unsigned cur = 0; + while (fgets(buf, 4096, f)) { + char *lim = strchr(buf, ':'); + if (!lim) + continue; + *lim = '\0'; + lim++; + + const char * const pw = lim; + + lim = strchr(lim, ':'); + if (!lim) + continue; + *lim = '\0'; + lim++; + + const char * const perms = lim; + + lim = strchr(lim, '\n'); + if (lim) + *lim = '\0'; + + if (strlen(buf) + 1 > sizeof(((struct kasmpasswd_entry_t *)0)->user)) { + fprintf(stderr, "Username %s too long\n", buf); + continue; + } + if (strlen(pw) + 1 > sizeof(((struct kasmpasswd_entry_t *)0)->password)) { + fprintf(stderr, "Password for user %s too long\n", buf); + continue; + } + + strcpy(set->entries[cur].user, buf); + strcpy(set->entries[cur].password, pw); + + if (strchr(perms, 'w')) + set->entries[cur].write = 1; + if (strchr(perms, 'o')) + set->entries[cur].owner = 1; + + cur++; + } + + fclose(f); + + set->num = cur; + + return set; +} + +void writekasmpasswd(const char path[], const struct kasmpasswd_t *set) { + char tmpname[PATH_MAX]; + + if (!set || !set->entries || !set->num) + return; + + snprintf(tmpname, PATH_MAX, "%s.tmp", path); + tmpname[PATH_MAX - 1] = '\0'; + + FILE *f = fopen(tmpname, "w"); + if (!f) { + fprintf(stderr, "Failed to open temp file %s\n", tmpname); + return; + } + + static const char * const perms[] = { + "", + "w", + "o", + "ow" + }; + + unsigned i; + for (i = 0; i < set->num; i++) { + if (!set->entries[i].user[0]) + continue; + + fprintf(f, "%s:%s:%s\n", + set->entries[i].user, + set->entries[i].password, + perms[set->entries[i].owner * 2 + set->entries[i].write]); + } + + fsync(fileno(f)); + fclose(f); + + if (rename(tmpname, path)) + fprintf(stderr, "Failed writing the password file %s\n", path); + chmod(path, S_IRUSR|S_IWUSR); +} diff --git a/unix/kasmvncpasswd/kasmpasswd.h b/unix/kasmvncpasswd/kasmpasswd.h new file mode 100644 index 0000000..6bff018 --- /dev/null +++ b/unix/kasmvncpasswd/kasmpasswd.h @@ -0,0 +1,27 @@ +#ifndef KASMPASSWD_H +#define KASMPASSWD_H + +#ifdef __cplusplus +extern "C" { +#endif + +struct kasmpasswd_entry_t { + char user[32]; + char password[128]; + unsigned char write : 1; + unsigned char owner : 1; +}; + +struct kasmpasswd_t { + struct kasmpasswd_entry_t *entries; + unsigned num; +}; + +struct kasmpasswd_t *readkasmpasswd(const char path[]); +void writekasmpasswd(const char path[], const struct kasmpasswd_t *set); + +#ifdef __cplusplus +} // extern C +#endif + +#endif diff --git a/unix/kasmvncpasswd/kasmvncpasswd.c b/unix/kasmvncpasswd/kasmvncpasswd.c index 6c41da4..cb893cc 100644 --- a/unix/kasmvncpasswd/kasmvncpasswd.c +++ b/unix/kasmvncpasswd/kasmvncpasswd.c @@ -29,16 +29,30 @@ #include #include +#include "kasmpasswd.h" + static void usage(const char *prog) { - fprintf(stderr, "Usage: %s [file]\n", prog); - fprintf(stderr, " %s -f\n", prog); + fprintf(stderr, "Usage: %s -u username [-wnod] [file]\n" + "-w Write permission\n" + "-o Owner\n" + "-n Don't change password, change permissions only\n" + "-d Delete this user\n" + "\n" + "The file is updated atomically.\n\n" + "To pass the password via a pipe, use\n" + "echo -e \"password\\npassword\\n\" | %s [-args]\n", + prog, prog); exit(1); } static void enableEcho(unsigned char enable) { struct termios attrs; + + if (!isatty(fileno(stdin))) + return; + tcgetattr(fileno(stdin), &attrs); if (enable) attrs.c_lflag |= ECHO; @@ -53,7 +67,7 @@ static const char *encryptpw(const char *in) { } static char* getpassword(const char* prompt, char *buf) { - if (prompt) fputs(prompt, stdout); + if (prompt && isatty(fileno(stdin))) fputs(prompt, stdout); enableEcho(0); char* result = fgets(buf, 4096, stdin); enableEcho(1); @@ -68,18 +82,6 @@ static char* getpassword(const char* prompt, char *buf) { static char pw1[4096]; static char pw2[4096]; -// Reads passwords from stdin and prints encrypted passwords to stdout. -static int encrypt_pipe() { - char *result = getpassword(NULL, pw1); - if (!result) - return 1; - - printf("%s", encryptpw(result)); - fflush(stdout); - - return 0; -} - static const char *readpassword() { while (1) { if (!getpassword("Password:", pw1)) { @@ -100,6 +102,10 @@ static const char *readpassword() { exit(1); } if (strcmp(pw1, pw2) != 0) { + if (!isatty(fileno(stdin))) { + fprintf(stderr,"Passwords don't match\n"); + exit(1); + } fprintf(stderr,"Passwords don't match - try again\n"); continue; } @@ -110,20 +116,49 @@ static const char *readpassword() { int main(int argc, char** argv) { - char* fname = 0; + const char *fname = NULL; + const char *user = NULL; + const char args[] = "u:wnod"; + int opt; - for (int i = 1; i < argc; i++) { - if (strncmp(argv[i], "-f", 2) == 0) { - return encrypt_pipe(); - } else if (argv[i][0] == '-') { - usage(argv[0]); - } else if (!fname) { - fname = argv[i]; - } else { - usage(argv[0]); + unsigned char nopass = 0, writer = 0, owner = 0, deleting = 0; + + while ((opt = getopt(argc, argv, args)) != -1) { + switch (opt) { + case 'u': + user = optarg; + if (strlen(user) + 1 > sizeof(((struct kasmpasswd_entry_t *)0)->user)) { + fprintf(stderr, "Username %s too long\n", user); + exit(1); + } + break; + case 'n': + nopass = 1; + break; + case 'w': + writer = 1; + break; + case 'o': + owner = 1; + break; + case 'd': + deleting = 1; + break; + default: + usage(argv[0]); + break; } } + if (deleting && (nopass || writer || owner)) + usage(argv[0]); + + if (!user) + usage(argv[0]); + + if (optind < argc) + fname = argv[optind]; + if (!fname) { wordexp_t wexp; if (!wordexp("~/.kasmpasswd", &wexp, WRDE_NOCMD) && wexp.we_wordv[0]) @@ -133,23 +168,54 @@ int main(int argc, char** argv) if (!fname) usage(argv[0]); - while (1) { + // Action + struct kasmpasswd_t *set = readkasmpasswd(fname); + unsigned i; + + if (nopass) { + for (i = 0; i < set->num; i++) { + if (!strcmp(set->entries[i].user, user)) { + set->entries[i].write = writer; + set->entries[i].owner = owner; + + writekasmpasswd(fname, set); + return 0; + } + } + fprintf(stderr, "No user named %s found\n", user); + return 1; + + } else if (deleting) { + for (i = 0; i < set->num; i++) { + if (!strcmp(set->entries[i].user, user)) { + set->entries[i].user[0] = '\0'; + + writekasmpasswd(fname, set); + return 0; + } + } + fprintf(stderr, "No user named %s found\n", user); + return 1; + } else { const char *encrypted = readpassword(); - - FILE* fp = fopen(fname, "w"); - if (!fp) { - fprintf(stderr, "Couldn't open %s for writing\n", fname); - exit(1); - } - chmod(fname, S_IRUSR|S_IWUSR); - - if (fwrite(encrypted, strlen(encrypted), 1, fp) != 1) { - fprintf(stderr,"Writing to %s failed\n",fname); - exit(1); + for (i = 0; i < set->num; i++) { + if (!strcmp(set->entries[i].user, user)) + break; } - fclose(fp); + // No existing user by that name? + if (i >= set->num) { + i = set->num++; + set->entries = realloc(set->entries, set->num * sizeof(struct kasmpasswd_entry_t)); + } - return 0; + strcpy(set->entries[i].user, user); + strcpy(set->entries[i].password, encrypted); + set->entries[i].write = writer; + set->entries[i].owner = owner; + + writekasmpasswd(fname, set); } + + return 0; }