diff --git a/.gitignore b/.gitignore index 46ba2ea..f4a278e 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,7 @@ config.h builder/build/ builder/www/ +spec/tmp # Deb building artefacts debian/.debhelper/ diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 27437a8..109d43b 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -15,6 +15,7 @@ variables: stages: - www - build + - test - upload .prepare_build: &prepare_build @@ -371,6 +372,13 @@ build_opensuse_15_arm: paths: - output/ +test: + stage: test + before_script: + - *prepare_build + script: + - bash builder/test-vncserver + upload: stage: upload image: ubuntu:focal diff --git a/Pipfile b/Pipfile new file mode 100644 index 0000000..aa90173 --- /dev/null +++ b/Pipfile @@ -0,0 +1,15 @@ +[[source]] +url = "https://pypi.python.org/simple" +verify_ssl = true +name = "pypi" + +[packages] +mamba = "*" +expects = "*" +"path.py" = "*" +pexpect = "*" + +[dev-packages] + +[requires] +python_version = "3.8" diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 0000000..1006c73 --- /dev/null +++ b/Pipfile.lock @@ -0,0 +1,136 @@ +{ + "_meta": { + "hash": { + "sha256": "6745d5e5d90e44a18d73a0e23bc3d3e68acb950af0b87df50b45272d25b9e615" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.8" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.python.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "args": { + "hashes": [ + "sha256:a785b8d837625e9b61c39108532d95b85274acd679693b71ebb5156848fcf814" + ], + "version": "==0.1.0" + }, + "clint": { + "hashes": [ + "sha256:05224c32b1075563d0b16d0015faaf9da43aa214e4a2140e51f08789e7a4c5aa" + ], + "version": "==0.5.1" + }, + "coverage": { + "hashes": [ + "sha256:004d1880bed2d97151facef49f08e255a20ceb6f9432df75f4eef018fdd5a78c", + "sha256:01d84219b5cdbfc8122223b39a954820929497a1cb1422824bb86b07b74594b6", + "sha256:040af6c32813fa3eae5305d53f18875bedd079960822ef8ec067a66dd8afcd45", + "sha256:06191eb60f8d8a5bc046f3799f8a07a2d7aefb9504b0209aff0b47298333302a", + "sha256:13034c4409db851670bc9acd836243aeee299949bd5673e11844befcb0149f03", + "sha256:13c4ee887eca0f4c5a247b75398d4114c37882658300e153113dafb1d76de529", + "sha256:184a47bbe0aa6400ed2d41d8e9ed868b8205046518c52464fde713ea06e3a74a", + "sha256:18ba8bbede96a2c3dde7b868de9dcbd55670690af0988713f0603f037848418a", + "sha256:1aa846f56c3d49205c952d8318e76ccc2ae23303351d9270ab220004c580cfe2", + "sha256:217658ec7187497e3f3ebd901afdca1af062b42cfe3e0dafea4cced3983739f6", + "sha256:24d4a7de75446be83244eabbff746d66b9240ae020ced65d060815fac3423759", + "sha256:2910f4d36a6a9b4214bb7038d537f015346f413a975d57ca6b43bf23d6563b53", + "sha256:2949cad1c5208b8298d5686d5a85b66aae46d73eec2c3e08c817dd3513e5848a", + "sha256:2a3859cb82dcbda1cfd3e6f71c27081d18aa251d20a17d87d26d4cd216fb0af4", + "sha256:2cafbbb3af0733db200c9b5f798d18953b1a304d3f86a938367de1567f4b5bff", + "sha256:2e0d881ad471768bf6e6c2bf905d183543f10098e3b3640fc029509530091502", + "sha256:30c77c1dc9f253283e34c27935fded5015f7d1abe83bc7821680ac444eaf7793", + "sha256:3487286bc29a5aa4b93a072e9592f22254291ce96a9fbc5251f566b6b7343cdb", + "sha256:372da284cfd642d8e08ef606917846fa2ee350f64994bebfbd3afb0040436905", + "sha256:41179b8a845742d1eb60449bdb2992196e211341818565abded11cfa90efb821", + "sha256:44d654437b8ddd9eee7d1eaee28b7219bec228520ff809af170488fd2fed3e2b", + "sha256:4a7697d8cb0f27399b0e393c0b90f0f1e40c82023ea4d45d22bce7032a5d7b81", + "sha256:51cb9476a3987c8967ebab3f0fe144819781fca264f57f89760037a2ea191cb0", + "sha256:52596d3d0e8bdf3af43db3e9ba8dcdaac724ba7b5ca3f6358529d56f7a166f8b", + "sha256:53194af30d5bad77fcba80e23a1441c71abfb3e01192034f8246e0d8f99528f3", + "sha256:5fec2d43a2cc6965edc0bb9e83e1e4b557f76f843a77a2496cbe719583ce8184", + "sha256:6c90e11318f0d3c436a42409f2749ee1a115cd8b067d7f14c148f1ce5574d701", + "sha256:74d881fc777ebb11c63736622b60cb9e4aee5cace591ce274fb69e582a12a61a", + "sha256:7501140f755b725495941b43347ba8a2777407fc7f250d4f5a7d2a1050ba8e82", + "sha256:796c9c3c79747146ebd278dbe1e5c5c05dd6b10cc3bcb8389dfdf844f3ead638", + "sha256:869a64f53488f40fa5b5b9dcb9e9b2962a66a87dab37790f3fcfb5144b996ef5", + "sha256:8963a499849a1fc54b35b1c9f162f4108017b2e6db2c46c1bed93a72262ed083", + "sha256:8d0a0725ad7c1a0bcd8d1b437e191107d457e2ec1084b9f190630a4fb1af78e6", + "sha256:900fbf7759501bc7807fd6638c947d7a831fc9fdf742dc10f02956ff7220fa90", + "sha256:92b017ce34b68a7d67bd6d117e6d443a9bf63a2ecf8567bb3d8c6c7bc5014465", + "sha256:970284a88b99673ccb2e4e334cfb38a10aab7cd44f7457564d11898a74b62d0a", + "sha256:972c85d205b51e30e59525694670de6a8a89691186012535f9d7dbaa230e42c3", + "sha256:9a1ef3b66e38ef8618ce5fdc7bea3d9f45f3624e2a66295eea5e57966c85909e", + "sha256:af0e781009aaf59e25c5a678122391cb0f345ac0ec272c7961dc5455e1c40066", + "sha256:b6d534e4b2ab35c9f93f46229363e17f63c53ad01330df9f2d6bd1187e5eaacf", + "sha256:b7895207b4c843c76a25ab8c1e866261bcfe27bfaa20c192de5190121770672b", + "sha256:c0891a6a97b09c1f3e073a890514d5012eb256845c451bd48f7968ef939bf4ae", + "sha256:c2723d347ab06e7ddad1a58b2a821218239249a9e4365eaff6649d31180c1669", + "sha256:d1f8bf7b90ba55699b3a5e44930e93ff0189aa27186e96071fac7dd0d06a1873", + "sha256:d1f9ce122f83b2305592c11d64f181b87153fc2c2bbd3bb4a3dde8303cfb1a6b", + "sha256:d314ed732c25d29775e84a960c3c60808b682c08d86602ec2c3008e1202e3bb6", + "sha256:d636598c8305e1f90b439dbf4f66437de4a5e3c31fdf47ad29542478c8508bbb", + "sha256:deee1077aae10d8fa88cb02c845cfba9b62c55e1183f52f6ae6a2df6a2187160", + "sha256:ebe78fe9a0e874362175b02371bdfbee64d8edc42a044253ddf4ee7d3c15212c", + "sha256:f030f8873312a16414c0d8e1a1ddff2d3235655a2174e3648b4fa66b3f2f1079", + "sha256:f0b278ce10936db1a37e6954e15a3730bea96a0997c26d7fee88e6c396c2086d", + "sha256:f11642dddbb0253cc8853254301b51390ba0081750a8ac03f20ea8103f0c56b6" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'", + "version": "==5.5" + }, + "expects": { + "hashes": [ + "sha256:419902ccafe81b7e9559eeb6b7a07ef9d5c5604eddb93000f0642b3b2d594f4c" + ], + "index": "pypi", + "version": "==0.9.0" + }, + "mamba": { + "hashes": [ + "sha256:75cfc6dfd287dcccaf86dd753cf48e0a7337487c7c3fafda05a6a67ded6da496" + ], + "index": "pypi", + "version": "==0.11.2" + }, + "path": { + "hashes": [ + "sha256:2de925e8d421f93bcea80d511b81accfb6a7e6b249afa4a5559557b0cf817097", + "sha256:340054c5bb459fc9fd40e7eb6768c5989f3e599d18224238465b5333bc8faa7d" + ], + "markers": "python_version >= '3.6'", + "version": "==16.2.0" + }, + "path.py": { + "hashes": [ + "sha256:8d885e8b2497aed005703d94e0fd97943401f035e42a136810308bff034529a8", + "sha256:a43e82eb2c344c3fd0b9d6352f6b856f40b8b7d3d65cc05978b42c3715668496" + ], + "index": "pypi", + "version": "==12.5.0" + }, + "pexpect": { + "hashes": [ + "sha256:0b48a55dcb3c05f3329815901ea4fc1537514d6ba867a152b581d69ae3710937", + "sha256:fc65a43959d153d0114afe13997d439c22823a27cefceb5ff35c2178c6784c0c" + ], + "index": "pypi", + "version": "==4.8.0" + }, + "ptyprocess": { + "hashes": [ + "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35", + "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220" + ], + "version": "==0.7.0" + } + }, + "develop": {} +} diff --git a/README.md b/README.md index 440135e..92c1cd8 100644 --- a/README.md +++ b/README.md @@ -56,41 +56,34 @@ wget sudo apt-get install ./kasmvncserver_*.deb -# We provide an example script to run KasmVNC at # -# /usr/share/doc/kasmvncserver/examples/kasmvncserver-easy-start. It runs a VNC -# server on display :10 and on interface 0.0.0.0. If you're happy with those -# defaults you can just use it as is: -sudo ln -s /usr/share/doc/kasmvncserver/examples/kasmvncserver-easy-start /usr/bin/ - # Add your user to the ssl-cert group sudo addgroup $USER ssl-cert # You will need to re-connect in order to pick up the group change -# Create a KasmVNC user able to use keyboard and mouse: -vncpasswd -u $USER -w - -# Create ~/.vnc directory and corresponding files. -kasmvncserver-easy-start -d && kasmvncserver-easy-start -kill +# On the first run, vncserver will ask you to create a KasmVNC user and choose a desktop +# environment you want to run. It can detect Cinnamon, Mate, LXDE, LXQT, KDE, Gnome, +# XFCE. You can also choose to manually edit xstartup. +# After you chose a desktop environment or to manually edit xstartup, +# vncserver won't ask you again, unless you run it as: +vncserver -select-de -# Modify vncstartup to launch your environment of choice, in this example LXDE -# This may be optional depending on your system configuration -echo '/usr/bin/lxsession -s LXDE &' >> ~/.vnc/xstartup - -# Start KasmVNC with debug logging: -kasmvncserver-easy-start -d +# You can select a specific Desktop Environment like this: +vncserver -select-de mate # Tail the logs -tail -f ~/.vnc/`hostname`:10.log +tail -f ~/.vnc/*.log ``` -Now navigate to your system at https://[ip-address]:8443/ +Now navigate to your system at the urls printed by `vncserver`. -To stop a running KasmVNC: +To stop the KasmVNC you started earlier on display 10: ```sh -kasmvncserver-easy-start -kill +vncserver -kill ``` +Settings can be modified via editing `/etc/kasmvnc/kasmvnc.yaml` or `~/.vnc/kasmvnc.yaml`. + The options for vncserver: | Argument | Description | diff --git a/builder/README.md b/builder/README.md index f840c78..2687777 100644 --- a/builder/README.md +++ b/builder/README.md @@ -110,6 +110,37 @@ prepare_upload_filename "bionic/kasmvncserver_0.9.1~beta-1+libjpeg-turbo-latest_ echo $upload_filename;' ``` +# vncserver development + +## Building a Ubuntu Focal package +``` +builder/build-package ubuntu focal +``` + +You don't need to `build-package` every time you change something, but you need +`Xvnc` and other files. + +## Running a development environment + +``` +builder/devenv-vncserver +``` + +`devenv-vncserver` starts a dockerized development environment. Edit +`vncserver`, its modules, and configuration files as usual. When you +run`vncserver`, not `/usr/bin/vncserver` will be used, but `./unix/vncserver`. +Same for configuration files. + +Python specs can be run with `ty` alias inside devenv. + +### Debug output +To see `vncserver` output, run `ty -d`. Anything you print inside `vncserver` +with `say` should be visible that way. + +### Finding which spec produces a warning +By default, each passing test is represented as a dot. `-v` flag causes tests to +emit test descriptions alongside the program's output. Run `ty -v`. + # ARM KasmVNC is supported on ARM, however, the build process needs to be broken into two parts with one occuring on a x64 system and the other on an ARM system. All our testing and official builds are done on AWS Graviton instances. diff --git a/builder/build.sh b/builder/build.sh index ad439ad..402dcc6 100755 --- a/builder/build.sh +++ b/builder/build.sh @@ -23,7 +23,7 @@ cd /tmp # default to the version of x in Ubuntu 18.04, otherwise caller will need to specify XORG_VER=${XORG_VER:-"1.19.6"} XORG_PATCH=$(echo "$XORG_VER" | grep -Po '^\d.\d+' | sed 's#\.##') -wget https://www.x.org/archive/individual/xserver/xorg-server-${XORG_VER}.tar.bz2 +wget --no-check-certificate https://www.x.org/archive/individual/xserver/xorg-server-${XORG_VER}.tar.bz2 #git clone https://kasmweb@bitbucket.org/kasmtech/kasmvnc.git #cd kasmvnc diff --git a/builder/common.sh b/builder/common.sh new file mode 100644 index 0000000..50f2c06 --- /dev/null +++ b/builder/common.sh @@ -0,0 +1 @@ +VNC_PORT=8443 diff --git a/builder/devenv-vncserver b/builder/devenv-vncserver new file mode 100755 index 0000000..1ec401f --- /dev/null +++ b/builder/devenv-vncserver @@ -0,0 +1,20 @@ +#!/bin/bash + +set -e + +default_os=ubuntu +default_os_codename=focal + +cd "$(dirname "$0")/.." +. ./builder/os_ver_cli.sh +. ./builder/common.sh + +docker build --build-arg KASMVNC_PACKAGE_DIR="builder/build/${os_codename}" \ + -t kasmvncdevenv_${os}:$os_codename \ + -f builder/dockerfile.${os}_${os_codename}.vncserver_devenv.test . +docker run -it -v $(realpath ${PWD}):/src -p "443:$VNC_PORT" -p "$VNC_PORT:$VNC_PORT" \ + -p 8444:8444 \ + -p 8445:8445 --rm \ + -e KASMVNC_VERBOSE_LOGGING=$KASMVNC_VERBOSE_LOGGING \ + -e "VNC_USER=foo" -e "VNC_PW=foobar" \ + kasmvncdevenv_${os}:$os_codename diff --git a/builder/dockerfile.centos_core.barebones.rpm.test b/builder/dockerfile.centos_core.barebones.rpm.test index b5e22e9..84dcd08 100644 --- a/builder/dockerfile.centos_core.barebones.rpm.test +++ b/builder/dockerfile.centos_core.barebones.rpm.test @@ -1,18 +1,20 @@ FROM centos:centos7 +ENV STARTUPDIR=/dockerstartup + RUN yum install -y xterm RUN yum install -y vim less RUN yum install -y redhat-lsb-core +RUN yum install -y epel-release ARG KASMVNC_PACKAGE_DIR -COPY $KASMVNC_PACKAGE_DIR/*.rpm /tmp +COPY $KASMVNC_PACKAGE_DIR/*.rpm /tmp/ RUN yum localinstall -y /tmp/*.rpm -RUN useradd -m foo +RUN mkdir -p $STARTUPDIR +COPY startup/vnc_startup_barebones.sh $STARTUPDIR +RUN useradd -m foo USER foo:kasmvnc-cert -RUN mkdir ~/.vnc && echo '/usr/bin/xterm &' >> ~/.vnc/xstartup && \ - chmod +x ~/.vnc/xstartup - -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 " +ENTRYPOINT "/$STARTUPDIR/vnc_startup_barebones.sh" diff --git a/builder/dockerfile.centos_core.rpm.test b/builder/dockerfile.centos_core.rpm.test index 0634c40..4ae4377 100644 --- a/builder/dockerfile.centos_core.rpm.test +++ b/builder/dockerfile.centos_core.rpm.test @@ -41,13 +41,16 @@ RUN yum install -y vim less RUN echo 'source $STARTUPDIR/generate_container_user' >> $HOME/.bashrc RUN mkdir -p $STARTUPDIR -COPY startup/ $STARTUPDIR +COPY builder/startup/ $STARTUPDIR ### START CUSTOM STUFF #### +COPY ./builder/scripts/ /tmp/scripts/ +COPY ./centos/kasmvncserver.spec /tmp + ARG KASMVNC_PACKAGE_DIR -COPY $KASMVNC_PACKAGE_DIR/*.rpm /tmp -RUN yum localinstall -y /tmp/*.rpm +COPY $KASMVNC_PACKAGE_DIR/*.rpm /tmp/ +RUN /tmp/scripts/install_kasmvncserver_package ### END CUSTOM STUFF ### diff --git a/builder/dockerfile.debian_bullseye.deb.test b/builder/dockerfile.debian_bullseye.deb.test index 90ee85d..6980697 100644 --- a/builder/dockerfile.debian_bullseye.deb.test +++ b/builder/dockerfile.debian_bullseye.deb.test @@ -37,13 +37,16 @@ RUN apt-get update && apt-get -y install lsb-release RUN echo 'source $STARTUPDIR/generate_container_user' >> $HOME/.bashrc RUN mkdir -p $STARTUPDIR -COPY startup/ $STARTUPDIR +COPY builder/startup/ $STARTUPDIR ### START CUSTOM STUFF #### +COPY ./builder/scripts/ /tmp/scripts/ +COPY ./debian/changelog /tmp + ARG KASMVNC_PACKAGE_DIR COPY $KASMVNC_PACKAGE_DIR/kasmvncserver_*.deb /tmp/ -RUN rm -f /tmp/kasmvncserver_*+*.deb; dpkg -i /tmp/*.deb; apt-get -yf install +RUN /tmp/scripts/install_kasmvncserver_package ### END CUSTOM STUFF ### diff --git a/builder/dockerfile.debian_buster.barebones.deb.test b/builder/dockerfile.debian_buster.barebones.deb.test index 43147af..516bbca 100644 --- a/builder/dockerfile.debian_buster.barebones.deb.test +++ b/builder/dockerfile.debian_buster.barebones.deb.test @@ -1,19 +1,19 @@ FROM debian:buster-slim +ENV STARTUPDIR=/dockerstartup + +COPY ./builder/scripts/ /tmp/scripts/ +COPY ./debian/changelog /tmp + ARG KASMVNC_PACKAGE_DIR COPY $KASMVNC_PACKAGE_DIR/kasmvncserver_*.deb /tmp/ -RUN rm -f /tmp/kasmvncserver_*+*.deb; apt-get update && dpkg -i /tmp/*.deb; apt-get -yf install +RUN /tmp/scripts/install_kasmvncserver_package RUN apt-get update && apt-get -y install xterm -COPY startup/deb/kasmvncserver-easy-start /usr/local/bin +RUN mkdir -p $STARTUPDIR +COPY builder/startup/vnc_startup_barebones.sh $STARTUPDIR RUN useradd -m foo - USER foo:ssl-cert -RUN mkdir ~/.vnc && echo '/usr/bin/xterm &' >> ~/.vnc/xstartup && \ - chmod +x ~/.vnc/xstartup - -ENTRYPOINT bash -c "echo -e \"$VNC_PW\n$VNC_PW\n\" | \ - kasmvncpasswd -w -u $VNC_USER $HOME/.kasmpasswd && \ - kasmvncserver-easy-start && tail -f $HOME/.vnc/*.log" +ENTRYPOINT "/$STARTUPDIR/vnc_startup_barebones.sh" diff --git a/builder/dockerfile.debian_buster.deb.test b/builder/dockerfile.debian_buster.deb.test index 5b3753c..7193650 100644 --- a/builder/dockerfile.debian_buster.deb.test +++ b/builder/dockerfile.debian_buster.deb.test @@ -34,16 +34,26 @@ RUN apt-get purge -y pm-utils xscreensaver* RUN apt-get update && apt-get install -y vim less RUN apt-get update && apt-get -y install lsb-release +RUN apt-get update && apt-get install -y task-cinnamon-desktop +RUN apt-get update && apt-get install -y task-gnome-desktop +RUN mkdir -p /usr/share/man/man1 +RUN apt-get update && apt-get install -y apt-utils openjdk-11-jre +RUN apt-get update && apt-get install -y task-lxde-desktop +RUN apt-get update && apt-get install -y task-mate-desktop +RUN apt-get update && apt-get install -y task-kde-desktop + RUN echo 'source $STARTUPDIR/generate_container_user' >> $HOME/.bashrc RUN mkdir -p $STARTUPDIR -COPY startup/ $STARTUPDIR +COPY builder/startup/ $STARTUPDIR ### START CUSTOM STUFF #### +COPY ./builder/scripts/ /tmp/scripts/ +COPY ./debian/changelog /tmp ARG KASMVNC_PACKAGE_DIR COPY $KASMVNC_PACKAGE_DIR/kasmvncserver_*.deb /tmp/ -RUN rm -f /tmp/kasmvncserver_*+*.deb; dpkg -i /tmp/*.deb; apt-get -yf install +RUN /tmp/scripts/install_kasmvncserver_package ### END CUSTOM STUFF ### diff --git a/builder/dockerfile.fedora_thirtythree.barebones.rpm.test b/builder/dockerfile.fedora_thirtythree.barebones.rpm.test index b592c89..07f215c 100644 --- a/builder/dockerfile.fedora_thirtythree.barebones.rpm.test +++ b/builder/dockerfile.fedora_thirtythree.barebones.rpm.test @@ -1,18 +1,19 @@ FROM fedora:33 +ENV STARTUPDIR=/dockerstartup + 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 +COPY $KASMVNC_PACKAGE_DIR/*.rpm /tmp/ RUN dnf localinstall -y /tmp/*.rpm -RUN useradd -m foo +RUN mkdir -p $STARTUPDIR +COPY startup/vnc_startup_barebones.sh $STARTUPDIR +RUN useradd -m foo USER foo:kasmvnc-cert -RUN mkdir ~/.vnc && echo '/usr/bin/xterm &' >> ~/.vnc/xstartup && \ - chmod +x ~/.vnc/xstartup - -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 " +ENTRYPOINT "/$STARTUPDIR/vnc_startup_barebones.sh" diff --git a/builder/dockerfile.fedora_thirtythree.rpm.test b/builder/dockerfile.fedora_thirtythree.rpm.test index 895bbc1..fdbcd9c 100644 --- a/builder/dockerfile.fedora_thirtythree.rpm.test +++ b/builder/dockerfile.fedora_thirtythree.rpm.test @@ -35,22 +35,24 @@ RUN dnf install -y xorg-x11-xauth xorg-x11-xkb-utils \ xkeyboard-config xorg-x11-server-utils # xorg-x11-server-Xorg # RUN dnf install -y @xfce-desktop-environment -RUN dnf group install -y lxde-desktop RUN dnf erase -y pm-utils xscreensaver* RUN dnf install -y redhat-lsb-core RUN dnf install -y vim less +RUN dnf install -y @xfce-desktop-environment RUN echo 'source $STARTUPDIR/generate_container_user' >> $HOME/.bashrc RUN mkdir -p $STARTUPDIR -COPY startup/ $STARTUPDIR +COPY builder/startup/ $STARTUPDIR ### START CUSTOM STUFF #### +COPY ./builder/scripts/ /tmp/scripts/ +COPY ./centos/kasmvncserver.spec /tmp ARG KASMVNC_PACKAGE_DIR -COPY $KASMVNC_PACKAGE_DIR/*.rpm /tmp +COPY $KASMVNC_PACKAGE_DIR/*.rpm /tmp/ # RUN dnf remove -y tigervnc-server-minimal -RUN dnf localinstall -y --allowerasing /tmp/*.rpm +RUN /tmp/scripts/install_kasmvncserver_package ### END CUSTOM STUFF ### @@ -58,7 +60,4 @@ RUN chown -R 1000:0 $HOME USER 1000:kasmvnc-cert WORKDIR $HOME -RUN mkdir ~/.vnc && echo '/usr/bin/lxsession -s LXDE &' >> ~/.vnc/xstartup && \ - chmod +x ~/.vnc/xstartup - ENTRYPOINT [ "/dockerstartup/vnc_startup.sh" ] diff --git a/builder/dockerfile.kali_kali-rolling.deb.test b/builder/dockerfile.kali_kali-rolling.deb.test index 5d95f99..8c6ee46 100644 --- a/builder/dockerfile.kali_kali-rolling.deb.test +++ b/builder/dockerfile.kali_kali-rolling.deb.test @@ -37,13 +37,15 @@ RUN apt-get update && apt-get -y install lsb-release RUN echo 'source $STARTUPDIR/generate_container_user' >> $HOME/.bashrc RUN mkdir -p $STARTUPDIR -COPY startup/ $STARTUPDIR +COPY builder/startup/ $STARTUPDIR ### START CUSTOM STUFF #### +COPY ./builder/scripts/ /tmp/scripts/ +COPY ./debian/changelog /tmp ARG KASMVNC_PACKAGE_DIR COPY $KASMVNC_PACKAGE_DIR/kasmvncserver_*.deb /tmp/ -RUN rm -f /tmp/kasmvncserver_*+*.deb; dpkg -i /tmp/*.deb; apt-get -yf install +RUN /tmp/scripts/install_kasmvncserver_package ### END CUSTOM STUFF ### diff --git a/builder/dockerfile.opensuse_15.barebones.rpm.test b/builder/dockerfile.opensuse_15.barebones.rpm.test index c7bf3b1..89f1fbf 100644 --- a/builder/dockerfile.opensuse_15.barebones.rpm.test +++ b/builder/dockerfile.opensuse_15.barebones.rpm.test @@ -1,5 +1,7 @@ FROM opensuse/leap:15.3 +ENV STARTUPDIR=/dockerstartup + # base tools RUN zypper -n install -y \ less \ @@ -27,11 +29,10 @@ ARG KASMVNC_PACKAGE_DIR COPY $KASMVNC_PACKAGE_DIR/*.rpm /tmp RUN zypper install -y --allow-unsigned-rpm /tmp/*.rpm -RUN useradd -m foo +RUN mkdir -p $STARTUPDIR +COPY startup/vnc_startup_barebones.sh $STARTUPDIR +RUN useradd -m foo USER foo:kasmvnc-cert -RUN mkdir ~/.vnc && echo '/usr/bin/xterm &' >> ~/.vnc/xstartup && \ - chmod +x ~/.vnc/xstartup - -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 " +ENTRYPOINT "/$STARTUPDIR/vnc_startup_barebones.sh" diff --git a/builder/dockerfile.oracle_8.barebones.rpm.test b/builder/dockerfile.oracle_8.barebones.rpm.test index e04170e..d4cc293 100644 --- a/builder/dockerfile.oracle_8.barebones.rpm.test +++ b/builder/dockerfile.oracle_8.barebones.rpm.test @@ -1,20 +1,23 @@ FROM oraclelinux:8 +ENV STARTUPDIR=/dockerstartup + RUN dnf install -y \ less \ redhat-lsb-core \ vim \ xterm - +RUN dnf config-manager --set-enabled ol8_codeready_builder +RUN dnf install -y oracle-epel-release-el8 + ARG KASMVNC_PACKAGE_DIR COPY $KASMVNC_PACKAGE_DIR/*.rpm /tmp RUN dnf localinstall -y /tmp/*.rpm -RUN useradd -m foo +RUN mkdir -p $STARTUPDIR +COPY startup/vnc_startup_barebones.sh $STARTUPDIR +RUN useradd -m foo USER foo:kasmvnc-cert -RUN mkdir ~/.vnc && echo '/usr/bin/xterm &' >> ~/.vnc/xstartup && \ - chmod +x ~/.vnc/xstartup - -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 " +ENTRYPOINT "/$STARTUPDIR/vnc_startup_barebones.sh" diff --git a/builder/dockerfile.ubuntu_bionic+libjpeg-turbo_latest.deb.build b/builder/dockerfile.ubuntu_bionic+libjpeg-turbo_latest.deb.build index ab88621..7782f10 100644 --- a/builder/dockerfile.ubuntu_bionic+libjpeg-turbo_latest.deb.build +++ b/builder/dockerfile.ubuntu_bionic+libjpeg-turbo_latest.deb.build @@ -4,7 +4,7 @@ RUN apt-get update && \ apt-get -y install vim build-essential devscripts equivs ARG KASMVNC_PACKAGE_DIR -COPY $KASMVNC_PACKAGE_DIR/libjpeg-turbo*deb /tmp +COPY $KASMVNC_PACKAGE_DIR/libjpeg-turbo*deb /tmp/ RUN apt-get install /tmp/libjpeg-turbo*deb # Install build-deps for the package. diff --git a/builder/dockerfile.ubuntu_bionic+libjpeg-turbo_latest.deb.test b/builder/dockerfile.ubuntu_bionic+libjpeg-turbo_latest.deb.test index 9f3236b..635ee0f 100644 --- a/builder/dockerfile.ubuntu_bionic+libjpeg-turbo_latest.deb.test +++ b/builder/dockerfile.ubuntu_bionic+libjpeg-turbo_latest.deb.test @@ -37,17 +37,19 @@ RUN apt-get update && apt-get -y install lsb-release RUN echo 'source $STARTUPDIR/generate_container_user' >> $HOME/.bashrc RUN mkdir -p $STARTUPDIR -COPY startup/ $STARTUPDIR +COPY builder/startup/ $STARTUPDIR ### START CUSTOM STUFF #### +COPY ./builder/scripts/ /tmp/scripts/ +COPY ./debian/changelog /tmp ARG KASMVNC_PACKAGE_DIR -COPY $KASMVNC_PACKAGE_DIR/libjpeg-turbo_*.deb /tmp +COPY $KASMVNC_PACKAGE_DIR/libjpeg-turbo_*.deb /tmp/ RUN apt-get install /tmp/libjpeg-turbo*deb ARG BUILD_DEBIAN_REVISION -COPY $KASMVNC_PACKAGE_DIR/kasmvncserver_*$BUILD_DEBIAN_REVISION*.deb /tmp -RUN dpkg -i /tmp/*.deb; apt-get -yf install +COPY $KASMVNC_PACKAGE_DIR/kasmvncserver_*$BUILD_DEBIAN_REVISION*.deb /tmp/ +RUN /tmp/scripts/install_kasmvncserver_package "$BUILD_DEBIAN_REVISION" ### END CUSTOM STUFF ### diff --git a/builder/dockerfile.ubuntu_bionic.deb.test b/builder/dockerfile.ubuntu_bionic.deb.test index 7230475..8d68055 100644 --- a/builder/dockerfile.ubuntu_bionic.deb.test +++ b/builder/dockerfile.ubuntu_bionic.deb.test @@ -37,13 +37,16 @@ RUN apt-get update && apt-get -y install lsb-release RUN echo 'source $STARTUPDIR/generate_container_user' >> $HOME/.bashrc RUN mkdir -p $STARTUPDIR -COPY startup/ $STARTUPDIR +COPY builder/startup/ $STARTUPDIR ### START CUSTOM STUFF #### +COPY ./builder/scripts/ /tmp/scripts/ +COPY ./debian/changelog /tmp + ARG KASMVNC_PACKAGE_DIR COPY $KASMVNC_PACKAGE_DIR/kasmvncserver_*.deb /tmp/ -RUN rm -f /tmp/kasmvncserver_*+*.deb; dpkg -i /tmp/*.deb; apt-get -yf install +RUN /tmp/scripts/install_kasmvncserver_package ### END CUSTOM STUFF ### diff --git a/builder/dockerfile.ubuntu_bionic.test b/builder/dockerfile.ubuntu_bionic.test index 35470ff..de10bd1 100644 --- a/builder/dockerfile.ubuntu_bionic.test +++ b/builder/dockerfile.ubuntu_bionic.test @@ -35,7 +35,7 @@ RUN apt-get purge -y pm-utils xscreensaver* RUN echo 'source $STARTUPDIR/generate_container_user' >> $HOME/.bashrc RUN mkdir -p $STARTUPDIR -COPY startup/ $STARTUPDIR +COPY builder/startup/ $STARTUPDIR ### START CUSTOM STUFF #### diff --git a/builder/dockerfile.ubuntu_focal.barebones.deb.test b/builder/dockerfile.ubuntu_focal.barebones.deb.test index ae046ad..9b16154 100644 --- a/builder/dockerfile.ubuntu_focal.barebones.deb.test +++ b/builder/dockerfile.ubuntu_focal.barebones.deb.test @@ -1,15 +1,16 @@ FROM ubuntu:focal +ENV STARTUPDIR=/dockerstartup + ARG KASMVNC_PACKAGE_DIR COPY $KASMVNC_PACKAGE_DIR/kasmvncserver_*.deb /tmp/ RUN rm -f /tmp/kasmvncserver_*+*.deb; apt-get update && dpkg -i /tmp/*.deb; apt-get -yf install RUN apt-get update && apt-get -y install xterm lsb-release -RUN useradd -m foo && addgroup foo ssl-cert +RUN mkdir -p $STARTUPDIR +COPY builder/startup/vnc_startup_barebones.sh $STARTUPDIR +RUN useradd -m foo && addgroup foo ssl-cert USER foo -RUN mkdir ~/.vnc && echo '/usr/bin/xterm &' >> ~/.vnc/xstartup && \ - chmod +x ~/.vnc/xstartup - -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 " +ENTRYPOINT "/$STARTUPDIR/vnc_startup_barebones.sh" diff --git a/builder/dockerfile.ubuntu_focal.deb.test b/builder/dockerfile.ubuntu_focal.deb.test index 867dbd7..16cd8a0 100644 --- a/builder/dockerfile.ubuntu_focal.deb.test +++ b/builder/dockerfile.ubuntu_focal.deb.test @@ -37,13 +37,16 @@ RUN apt-get update && apt-get -y install lsb-release RUN echo 'source $STARTUPDIR/generate_container_user' >> $HOME/.bashrc RUN mkdir -p $STARTUPDIR -COPY startup/ $STARTUPDIR +COPY builder/startup/ $STARTUPDIR ### START CUSTOM STUFF #### +COPY ./builder/scripts/ /tmp/scripts/ +COPY ./debian/changelog /tmp + ARG KASMVNC_PACKAGE_DIR COPY $KASMVNC_PACKAGE_DIR/kasmvncserver_*.deb /tmp/ -RUN rm -f /tmp/kasmvncserver_*+*.deb; dpkg -i /tmp/*.deb; apt-get -yf install +RUN /tmp/scripts/install_kasmvncserver_package RUN mkdir ~/.vnc && echo '/usr/bin/xfce4-session &' >> ~/.vnc/xstartup && \ chmod +x ~/.vnc/xstartup diff --git a/builder/dockerfile.ubuntu_focal.specs.test b/builder/dockerfile.ubuntu_focal.specs.test new file mode 100644 index 0000000..663fe3d --- /dev/null +++ b/builder/dockerfile.ubuntu_focal.specs.test @@ -0,0 +1,30 @@ +FROM ubuntu:focal + +ENV DEBIAN_FRONTEND=noninteractive + +RUN apt-get update && apt-get install -y vim less +RUN apt-get update && apt-get install -y python3-pip +RUN apt-get update && apt-get install -y strace silversearcher-ag xfonts-base +RUN apt-get update && apt-get install -y cinnamon +RUN apt-get update && apt-get install -y mate + +RUN useradd -m docker + +ARG KASMVNC_PACKAGE_DIR +COPY $KASMVNC_PACKAGE_DIR/kasmvncserver_*.deb /tmp/ +RUN apt-get install -y /tmp/*$(dpkg --print-architecture).deb + +ENV USER docker +ENV HOME /home/docker + +RUN chown -R 1000:0 $HOME +USER 1000:ssl-cert +WORKDIR $HOME + +RUN pip3 install --user pipenv +RUN echo 'PATH="~/.local/bin/:$PATH"' >> ~/.bashrc + +ENV LC_ALL=C.UTF-8 +ENV LANG=C.UTF-8 + +ENTRYPOINT ["/bin/bash", "-ic", "/src/builder/run-specs-inside-docker"] diff --git a/builder/dockerfile.ubuntu_focal.test b/builder/dockerfile.ubuntu_focal.test index 18bf413..7cf307b 100644 --- a/builder/dockerfile.ubuntu_focal.test +++ b/builder/dockerfile.ubuntu_focal.test @@ -35,7 +35,7 @@ RUN apt-get purge -y pm-utils xscreensaver* RUN echo 'source $STARTUPDIR/generate_container_user' >> $HOME/.bashrc RUN mkdir -p $STARTUPDIR -COPY startup/ $STARTUPDIR +COPY builder/startup/ $STARTUPDIR ### START CUSTOM STUFF #### diff --git a/builder/dockerfile.ubuntu_focal.vncserver_devenv.test b/builder/dockerfile.ubuntu_focal.vncserver_devenv.test new file mode 100644 index 0000000..7ce198a --- /dev/null +++ b/builder/dockerfile.ubuntu_focal.vncserver_devenv.test @@ -0,0 +1,67 @@ +FROM ubuntu:focal + +ENV DEBIAN_FRONTEND=noninteractive +ENV VNC_PORT 8443 +ENV VNC_PORT2 8444 +ENV VNC_PORT3 8445 +EXPOSE $VNC_PORT +EXPOSE $VNC_PORT2 +EXPOSE $VNC_PORT3 + +RUN apt-get update && apt-get install -y supervisor xfce4 xfce4-terminal xterm libnss-wrapper gettext wget +RUN apt-get update && apt-get install -y vim less +RUN apt-get update && apt-get -y install lsb-release +RUN apt-get update && apt-get -y install net-tools + +# RUN mkdir -p /usr/share/man/man1 +# RUN apt-get update && apt-get install -y apt-utils openjdk-11-jre +RUN apt-get update && apt-get install -y ubuntu-mate-desktop +RUN apt-get update && apt-get install -y lxde +RUN apt-get update && apt-get install -y lxqt +RUN apt-get update && apt-get install -y kde-plasma-desktop +RUN apt-get update && apt-get install -y cinnamon +RUN apt-get update && apt-get install -y gnome-session +RUN apt-get purge -y pm-utils xscreensaver* +RUN apt-get purge -y clipit magnus kgpg + +RUN apt-get update && apt-get install -y python3-pip +RUN apt-get update && apt-get install -y strace +RUN apt-get update && apt-get install -y silversearcher-ag +RUN apt-get update && apt-get install -y libdevel-stacktrace-perl + +RUN useradd -m docker +ENV USER docker + +COPY ./builder/scripts/ /tmp/scripts/ +COPY ./debian/changelog /tmp + +ARG KASMVNC_PACKAGE_DIR +COPY $KASMVNC_PACKAGE_DIR/kasmvncserver_*.deb /tmp/ +RUN /tmp/scripts/install_kasmvncserver_package + +ENV KASMVNC_DEVELOPMENT 1 +ENV HOME /home/docker +RUN chown -R 1000:0 $HOME +USER 1000:ssl-cert +WORKDIR $HOME + +RUN pip3 install --user pipenv +RUN echo 'PATH="/src/unix:~/.local/bin/:$PATH"' >> ~/.bashrc +RUN echo 'export PERL5LIB="/src/unix"' >> ~/.bashrc +RUN echo 'ulimit -c 0' >> ~/.bashrc +RUN echo 'alias s="vncserver"' >> ~/.bashrc +RUN echo 'alias k="vncserver -kill :1; pkill baloo_file; pkill gpg-agent; pkill ssh-agent; pkill xiccd"' >> ~/.bashrc +RUN echo 'alias r="k; s"' >> ~/.bashrc +RUN echo 'alias go="vncserver; vncserver -kill :1"' >> ~/.bashrc +RUN echo 'alias ns="netstat -nltup"' >> ~/.bashrc +RUN echo 'alias tv="./run-specs spec/vncserver_yaml_validation_spec.py"' >> ~/.bashrc +RUN echo 'alias ty="./run-specs spec/vncserver_*spec.py"' >> ~/.bashrc +RUN echo 'alias ta="./run-specs"' >> ~/.bashrc +RUN echo 'alias vd="vncserver -dry-run"' >> ~/.bashrc +ENV SET_PASSWORD_FUNC 'sp() { echo -e "$VNC_PW\\n$VNC_PW\\n" | kasmvncpasswd -w -u $USER $HOME/.kasmpasswd; }' +RUN echo $SET_PASSWORD_FUNC >> ~/.bashrc + +ENV LC_ALL=C.UTF-8 +ENV LANG=C.UTF-8 + +ENTRYPOINT ["bash", "-ic", "cd /src && pipenv install; exec bash"] diff --git a/builder/dockerfile.ubuntu_jammy.deb.test b/builder/dockerfile.ubuntu_jammy.deb.test index 6af7d22..4e5f0dd 100644 --- a/builder/dockerfile.ubuntu_jammy.deb.test +++ b/builder/dockerfile.ubuntu_jammy.deb.test @@ -37,7 +37,7 @@ RUN apt-get update && apt-get -y install lsb-release RUN echo 'source $STARTUPDIR/generate_container_user' >> $HOME/.bashrc RUN mkdir -p $STARTUPDIR -COPY startup/ $STARTUPDIR +COPY builder/startup/ $STARTUPDIR ### START CUSTOM STUFF #### diff --git a/builder/os_ver_cli.sh b/builder/os_ver_cli.sh index 9dc4f80..f23fb9e 100644 --- a/builder/os_ver_cli.sh +++ b/builder/os_ver_cli.sh @@ -1,9 +1,9 @@ -default_os=ubuntu -default_os_codename=bionic +default_os=${default_os:-ubuntu} +default_os_codename=${default_os_codename:-bionic} os=${1:-$default_os} os_codename=${2:-$default_os_codename} -build_tag="${3:-}" +build_tag="$3" if [[ -n "$build_tag" ]]; then build_tag_for_images="_${build_tag#+}" build_debian_revision="$(echo $build_tag | tr _ -)" diff --git a/builder/run-specs-inside-docker b/builder/run-specs-inside-docker new file mode 100755 index 0000000..7951832 --- /dev/null +++ b/builder/run-specs-inside-docker @@ -0,0 +1,18 @@ +#!/usr/bin/env bash + +set -euo pipefail +logfile=/tmp/specs.log +red='\033[0;31m' +no_color='\033[0m' + +fail_on_perl_warnings() { + if grep -qP 'line \d+\.$' "$logfile"; then + echo -e "${red}Failure: Perl warnings were found${no_color}" + false + fi +} + +cd /src +pipenv install +./run-specs 2>&1 | tee "$logfile" +fail_on_perl_warnings diff --git a/builder/scripts/install_kasmvncserver_package b/builder/scripts/install_kasmvncserver_package new file mode 100755 index 0000000..7537d57 --- /dev/null +++ b/builder/scripts/install_kasmvncserver_package @@ -0,0 +1,64 @@ +#!/bin/bash + +set -e + +if [[ -n "$1" ]]; then + tag="$1" +fi + +is_debian() { + [[ -f /etc/debian_version ]] +} + +check_package_version_exists() { + if ! stat /tmp/kasmvncserver_"$package_version"*.deb; then + >&2 echo "No package found for version $package_version" + exit 1 + fi +} + +remove_tagged_debs_unless_tag_used() { + if [[ -n "$tag" ]]; then + return + fi + + rm -f /tmp/kasmvncserver_*+*.deb +} + +detect_rpm_package_manager() { + if command -v dnf >/dev/null; then + rpm_package_manager=dnf + else + rpm_package_manager=yum + fi +} + +install_package_built_for_current_branch_package_version_deb() { + remove_tagged_debs_unless_tag_used + + apt-get update + apt-get install -y dpkg-dev + package_version=$(dpkg-parsechangelog --show-field Version \ + --file /tmp/changelog) + + check_package_version_exists + apt-get install -y /tmp/kasmvncserver_"$package_version"*"$tag"*.deb +} + +install_package_built_for_current_branch_package_version_rpm() { + detect_rpm_package_manager + $rpm_package_manager install -y rpmdevtools + + package_version=$(rpmspec -q --qf '%{version}\n' /tmp/kasmvncserver.spec 2>/dev/null) + if [[ $rpm_package_manager = "dnf" ]]; then + dnf localinstall -y --allowerasing /tmp/kasmvncserver-"$package_version"*.rpm + else + yum install -y /tmp/kasmvncserver-"$package_version"*.rpm + fi +} + +if is_debian ; then + install_package_built_for_current_branch_package_version_deb +else + install_package_built_for_current_branch_package_version_rpm +fi diff --git a/builder/startup/deb/cli-processing.sh b/builder/startup/deb/cli-processing.sh new file mode 100644 index 0000000..f0e7fc6 --- /dev/null +++ b/builder/startup/deb/cli-processing.sh @@ -0,0 +1,50 @@ +#!/bin/bash + +debug() { + if [ -z "$debug" ]; then return; fi + + echo "$@" +} + +enable_debug() { + debug=1 + log_option="-log *:stderr:100" +} + +kill_vnc_server() { + vncserver -kill $display +} + +process_cli_options() { + for option in "$@"; do + case "$option" in + --help) + show_help + exit + ;; + -d|--debug) + enable_debug + ;; + -k|--kill) + kill_vnc_server + exit + ;; + -s|--select-de) + action=select-de-and-start + ;; + *) + echo >&2 "Unsupported argument: $option" + exit 1 + esac + done +} + +show_help() { + cat >&2 <<-USAGE +Usage: $(basename "$0") [options] + -d, --debug Debug output + -k, --kill Kill vncserver + -s, --select-de Select desktop environent to run + --help Show this help +USAGE +} diff --git a/builder/startup/deb/kasmvncserver-easy-start b/builder/startup/deb/kasmvncserver-easy-start index 0d0bc88..538bbd6 100755 --- a/builder/startup/deb/kasmvncserver-easy-start +++ b/builder/startup/deb/kasmvncserver-easy-start @@ -4,43 +4,8 @@ set -e display=:10 interface=0.0.0.0 -cert_group=ssl-cert -if [[ "$1" = "--help" ]]; then - cat >&2 <<-USAGE -Usage: `basename $0` [options] - -d Debug output - -kill Kill vncserver - --help show this help -USAGE - exit -fi - -if [[ "$1" = "-d" ]]; then - log_option="-log *:stderr:100" -fi - -action=start -if [[ "$1" = "-kill" ]]; then - action=kill -fi - -if groups | grep -qvw ssl-cert; then - cat <<-EOF - Can't access TLS certificate. - Please add your user to $cert_group via 'addgroup ssl-cert' -EOF - exit 1 -fi - -if [[ "$action" = "kill" ]]; then - vncserver -kill $display - exit -fi - -vncserver $display -interface $interface -vncserver -kill $display vncserver $display -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 $interface -httpd /usr/share/kasmvnc/www $log_option + -interface $interface -httpd /usr/share/kasmvnc/www diff --git a/builder/startup/deb/select-de.sh b/builder/startup/deb/select-de.sh new file mode 100755 index 0000000..4a02804 --- /dev/null +++ b/builder/startup/deb/select-de.sh @@ -0,0 +1,266 @@ +#!/bin/bash + +set -e + +xstartup_script=~/.vnc/xstartup +de_was_selected_file="$HOME/.vnc/.de-was-selected" + +debug() { + if [ -z "$debug" ]; then return; fi + + echo "$@" +} + +enable_debug() { + debug=1 +} + +process_cli_options() { + while [ $# -gt 0 ]; do + local option="$1" + shift + + case "$option" in + --help|-h) + show_help + exit + ;; + -d|--debug) + enable_debug + ;; + -y|--assume-yes) + assume_yes=1 + ;; + -s|--select-de) + action=select-de + if [[ -n "$1" && "${1:0:1}" != "-" ]]; then + selected_de="$1" + assume_yes_for_xstartup_overwrite=1 + if [ "$selected_de" = "manual" ]; then + selected_de="$manual_xstartup_choice" + fi + shift + fi + ;; + *) + echo >&2 "Unsupported argument: $option" + exit 1 + esac + done +} + +show_help() { + cat >&2 <<-USAGE +Usage: $(basename "$0") [options] + -d, --debug Debug output + -y, --assume-yes Automatic "yes" to prompts + -s, --select-de Select desktop environent to run + --help Show this help +USAGE +} + +add_uppercase_desktop_environment_keys() { + local de_cmd + + for de in "${!all_desktop_environments[@]}"; do + de_cmd=${all_desktop_environments[$de]}; + all_desktop_environments[${de^^}]="$de_cmd" + done +} + +manual_xstartup_choice="Manually edit xstartup" + +process_cli_options "$@" + +declare -A all_desktop_environments=( + [Cinnamon]="exec cinnamon-session" + [Mate]="XDG_CURRENT_DESKTOP=MATE exec dbus-launch --exit-with-session mate-session" + [LXDE]="exec lxsession" + [Lxqt]="exec startlxqt" + [KDE]="exec startkde" + [Gnome]="XDG_CURRENT_DESKTOP=GNOME exec dbus-launch --exit-with-session /usr/bin/gnome-session" + [XFCE]="exec xfce4-session") + +readarray -t sorted_desktop_environments < <(for de in "${!all_desktop_environments[@]}"; do echo "$de"; done | sort) + +all_desktop_environments[$manual_xstartup_choice]="" +sorted_desktop_environments+=("$manual_xstartup_choice") +add_uppercase_desktop_environment_keys + +detected_desktop_environments=() +declare -A numbered_desktop_environments + +print_detected_desktop_environments() { + declare -i i=1 + + echo "Please choose Desktop Environment to run:" + for detected_de in "${detected_desktop_environments[@]}"; do + echo "[$i] $detected_de" + numbered_desktop_environments[$i]=$detected_de + i+=1 + done +} + +detect_desktop_environments() { + for de_name in "${sorted_desktop_environments[@]}"; do + if [[ "$de_name" = "$manual_xstartup_choice" ]]; then + detected_desktop_environments+=("$de_name") + continue; + fi + + local executable=${all_desktop_environments[$de_name]} + executable=($executable) + executable=${executable[-1]} + + if detect_desktop_environment "$de_name" "$executable"; then + detected_desktop_environments+=("$de_name") + fi + done +} + +ask_user_to_choose_de() { + while : ; do + print_detected_desktop_environments + read -r de_number_to_run + if [[ -z "$de_number_to_run" ]]; then + continue + fi + de_name_from_number "$de_number_to_run" + if [[ -n "$de_name" ]]; then + break; + fi + + echo "Incorrect number: $de_number_to_run" + echo + done +} + +remember_de_choice() { + touch "$de_was_selected_file" +} + +de_was_selected_on_previous_run() { + [[ -f "$de_was_selected_file" ]] +} + +detect_desktop_environment() { + local de_name="$1" + local executable="$2" + + if command -v "$executable" &>/dev/null; then + return 0 + fi + + return 1 +} + +did_user_forbid_replacing_xstartup() { + grep -q -v KasmVNC-safe-to-replace-this-file "$xstartup_script" +} + +de_cmd_from_name() { + de_cmd=${all_desktop_environments[${de_name^^}]} +} + +de_name_from_number() { + local de_number_to_run="$1" + + de_name=${numbered_desktop_environments[$de_number_to_run]} +} + +warn_xstartup_will_be_overwriten() { + if [[ -n "$assume_yes" || -n "$assume_yes_for_xstartup_overwrite" ]]; then + return 0 + fi + + if [ ! -f "$xstartup_script" ]; then + return 0 + fi + + echo -n "WARNING: $xstartup_script will be overwritten y/N?" + read -r do_overwrite_xstartup + if [[ "$do_overwrite_xstartup" = "y" || "$do_overwrite_xstartup" = "Y" ]]; then + return 0 + fi + + return 1 +} + +setup_de_to_run_via_xstartup() { + warn_xstartup_will_be_overwriten + generate_xstartup "$de_name" +} + +generate_xstartup() { + local de_name="$1" + + de_cmd_from_name + + cat <<-SCRIPT > "$xstartup_script" +#!/bin/sh +$de_cmd +SCRIPT + chmod +x "$xstartup_script" +} + +user_asked_to_select_de() { + [[ "$action" = "select-de" ]] +} + +user_specified_de() { + [ -n "$selected_de" ] +} + +check_de_name_is_valid() { + local selected_de="$1" + if [[ "$selected_de" = "$manual_xstartup_choice" ]]; then + return 0; + fi + + local de_cmd=${all_desktop_environments["${selected_de^^}"]:-} + if [ -z "$de_cmd" ]; then + echo >&2 "'$selected_de': not supported Desktop Environment" + return 1 + fi +} + +de_installed() { + local de_name="${1^^}" + + for de in "${detected_desktop_environments[@]}"; do + if [ "${de^^}" = "$de_name" ]; then + return 0 + fi + done + + return 1 +} + +check_de_installed() { + local de_name="$1" + + if ! de_installed "$de_name"; then + echo >&2 "'$de_name': Desktop Environment not installed" + return 1 + fi +} + +if user_asked_to_select_de || ! de_was_selected_on_previous_run; then + if user_specified_de; then + check_de_name_is_valid "$selected_de" + fi + + detect_desktop_environments + if user_specified_de; then + check_de_installed "$selected_de" + de_name="$selected_de" + else + ask_user_to_choose_de + fi + + debug "You selected $de_name desktop environment" + if [[ "$de_name" != "$manual_xstartup_choice" ]]; then + setup_de_to_run_via_xstartup + fi + remember_de_choice +fi diff --git a/builder/startup/vnc_startup.sh b/builder/startup/vnc_startup.sh index c0c3b3c..570d81c 100755 --- a/builder/startup/vnc_startup.sh +++ b/builder/startup/vnc_startup.sh @@ -12,28 +12,6 @@ cleanup () { } trap cleanup SIGINT SIGTERM -detect_www_dir() { - local package_www_dir="/usr/share/kasmvnc/www" - if [[ -d "$package_www_dir" ]]; then - package_www_dir_option="-httpd $package_www_dir" - fi -} - -detect_cert_location() { - local tarball_cert="$HOME/.vnc/self.pem" - local deb_cert="/etc/ssl/certs/ssl-cert-snakeoil.pem" - local deb_key="/etc/ssl/private/ssl-cert-snakeoil.key" - local rpm_cert="/etc/pki/tls/private/kasmvnc.pem" - - if [[ -f "$deb_cert" ]]; then - cert_option="-cert $deb_cert -key $deb_key" - elif [[ -f "$rpm_cert" ]]; then - cert_option="-cert $rpm_cert" - else - cert_option="-cert $tarball_cert" - fi -} - add_vnc_user() { local username="$1" local password="$2" @@ -61,9 +39,6 @@ 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" -vncserver :1 -interface 0.0.0.0 -vncserver -kill :1 - if [[ -f $PASSWD_PATH ]]; then rm -f $PASSWD_PATH fi @@ -84,12 +59,10 @@ vncserver -kill $DISPLAY &> $HOME/.vnc/vnc_startup.log \ || echo "no locks present" -detect_www_dir -detect_cert_location -[ -n "$KASMVNC_VERBOSE_LOGGING" ] && verbose_logging_option="-log *:stderr:100" +[ -n "$KASMVNC_VERBOSE_LOGGING" ] && verbose_logging_option="-debug" 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 $verbose_logging_option #&> $STARTUPDIR/no_vnc_startup.log +vncserver $DISPLAY -select-de xfce -depth $VNC_COL_DEPTH -geometry $VNC_RESOLUTION -FrameRate=$MAX_FRAME_RATE -websocketPort $VNC_PORT $VNCOPTIONS $verbose_logging_option #&> $STARTUPDIR/no_vnc_startup.log PID_SUN=$! diff --git a/builder/startup/vnc_startup_barebones.sh b/builder/startup/vnc_startup_barebones.sh new file mode 100755 index 0000000..2fea7f8 --- /dev/null +++ b/builder/startup/vnc_startup_barebones.sh @@ -0,0 +1,22 @@ +#!/bin/bash + +set -euo pipefail + +set_xterm_to_run() { + mkdir "$config_dir" + echo '/usr/bin/xterm &' >> "$xstartup" + chmod +x "$xstartup" +} + +create_kasm_user() { + echo -e "$VNC_PW\n$VNC_PW\n" | kasmvncpasswd -w -u "$VNC_USER" +} + +config_dir="$HOME/.vnc" +xstartup="$config_dir/xstartup" + +set_xterm_to_run +create_kasm_user + +vncserver -select-de manual -websocketPort "$VNC_PORT" +tail -f "$config_dir"/*.log diff --git a/builder/test-deb b/builder/test-deb index fa135d9..50accfa 100755 --- a/builder/test-deb +++ b/builder/test-deb @@ -2,15 +2,17 @@ set -e -cd "$(dirname "$0")" -. ./os_ver_cli.sh +cd "$(dirname "$0")/.." +. ./builder/os_ver_cli.sh +. ./builder/common.sh -tester_image=kasmvnctester_${os}:$os_codename${build_tag_for_images} -docker build --build-arg KASMVNC_PACKAGE_DIR="build/${os_codename}" \ +tester_image="builder/kasmvnctester_${os}:$os_codename${build_tag_for_images}" +docker build --build-arg KASMVNC_PACKAGE_DIR="builder/build/${os_codename}" \ --build-arg BUILD_DEBIAN_REVISION="$build_debian_revision" \ -t "$tester_image" \ - -f dockerfile.${os}_${os_codename}${build_tag}.deb.test . -docker run -it -p 443:8443 --rm \ + -f "builder/dockerfile.${os}_${os_codename}${build_tag}.deb.test" . +docker run -it -p "443:$VNC_PORT" --rm \ -e KASMVNC_VERBOSE_LOGGING=$KASMVNC_VERBOSE_LOGGING \ -e "VNC_USER=foo" -e "VNC_PW=foobar" \ + -e "VNC_PORT=$VNC_PORT" \ "$tester_image" diff --git a/builder/test-deb-barebones b/builder/test-deb-barebones index 4fe878c..d29978e 100755 --- a/builder/test-deb-barebones +++ b/builder/test-deb-barebones @@ -2,13 +2,15 @@ set -e -cd "$(dirname "$0")" +cd "$(dirname "$0")/.." +. ./builder/common.sh os="${1:-debian}" os_codename="${2:-buster}" -docker build --build-arg KASMVNC_PACKAGE_DIR="build/${os_codename}" \ +docker build --build-arg KASMVNC_PACKAGE_DIR="builder/build/${os_codename}" \ -t kasmvnctester_barebones_${os}:$os_codename \ - -f dockerfile.${os}_${os_codename}.barebones.deb.test . + -f builder/dockerfile.${os}_${os_codename}.barebones.deb.test . echo -docker run -it -p 443:8443 --rm -e "VNC_USER=foo" -e "VNC_PW=foobar" \ +docker run -it -p "443:$VNC_PORT" --rm -e "VNC_USER=foo" -e "VNC_PW=foobar" \ + -e "VNC_PORT=$VNC_PORT" \ kasmvnctester_barebones_${os}:$os_codename diff --git a/builder/test-rpm b/builder/test-rpm index 719b65f..fbf731e 100755 --- a/builder/test-rpm +++ b/builder/test-rpm @@ -2,16 +2,18 @@ set -e -cd "$(dirname "$0")" -. ./os_ver_cli.sh +cd "$(dirname "$0")/.." +. ./builder/os_ver_cli.sh +. ./builder/common.sh echo kasmvnctester_${os}:$os_codename docker build --build-arg \ - KASMVNC_PACKAGE_DIR="build/${os}_${os_codename}" \ + KASMVNC_PACKAGE_DIR="builder/build/${os}_${os_codename}" \ -t kasmvnctester_${os}:$os_codename \ - -f dockerfile.${os}_${os_codename}.rpm.test . + -f builder/dockerfile.${os}_${os_codename}.rpm.test . -docker run -it -p 443:8443 --rm \ +docker run -it -p "443:$VNC_PORT" --rm \ -e KASMVNC_VERBOSE_LOGGING=$KASMVNC_VERBOSE_LOGGING \ -e "VNC_USER=foo" -e "VNC_PW=foobar" \ + -e "VNC_PORT=$VNC_PORT" \ kasmvnctester_${os}:$os_codename diff --git a/builder/test-rpm-barebones b/builder/test-rpm-barebones index 7ad42cf..b804a75 100755 --- a/builder/test-rpm-barebones +++ b/builder/test-rpm-barebones @@ -3,11 +3,13 @@ set -e cd "$(dirname "$0")" +. ./common.sh os="${1:-centos}" 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=foobar" \ +docker run -it -p "443:$VNC_PORT" --rm -e "VNC_USER=foo" -e "VNC_PW=foobar" \ + -e "VNC_PORT=$VNC_PORT" \ kasmvnctester_barebones_${os}:$os_codename diff --git a/builder/test-vncserver b/builder/test-vncserver new file mode 100755 index 0000000..3263943 --- /dev/null +++ b/builder/test-vncserver @@ -0,0 +1,23 @@ +#!/bin/bash + +set -e + +default_os=ubuntu +default_os_codename=focal + +. ./builder/os_ver_cli.sh + +if [[ -n "$GITLAB_CI" ]]; then + package_dir="output/${os_codename}" +else + package_dir="builder/build/${os_codename}" +fi + +docker build --build-arg KASMVNC_PACKAGE_DIR="$package_dir" \ + -t kasmvnctester_${os}:$os_codename \ + -f builder/dockerfile.${os}_${os_codename}.specs.test . +docker run -v $(realpath ${PWD}):/src \ + --rm \ + -e KASMVNC_VERBOSE_LOGGING=$KASMVNC_VERBOSE_LOGGING \ + -e "VNC_USER=foo" -e "VNC_PW=foobar" \ + kasmvnctester_${os}:$os_codename diff --git a/centos/kasmvncserver.spec b/centos/kasmvncserver.spec index 6d49fdf..532cc85 100644 --- a/centos/kasmvncserver.spec +++ b/centos/kasmvncserver.spec @@ -7,7 +7,7 @@ License: GPLv2+ URL: https://github.com/kasmtech/KasmVNC BuildRequires: rsync -Requires: xorg-x11-xauth, xorg-x11-xkb-utils, xkeyboard-config, xorg-x11-server-utils, openssl, perl +Requires: xorg-x11-xauth, xorg-x11-xkb-utils, xkeyboard-config, xorg-x11-server-utils, openssl, perl, perl-Switch, perl-YAML-Tiny, perl-Hash-Merge-Simple, perl-Scalar-List-Utils, perl-List-MoreUtils, perl-Try-Tiny Conflicts: tigervnc-server, tigervnc-server-minimal %description @@ -31,6 +31,8 @@ create a by default secure, web based experience. Simplicity - KasmVNC aims at being simple to deploy and configure. +WARNING: this package requires EPEL. + %prep %install @@ -48,17 +50,28 @@ DESTDIR=$RPM_BUILD_ROOT DST_MAN=$DESTDIR/usr/share/man/man1 mkdir -p $DESTDIR/usr/bin $DESTDIR/usr/share/man/man1 \ - $DESTDIR/usr/share/doc/kasmvncserver + $DESTDIR/usr/share/doc/kasmvncserver $DESTDIR/usr/lib \ + $DESTDIR/usr/share/perl5 $DESTDIR/etc/kasmvnc + cp $SRC_BIN/Xvnc $DESTDIR/usr/bin; cp $SRC_BIN/vncserver $DESTDIR/usr/bin; +cp -a $SRC_BIN/KasmVNC $DESTDIR/usr/share/perl5/ cp $SRC_BIN/vncconfig $DESTDIR/usr/bin; cp $SRC_BIN/kasmvncpasswd $DESTDIR/usr/bin; cp $SRC_BIN/kasmxproxy $DESTDIR/usr/bin; +cp -r $SRC/lib/kasmvnc/ $DESTDIR/usr/lib/kasmvncserver cd $DESTDIR/usr/bin && ln -s kasmvncpasswd vncpasswd; cp -r $SRC/share/doc/kasmvnc*/* $DESTDIR/usr/share/doc/kasmvncserver/ rsync -r --exclude '.git*' --exclude po2js --exclude xgettext-html \ - --exclude www/utils/ --exclude .eslintrc \ + --exclude www/utils/ --exclude .eslintrc --exclude configure \ $SRC/share/kasmvnc $DESTDIR/usr/share + +sed -i -e 's!pem_certificate: .\+$!pem_certificate: /etc/pki/tls/private/kasmvnc.pem!' \ + $DESTDIR/usr/share/kasmvnc/kasmvnc_defaults.yaml +sed -i -e 's!pem_key: .\+$!pem_key: /etc/pki/tls/private/kasmvnc.pem!' \ + $DESTDIR/usr/share/kasmvnc/kasmvnc_defaults.yaml +sed -e 's/^\([^#]\)/# \1/' $DESTDIR/usr/share/kasmvnc/kasmvnc_defaults.yaml > \ + $DESTDIR/etc/kasmvnc/kasmvnc.yaml cp $SRC/man/man1/Xvnc.1 $DESTDIR/usr/share/man/man1/; cp $SRC/share/man/man1/vncserver.1 $DST_MAN; cp $SRC/share/man/man1/vncconfig.1 $DST_MAN; @@ -68,9 +81,13 @@ cd $DST_MAN && ln -s vncpasswd.1 kasmvncpasswd.1; %files +%config(noreplace) /etc/kasmvnc + /usr/bin/* +/usr/lib/kasmvncserver /usr/share/man/man1/* -/usr/share/kasmvnc/www +/usr/share/perl5/KasmVNC +/usr/share/kasmvnc %license /usr/share/doc/kasmvncserver/LICENSE.TXT %doc /usr/share/doc/kasmvncserver/README.md diff --git a/debian/Makefile.to_fakebuild_tar_package b/debian/Makefile.to_fakebuild_tar_package index 4cf3a73..c963323 100644 --- a/debian/Makefile.to_fakebuild_tar_package +++ b/debian/Makefile.to_fakebuild_tar_package @@ -11,16 +11,22 @@ install: unpack_tarball echo "TAR_DATA: $(TAR_DATA)" echo "installing files" mkdir -p $(DESTDIR)/usr/bin $(DESTDIR)/usr/share/man/man1 \ - $(DESTDIR)/usr/share/doc/kasmvncserver + $(DESTDIR)/usr/share/doc/kasmvncserver $(DESTDIR)/usr/lib \ + $(DESTDIR)/usr/share/perl5 \ + $(DESTDIR)/etc/kasmvnc cp $(SRC_BIN)/Xvnc $(DESTDIR)/usr/bin/Xkasmvnc cp $(SRC_BIN)/vncserver $(DESTDIR)/usr/bin/kasmvncserver + cp -r $(SRC_BIN)/KasmVNC $(DESTDIR)/usr/share/perl5/ cp $(SRC_BIN)/vncconfig $(DESTDIR)/usr/bin/kasmvncconfig cp $(SRC_BIN)/kasmvncpasswd $(DESTDIR)/usr/bin/ cp $(SRC_BIN)/kasmxproxy $(DESTDIR)/usr/bin/ + cp -r $(SRC)/lib/kasmvnc/ $(DESTDIR)/usr/lib/kasmvncserver cp -r $(SRC)/share/doc/kasmvnc*/* $(DESTDIR)/usr/share/doc/kasmvncserver/ rsync -r --exclude '.git*' --exclude po2js --exclude xgettext-html \ --exclude www/utils/ --exclude .eslintrc \ $(SRC)/share/kasmvnc $(DESTDIR)/usr/share + sed -e 's/^\([^#]\)/# \1/' $(SRC)/share/kasmvnc/kasmvnc_defaults.yaml > \ + $(DESTDIR)/etc/kasmvnc/kasmvnc.yaml cp $(SRC)/man/man1/Xvnc.1 $(DESTDIR)/usr/share/man/man1/Xkasmvnc.1 cp $(SRC)/share/man/man1/vncserver.1 $(DST_MAN)/kasmvncserver.1 cp $(SRC)/share/man/man1/kasmxproxy.1 $(DST_MAN)/kasmxproxy.1 diff --git a/debian/control b/debian/control index 64d63b8..fb80d7d 100644 --- a/debian/control +++ b/debian/control @@ -12,7 +12,9 @@ Homepage: https://github.com/kasmtech/KasmVNC Package: kasmvncserver Architecture: amd64 arm64 Depends: ${shlibs:Depends}, ${misc:Depends}, ${perl:Depends}, ssl-cert, xauth, - x11-xkb-utils, xkb-data, procps + x11-xkb-utils, xkb-data, procps, libswitch-perl, libyaml-tiny-perl, + libhash-merge-simple-perl, libscalar-list-utils-perl, liblist-moreutils-perl, + libtry-tiny-perl Provides: vnc-server Description: VNC server accessible from a web browser VNC stands for Virtual Network Computing. It is, in essence, a remote diff --git a/doc/YAML config FAQ.md b/doc/YAML config FAQ.md new file mode 100644 index 0000000..653ce5a --- /dev/null +++ b/doc/YAML config FAQ.md @@ -0,0 +1,11 @@ +### logging.log_dest + +Log to the instance's log file in `~/.vnc`, or syslog. You can find the right +log file by the display number. For example, in `9b1d402e204c:1.log`, `:1` is +the display number. When you start a new instance with `vncserver`, it prints +the display number it uses. + +### logging.level + +For the initial setup, it's a good idea to use 100. With 100, you're able to see +login attempts and reasons they fail. diff --git a/doc/YAML config.md b/doc/YAML config.md new file mode 100644 index 0000000..d74b08a --- /dev/null +++ b/doc/YAML config.md @@ -0,0 +1,763 @@ +### desktop.resolution.width, desktop.resolution.height +Set a fixed desktop resolution. Set `desktop.allow_resize` to `false` for +this to work. + +Unit: pixels +
+Default: 1024x768 + +### desktop.allow_resize +If turned on, resizing browser window changes desktop resolution to fit the +window. + +Set to `false` to have a fixed resolution (you need to set +[desktop.resolution.width and +desktop.resolution.heigh](#desktop.resolution.width%2C-desktop.resolution.height)). + +Unit: boolean +
+Default: `true` + +### desktop.pixel_depth + +Pixel depth in bits. Possible values are 16, 24, 32. These values are fail-safe. +You can specify any bit value via command-line. + +Unit: bits +
+Default: `24` + +### network.protocol +With KasmVNC, you can use a web browser client (set value to `http`) or a +traditional VNC client (set value to `vnc`). + +If you use `vnc` protocol, you're on your own. Passing `Xvnc` options via +command-line is required in this case. Most config settings are ignored. + +Unit: `http`, `vnc` +
+Default: `http` + +### network.interface +IP address or host name to listen on. Set to `0.0.0.0` to listen on all network +interfaces. + +Unit: ip address or host name +
+Default: `0.0.0.0` + +### network.websocket_port + +Listen for websocket connections on this port. `auto` translates to 8443 + X +display number. + +Unit: integer +
+Default: `auto` + +### network.use_ipv4 + +Use IPv4 for incoming and outgoing connections. + +Unit: boolean +
+Default: `true` + +### network.use_ipv6 +Use IPv6 for incoming and outgoing connections. + +Unit: boolean +
+Default: `true` + +### network.udp.public_ip + +KasmVNC server needs to know its IP from the perspective of the client. With `auto`, +it finds that out automatically (by using a public STUN server). + +If the server and client are on the same private network, and not connected +to internet, you need to specify a private IP address of the server, accessible +to the client. + +Unit: IP address, `auto`. +
+Default: `auto` + +### network.udp.port + +Set UDP port to use. With `auto`, +value is inherited from [network.websocket_port](#network.websocket_port). + +Unit: `auto`, integer +
+Default: `auto` + +### network.ssl.pem_certificate + +SSL pem certificate to use for websocket connections. + +Unit: path +
+Default: standard snake oil certificate on Debian-based distros. Auto-generated +on Fedora-based distros. + +### network.ssl.pem_key + +SSL pem key to use for websocket connections. If you have private and public +keys in one file, using +[network.ssl.pem_certificate](#network.ssl.pem_certificate) is enough to set +both. + +Unit: path +
+Default: standard snake oil certificate on Debian-based distros. Auto-generated +on Fedora-based distros. + +### network.ssl.require_ssl + +Require SSL for websocket connections. + +Unit: boolean +
+Default: `true` + +### user_session.session_type + +* `shared` - allow multiple sessions for a single desktop. +* `exclusive` - only one user session can work with the desktop. + +If set, overrides client settings. + +When combined with +[concurrent_connections_prompt](#user_session.concurrent_connections_prompt) , +the user is asked to let the incoming connection in. + +Unit: `shared`, `exclusive` +
+Default: not set, client settings are used + +## user_session.new_session_disconnects_existing_exclusive_session + +Only applies if [session_type](#user_session.session_type) is set to +`exclusive`. The user working with the desktop is kicked out by a new session. +New session takes over the desktop. + +When combined with +[concurrent_connections_prompt](#user_session.concurrent_connections_prompt), +the user is asked to confirm session takeover. + +Unit: boolean +
+Default: `false` + +### user_session.concurrent_connections_prompt + +Prompts the user of the desktop to explicitly accept or reject incoming +connections. + +The vncconfig(1) program must be running on the desktop. + +Unit: boolean +
+Default: `false` + +### user_session.concurrent_connections_prompt_timeout + +Active only if +[concurrent_connections_prompt](#user_session.concurrent_connections_prompt), +is turned on. + +Number of seconds to show the Accept Connection dialog before rejecting the +connection. + +Unit: seconds +
+Default: `10` + +### user_session.idle_timeout +⚠️ This setting only applies, when +[allow_client_to_override_kasm_server_settings](#runtime_configuration.allow_client_to_override_kasm_server_settings) +is turned off. + +The number of seconds after which an idle session is dropped. + +Unit: seconds +
+Default: `never` + +### keyboard.remap_keys + +Set up 1-to-1 character replacement. For example, to exchange the " and @ +symbols you would specify the following: `0x22->0x40`. Similar to tr(1). + +Unit: hex numbers in the format -> +
+Default: not set + +### keyboard.ignore_numlock +-- TODO + +Key affected by NumLock often require a fake Shift to be inserted in order for +the correct symbol to be generated. Turning on this setting avoids these extra +fake Shift events but may result in a slightly different symbol (for example, a +Return instead of a keypad Enter). + +Unit: boolean +
+Default: `false` + +### keyboard.raw_keyboard + +Send keyboard events straight through and avoid mapping them to the current +keyboard layout. This effectively makes the keyboard behave according to the +layout configured on the server instead of the layout configured on the client. + +Unit: boolean +
+Default: `false` + +### pointer.enabled + +Allows clicks from mice, trackpads, etc. + +Unit: boolean +
+Default: `true` + +### runtime_configuration.allow_client_to_override_kasm_server_settings + +KasmVNC exposes a few settings to the client the standard VNC does not. You can +let the client override these settings or forbid overriding. + +Unit: boolean +
+Default: `true` + +### runtime_configuration.allow_override_standard_vnc_server_settings + +If turned on, VNC settings defined in +[runtime_configuration.allow_override_list](#runtime_configuration.allow_override_list) +can be changed at runtime. + +Unit: boolean +
+Default: `true` + +### runtime_configuration.allow_override_list + +You can modify listed settings at runtime. Settings can be modified, for +example, using vncconfig(1) program from inside a running session. + +The list must contain absolute config keys like +[pointer.enable](#pointer.enable). To actually change +[pointer.enable](#pointer.enable), you need to pass the corresponding +command-line option `AcceptPointerEvents` to vncconfig(1). + +Unit: list of absolute config keys +
+Default: +``` + pointer.enabled + data_loss_prevention.clipboard.server_to_client.enabled + data_loss_prevention.clipboard.client_to_server.enabled + data_loss_prevention.clipboard.server_to_client.primary_clipboard_enabled +``` + +### logging.log_writer_name +Log all subsystems of KasmVNC by setting to `all`. To log a specific subsystem, +consult source code for `LogWriter` instances. For example, `static LogWriter +vlog("STrayIcon");`. `STrayIcon` is the log writer name here. + +Unit: `all`, \ +
+Default: `all` + +### logging.log_dest + +Log to the instance's log file in `~/.vnc`, or syslog. + +Unit: `logfile`, `syslog` +
+Default: `logfile` + +### logging.level + +Logging verbosity level. Can be in the 0.\.100 range. 100 meaning most verbose +output, 0 meaning most concise. + +Unit: integer in the 0.\.100 range +
+Default: `30` + +### security.brute_force_protection.blacklist_threshold + +The number of unauthenticated connection attempts allowed from any individual +host before that host is black-listed. + +Unit: integer +
+Default: `5` + +### security.brute_force_protection.blacklist_timeout + +The initial timeout applied when a host is first black-listed by failing to +authenticate +[blacklist_threshold](#security.brute_force_protection.blacklist_threshold) +times. The host cannot re-attempt a connection until the timeout expires. + +Unit: seconds +
+Default: `10` + +### data_loss_prevention.visible_region.top, data_loss_prevention.visible_region.left, data_loss_prevention.visible_region.right, data_loss_prevention.visible_region.bottom + +The regions feature allows you to select a region of the screen to render to the +user. Concealed portions of the screen are blacked out. + +#### Absolute coordinates + +Select a region using pixels: +``` +top: 10 +left: 10 +right: 40 +bottom: 40 +``` + +#### Offset coordinates + +Use negative numbers to offset from the screen boundary. For `left` and `top`, +this means 0 plus the provided number. In the below example that would be 10. +For `right` and `bottom`, that means the maximum horizontal or vertical +resolution minus the provided number. + +If the resolution was 1080x720 in the below example that would equate to a +`right` of 1070 and `bottom` of 710. Therefore, the example below would be +translated to 10, 10, 1070, 710. Using offset coordinates has an advantage of +scaling with screen size changes versus using absolute values. + +Select a region using pixel offsets: +``` +top: -10 +left: -10 +right: -10 +bottom: -10 +``` + +You can combine absolute values with offset values, such as the following +example: + +``` +top: 50 +left: 10 +right: -10 +bottom: -10 +``` + +#### Percentages + +Regions does support percent values, which are evaluated as a border that is a +percent of the total width and height respectively. Regions does not support +mixing percent values and absolute or offset values. + +For example: + +``` +top: 10% +left: 10% +right: 20% +bottom: 20% +``` + +Unit: pixels, offset pixels, percentages +
+Default: not set + +### data_loss_prevention.visible_region.concealed_region.allow_click_down +Allow mouse button down events within the concealed regions, by default they are +blocked. + +Unit: boolean +
+Default: `false` + +### data_loss_prevention.visible_region.concealed_region.allow_click_release + +Allow mouse button releases within the concealed regions, by default they are +blocked until the cursor returns to the visible region. + +Unit: boolean +
+Default: `false` + +### data_loss_prevention.clipboard.delay_between_operations + +This many milliseconds must pass between clipboard actions. + +Unit: milliseconds, `none` +
+Default: `none` + +### data_loss_prevention.clipboard.allow_mimetypes + +Allowed binary clipboard mimetypes. + +Unit: mimetype +
+Default: +``` +chromium/x-web-custom-data +text/html +image/png` +``` + +### data_loss_prevention.clipboard.server_to_client.enabled + +⚠️ This setting only applies, when +[allow_client_to_override_kasm_server_settings](#runtime_configuration.allow_client_to_override_kasm_server_settings) +is turned off. + +Whether to send desktop clipboard changes to clients. + +Unit: boolean +
+Default: `false` + +### data_loss_prevention.clipboard.server_to_client.size + +Limit clipboard bytes to send to clients in one transaction. + +Unit: number of bytes, `unlimited` +
+Default: `unlimited` + +### data_loss_prevention.clipboard.server_to_client.primary_clipboard_enabled + +Send the primary selection to the client. Meaning, mouse-selected text is copied +to clipboard. Only works in Chromium-based browsers. + +Unit: boolean +
+Default: `false` + +### data_loss_prevention.clipboard.client_to_server.enabled +⚠️ This setting only applies, when +[allow_client_to_override_kasm_server_settings](#runtime_configuration.allow_client_to_override_kasm_server_settings) +is turned off. + +Accept clipboard updates from clients. + +Unit: boolean +
+Default: `false` + +### data_loss_prevention.clipboard.client_to_server.size + +Limit clipboard bytes to receive from clients in one transaction. + +Unit: number of bytes, `unlimited` +
+Default: `unlimited` + +### data_loss_prevention.keyboard.enabled + +Accept key press and release events from clients. + +Unit: boolean +
+Default: `true` + +### data_loss_prevention.keyboard.rate_limit + +Reject keyboard presses over this many per second. + +Unit: integer, `unlimited` +
+Default: `unlimited` + +### data_loss_prevention.logging.level + +Log clipboard and keyboard actions. `info` logs just clipboard direction and +size, `verbose` adds the contents for both. + +Unit: `off`, `info`, `verbose` +
+Default: `off` + +### encoding.max_frame_rate +⚠️ This setting only applies, when +[allow_client_to_override_kasm_server_settings](#runtime_configuration.allow_client_to_override_kasm_server_settings) +is turned off. + +The maximum number of updates per second sent to clients. If the screen +updates any faster then those changes are aggregated and sent in a single +update to the client. Note that this only controls the maximum rate and a client +may get a lower rate when resources are limited. + +Unit: integer +
+Default: `60` + +### encoding.full_frame_updates + +KasmVNC cuts the screen up into smaller rectangles and only sends sections of +the screen that change. When using UDP, some rectangles can be dropped, so this +option forces a full screen update every X frames. + +Unit: `none`, positive integer +
+Default: `none` + +### encoding.rect_encoding_mode.min_quality +⚠️ This setting only applies, when +[allow_client_to_override_kasm_server_settings](#runtime_configuration.allow_client_to_override_kasm_server_settings) +is turned off. + +The minimum quality setting for JPEG/WEBP encoding. Rendering automatically +degrades JPEG quality when there is a lot of motion in a particular block. This +setting controls the minimum quality to use when there is a high degree of +change. The accepted values are 0.\.9 where 0 is low and 9 is high. + +Unit: integer in the 0.\.9 range +
+Default: `7` + +### encoding.rect_encoding_mode.max_quality +⚠️ This setting only applies, when +[allow_client_to_override_kasm_server_settings](#runtime_configuration.allow_client_to_override_kasm_server_settings) +is turned off. + +The maximum quality setting for JPEG/WEBP encoding. Rendering automatically +degrades JPEG quality when there is a lot of motion in a particular block. This +setting controls the maximum quality to use when there is no or little motion. +The accepted values are 0.\.9 where 0 is low and 9 is high. + +Unit: integer in the 0.\.9 range +
+Default: `8` + +### encoding.rect_encoding_mode.consider_lossless_quality +⚠️ This setting only applies, when +[allow_client_to_override_kasm_server_settings](#runtime_configuration.allow_client_to_override_kasm_server_settings) +is turned off. + +KasmVNC dynamically adjusts the JPEG/WEBP quality based on how much change +there is in a particular section. Every x number of frames it sends a +'lossless' update. That way, if you are scrolling, the text blurs a little while +you scroll but as soon as you stop, it should clear up on the next lossless +update. `consider_lossless_quality` means "treat this quality as lossless." +Assuming the min quality of 3, the max of 7 and treat lossless of 5. KasmVNC +would constantly adjust the quality of images sent anywhere from 3 to 7 +depending on the rate of change. If the last rectangle sent was at 5 then it +would not send a lossless update for that part of the screen. + +Unit: integer in the 0.\.10 range +
+Default: `10` + +### encoding.rect_encoding_mode.rectangle_compress_threads + +Use this many threads to compress rectangles in parallel. `auto` sets threads to +match the core count. + +Unit: integer, `auto`. +
+Default: `auto` + +### encoding.video_encoding_mode.jpeg_quality +⚠️ This setting only applies, when +[allow_client_to_override_kasm_server_settings](#runtime_configuration.allow_client_to_override_kasm_server_settings) +is turned off. + +The JPEG quality to use when in video mode. The accepted values are 0.\.9 where +0 is low and 9 is high. A value of -1 keeps the quality level used in normal +mode. + +Unit: integer in the -1..9 range +
+Default: `-1` + +### encoding.video_encoding_mode.webp_quality +⚠️ This setting only applies, when +[allow_client_to_override_kasm_server_settings](#runtime_configuration.allow_client_to_override_kasm_server_settings) +is turned off. + +The WEBP quality to use when in video mode. The accepted values are 0.\.9 where 0 +is low and 9 is high. A value of -1 keeps the quality level used in normal mode. + +Unit: integer in the -1..9 range +
+Default: `-1` + +### encoding.video_encoding_mode.max_resolution.width, encoding.video_encoding_mode.max_resolution.height +⚠️ This setting only applies, when +[allow_client_to_override_kasm_server_settings](#runtime_configuration.allow_client_to_override_kasm_server_settings) +is turned off. + +When in Video Mode, downscale the screen to this maximum size. Keeps aspect +ratio with client's actual resolution. + +Unit: pixels +
+Default: 1920x1080 + +### encoding.video_encoding_mode.enter_video_encoding_mode.time_threshold +⚠️ This setting only applies, when +[allow_client_to_override_kasm_server_settings](#runtime_configuration.allow_client_to_override_kasm_server_settings) +is turned off. + +Number of seconds that a high rate of change most occur before switching to +video mode. Setting to 0 forces Video Mode at all times. + +Unit: seconds +
+Default: `5` + +### encoding.video_encoding_mode.enter_video_encoding_mode.area_threshold +⚠️ This setting only applies, when +[allow_client_to_override_kasm_server_settings](#runtime_configuration.allow_client_to_override_kasm_server_settings) +is turned off. + +The percent of the screen that must be seeing high rates of change to meet the +threshold of Video Mode. This percentage of the screen must see rapid changes +for the amount of time specified by [encoding.video_encoding_mode.enter_video_encoding_mode.time_threshold](#encoding.video_encoding_mode.enter_video_encoding_mode.time_threshold). + +Unit: percentage of screen +
+Default: `45%` + +### encoding.video_encoding_mode.exit_video_encoding_mode.time_threshold +⚠️ This setting only applies, when +[allow_client_to_override_kasm_server_settings](#runtime_configuration.allow_client_to_override_kasm_server_settings) +is turned off. + +When in Video Mode, high rates of change must subside for this many seconds +before dropping out of video mode. + +Unit: seconds +
+Default: `3` + +### encoding.video_encoding_mode.logging.level + +Print the detected video area % value. This is useful when trying to tune your +settings for your particular use case. + +Unit: `info`, `off` +
+Default: `off` + +### encoding.video_encoding_mode.scaling_algorithm +⚠️ This setting only applies, when +[allow_client_to_override_kasm_server_settings](#runtime_configuration.allow_client_to_override_kasm_server_settings) +is turned off. + +The scaling method to use in video mode. + +Unit: `nearest`, `bilinear`, `progressive_bilinear` +
+Default: `progressive_bilinear` + +### encoding.compare_framebuffer + +Perform pixel comparison on frame buffer to reduce unnecessary updates. + +Unit: `off`, `always`, `auto` +
+Default: `auto` + +### encoding.zrle_zlib_level +⚠️ This setting only applies, when +[allow_client_to_override_kasm_server_settings](#runtime_configuration.allow_client_to_override_kasm_server_settings) +is turned off. + +Zlib compression level for ZRLE encoding (it does not affect Tight encoding). +Acceptable values are between 0.\.9. Set to `auto` to use the standard +compression level provided by the zlib(3) compression library. + +Unit: integer in the 0.\.9 range, `auto` +
+Default: `auto` + +### encoding.hextile_improved_compression + +Use improved compression algorithm for Hextile encoding which achieves better +compression ratios by the cost of using slightly more CPU time. + +Unit: boolean +
+Default: `true` + +### server.advanced.x_font_path + +Specify X font path. + +Unit: path, `auto` +
+Default: `auto` + +### server.advanced.httpd_directory + +Run a mini-HTTP server which serves files from the given directory. Normally the +directory contains the kasmweb client. + +Unit: path +
+Default: `/usr/share/kasmvnc/www` + +### server.advanced.kasm_password_file + +Password file for Basic authentication, created with the `kasmvncpasswd(1)` +utility. + +Unit: path +
+Default: `${HOME}/.kasmpasswd` + +### server.advanced.x_authority_file + +Set to X authority file. X authority file stores credentials in cookies used by +`xauth(1)` for authentication of X sessions. + +`auto` means using file defined in `$XAUTHORITY` environment variable. If the +variable isn't defined, fallback to using `${HOME}/.Xauthority` file. + +Unit: path +
+Default: `auto` + +### server.auto_shutdown.no_user_session_timeout + +Terminate KasmVNC when no client has been connected for this many +seconds. + +Unit: seconds, `never` +
+Default: `never` + +### server.auto_shutdown.active_user_session_timeout + +Terminate KasmVNC when a client has been connected for this many +seconds. + +Unit: seconds, `never` +
+Default: `never` + +### server.auto_shutdown.inactive_user_session_timeout + +Terminate KasmVNC after this many seconds of user inactivity. + +Unit: seconds, `never` +
+Default: `never` + +### command_line.prompt + +Guide the user (by prompting), to ensure that KasmVNC is usable. For example, +prompt to create a KasmVNC user if no users exist or no users can control the +desktop. + +Unit: boolean +
+Default: `true` diff --git a/opensuse/kasmvncserver.spec b/opensuse/kasmvncserver.spec index 56e457b..901bd0a 100644 --- a/opensuse/kasmvncserver.spec +++ b/opensuse/kasmvncserver.spec @@ -7,7 +7,7 @@ License: GPLv2+ URL: https://github.com/kasmtech/KasmVNC BuildRequires: rsync -Requires: xauth, libxkbcommon-x11-0, xkeyboard-config, x11-tools, openssl, perl, libpixman-1-0, libjpeg8, libgomp1, libXfont2-2, libXdmcp6, libglvnd, xkbcomp +Requires: xauth, hostname, libxkbcommon-x11-0, xkeyboard-config, x11-tools, openssl, perl, libpixman-1-0, libjpeg8, libgomp1, libXfont2-2, libXdmcp6, libglvnd, xkbcomp, perl-Switch, perl-YAML-Tiny, perl-Hash-Merge-Simple, perl-Scalar-List-Utils, perl-List-MoreUtils, perl-Try-Tiny Conflicts: tigervnc, tigervnc-x11vnc %description @@ -48,17 +48,27 @@ DESTDIR=$RPM_BUILD_ROOT DST_MAN=$DESTDIR/usr/share/man/man1 mkdir -p $DESTDIR/usr/bin $DESTDIR/usr/share/man/man1 \ - $DESTDIR/usr/share/doc/kasmvncserver + $DESTDIR/usr/share/doc/kasmvncserver $DESTDIR/usr/lib \ + $DESTDIR/%perl_vendorlib $DESTDIR/etc/kasmvnc cp $SRC_BIN/Xvnc $DESTDIR/usr/bin; cp $SRC_BIN/vncserver $DESTDIR/usr/bin; +cp -a $SRC_BIN/KasmVNC $DESTDIR/%perl_vendorlib cp $SRC_BIN/vncconfig $DESTDIR/usr/bin; cp $SRC_BIN/kasmvncpasswd $DESTDIR/usr/bin; cp $SRC_BIN/kasmxproxy $DESTDIR/usr/bin; +cp -r $SRC/lib/kasmvnc/ $DESTDIR/usr/lib/kasmvncserver cd $DESTDIR/usr/bin && ln -s kasmvncpasswd vncpasswd; cp -r $SRC/share/doc/kasmvnc*/* $DESTDIR/usr/share/doc/kasmvncserver/ rsync -r --exclude '.git*' --exclude po2js --exclude xgettext-html \ - --exclude www/utils/ --exclude .eslintrc \ + --exclude www/utils/ --exclude .eslintrc --exclude configure \ $SRC/share/kasmvnc $DESTDIR/usr/share + +sed -i -e 's!pem_certificate: .\+$!pem_certificate: /etc/pki/tls/private/kasmvnc.pem!' \ + $DESTDIR/usr/share/kasmvnc/kasmvnc_defaults.yaml +sed -i -e 's!pem_key: .\+$!pem_key: /etc/pki/tls/private/kasmvnc.pem!' \ + $DESTDIR/usr/share/kasmvnc/kasmvnc_defaults.yaml +sed -e 's/^\([^#]\)/# \1/' $DESTDIR/usr/share/kasmvnc/kasmvnc_defaults.yaml > \ + $DESTDIR/etc/kasmvnc/kasmvnc.yaml cp $SRC/man/man1/Xvnc.1 $DESTDIR/usr/share/man/man1/; cp $SRC/share/man/man1/vncserver.1 $DST_MAN; cp $SRC/share/man/man1/vncconfig.1 $DST_MAN; @@ -68,9 +78,13 @@ cd $DST_MAN && ln -s vncpasswd.1 kasmvncpasswd.1; %files +%config(noreplace) /etc/kasmvnc + /usr/bin/* +/usr/lib/kasmvncserver /usr/share/man/man1/* -/usr/share/kasmvnc/www +%perl_vendorlib/KasmVNC +/usr/share/kasmvnc %license /usr/share/doc/kasmvncserver/LICENSE.TXT %doc /usr/share/doc/kasmvncserver/README.md diff --git a/oracle/kasmvncserver.spec b/oracle/kasmvncserver.spec index 3ff4f37..9226600 100644 --- a/oracle/kasmvncserver.spec +++ b/oracle/kasmvncserver.spec @@ -7,7 +7,7 @@ License: GPLv2+ URL: https://github.com/kasmtech/KasmVNC BuildRequires: rsync -Requires: xorg-x11-xauth, xorg-x11-xkb-utils, xkeyboard-config, xorg-x11-server-utils, openssl, perl +Requires: xorg-x11-xauth, xorg-x11-xkb-utils, xkeyboard-config, xorg-x11-server-utils, openssl, perl, perl-Switch, perl-YAML-Tiny, perl-Hash-Merge-Simple, perl-Scalar-List-Utils, perl-List-MoreUtils, perl-Try-Tiny Conflicts: tigervnc-server, tigervnc-server-minimal %description @@ -31,6 +31,8 @@ create a by default secure, web based experience. Simplicity - KasmVNC aims at being simple to deploy and configure. +WARNING: this package requires EPEL and CodeReady builder. + %prep %install @@ -48,17 +50,27 @@ DESTDIR=$RPM_BUILD_ROOT DST_MAN=$DESTDIR/usr/share/man/man1 mkdir -p $DESTDIR/usr/bin $DESTDIR/usr/share/man/man1 \ - $DESTDIR/usr/share/doc/kasmvncserver + $DESTDIR/usr/share/doc/kasmvncserver $DESTDIR/usr/lib \ + $DESTDIR/usr/share/perl5 $DESTDIR/etc/kasmvnc cp $SRC_BIN/Xvnc $DESTDIR/usr/bin; cp $SRC_BIN/vncserver $DESTDIR/usr/bin; +cp -a $SRC_BIN/KasmVNC $DESTDIR/usr/share/perl5 cp $SRC_BIN/vncconfig $DESTDIR/usr/bin; cp $SRC_BIN/kasmvncpasswd $DESTDIR/usr/bin; cp $SRC_BIN/kasmxproxy $DESTDIR/usr/bin; +cp -r $SRC/lib/kasmvnc/ $DESTDIR/usr/lib/kasmvncserver cd $DESTDIR/usr/bin && ln -s kasmvncpasswd vncpasswd; cp -r $SRC/share/doc/kasmvnc*/* $DESTDIR/usr/share/doc/kasmvncserver/ rsync -r --exclude '.git*' --exclude po2js --exclude xgettext-html \ - --exclude www/utils/ --exclude .eslintrc \ + --exclude www/utils/ --exclude .eslintrc --exclude configure \ $SRC/share/kasmvnc $DESTDIR/usr/share + +sed -i -e 's!pem_certificate: .\+$!pem_certificate: /etc/pki/tls/private/kasmvnc.pem!' \ + $DESTDIR/usr/share/kasmvnc/kasmvnc_defaults.yaml +sed -i -e 's!pem_key: .\+$!pem_key: /etc/pki/tls/private/kasmvnc.pem!' \ + $DESTDIR/usr/share/kasmvnc/kasmvnc_defaults.yaml +sed -e 's/^\([^#]\)/# \1/' $DESTDIR/usr/share/kasmvnc/kasmvnc_defaults.yaml > \ + $DESTDIR/etc/kasmvnc/kasmvnc.yaml cp $SRC/man/man1/Xvnc.1 $DESTDIR/usr/share/man/man1/; cp $SRC/share/man/man1/vncserver.1 $DST_MAN; cp $SRC/share/man/man1/vncconfig.1 $DST_MAN; @@ -68,9 +80,13 @@ cd $DST_MAN && ln -s vncpasswd.1 kasmvncpasswd.1; %files +%config(noreplace) /etc/kasmvnc + /usr/bin/* +/usr/lib/kasmvncserver /usr/share/man/man1/* -/usr/share/kasmvnc/www +/usr/share/perl5/KasmVNC +/usr/share/kasmvnc %license /usr/share/doc/kasmvncserver/LICENSE.TXT %doc /usr/share/doc/kasmvncserver/README.md diff --git a/release/maketarball.in b/release/maketarball.in index 5b382a9..79be58c 100644 --- a/release/maketarball.in +++ b/release/maketarball.in @@ -51,7 +51,15 @@ if [ $SERVER = 1 ]; then install -m 644 ./xorg.build/man/man1/Xserver.1 $OUTDIR/man/man1/Xserver.1 mkdir -p $OUTDIR/lib/dri/ install -m 755 ./xorg.build/lib/dri/swrast_dri.so $OUTDIR/lib/dri/ - mkdir -p $OUTDIR/share/kasmvnc + mkdir -p $OUTDIR/lib/kasmvnc + install -m 755 $SRCDIR/builder/startup/deb/select-de.sh $OUTDIR/lib/kasmvnc + mkdir -p $OUTDIR/share/kasmvnc + + cp -r $SRCDIR/unix/KasmVNC/ $OUTDIR/bin/ + cp $SRCDIR/unix/kasmvnc_defaults.yaml $OUTDIR/share/kasmvnc/ + mkdir -p $OUTDIR/etc/kasmvnc + cp $SRCDIR/unix/kasmvnc.yaml $OUTDIR/etc/kasmvnc + cp -r $SRCDIR/builder/www $OUTDIR/share/kasmvnc/www fi diff --git a/run-specs b/run-specs new file mode 100755 index 0000000..5096345 --- /dev/null +++ b/run-specs @@ -0,0 +1,36 @@ +#!/bin/bash + +set -e +export PYTHONPATH=${PWD}/spec + +RUN_CMD="pipenv run mamba" + +specs=() +for arg in "$@"; do + if [[ "$arg" = "-d" ]]; then + export KASMVNC_SPEC_DEBUG_OUTPUT=1 + continue; + fi + if [[ "$arg" = "-v" ]]; then + verbose=1 + continue + fi + + specs+=("$arg") +done +set -- "${specs[@]}" + +if [[ "$1" = "-h" || "$1" = "--help" ]]; then + echo >&2 "Usage: $(basename "$0") [-d] [-v] " + exit 0 +fi + +if [[ -n "$verbose" ]]; then + RUN_CMD="$RUN_CMD --format=documentation" +fi + +if [[ -n "$1" ]]; then + $RUN_CMD "$@" +else + $RUN_CMD spec/*_spec.py +fi diff --git a/spec/fixtures/defaults_config.yaml b/spec/fixtures/defaults_config.yaml new file mode 100644 index 0000000..55f14fd --- /dev/null +++ b/spec/fixtures/defaults_config.yaml @@ -0,0 +1,84 @@ +desktop: + resolution: + width: 1024 + height: 768 + allow_resize: true + pixel_depth: 24 + +network: + protocol: http + interface: 0.0.0.0 + websocket_port: auto + use_ipv4: true + use_ipv6: true + ssl: + pem_certificate: /etc/ssl/certs/ssl-cert-snakeoil.pem + pem_key: /etc/ssl/private/ssl-cert-snakeoil.key + require_ssl: true + +security: + brute_force_protection: + blacklist_threshold: 5 + blacklist_timeout: 10 + +data_loss_prevention: + visible_region: + # top: 10 + # left: 10 + # right: 40 + # bottom: 40 + concealed_region: + allow_click_down: false + allow_click_release: false + clipboard: + delay_between_operations: none + server_to_client: + enabled: true + size: 10000 + primary_clipboard_enabled: false + client_to_server: + enabled: true + size: 10000 + keyboard: + enabled: true + rate_limit: unlimited + logging: + level: off + +encoding: + max_frame_rate: 60 + rect_encoding_mode: + min_quality: 7 + max_quality: 8 + consider_lossless_quality: 10 + rectangle_compress_threads: auto + + video_encoding_mode: + jpeg_quality: -1 + webp_quality: -1 + max_resolution: + width: 1920 + height: 1080 + enter_video_encoding_mode: + time_threshold: 5 + area_threshold: 45% + exit_video_encoding_mode: + time_threshold: 3 + logging: + level: off + scaling_algorithm: progressive_bilinear + + compare_framebuffer: auto + zrle_zlib_level: auto + hextile_improved_compression: true + +server: + advanced: + x_font_path: auto + httpd_directory: /usr/share/kasmvnc/www + kasm_password_file: ~/.kasmpasswd + x_authority_file: auto + auto_shutdown: + no_user_session_timeout: never + active_user_session_timeout: never + inactive_user_session_timeout: never diff --git a/spec/fixtures/global_config.yaml b/spec/fixtures/global_config.yaml new file mode 100644 index 0000000..7ce450a --- /dev/null +++ b/spec/fixtures/global_config.yaml @@ -0,0 +1,3 @@ +security: + brute_force_protection: + blacklist_threshold: 6 diff --git a/spec/fixtures/user_config.yaml b/spec/fixtures/user_config.yaml new file mode 100644 index 0000000..6dadf25 --- /dev/null +++ b/spec/fixtures/user_config.yaml @@ -0,0 +1,4 @@ +security: + brute_force_protection: + blacklist_threshold: 7 + blacklist_timeout: 12 diff --git a/spec/helper/__init__.py b/spec/helper/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/spec/helper/spec_helper.py b/spec/helper/spec_helper.py new file mode 100644 index 0000000..cb12c9f --- /dev/null +++ b/spec/helper/spec_helper.py @@ -0,0 +1,93 @@ +import os +import sys +import shutil +import subprocess +import pexpect +from path import Path +from expects import expect, equal + +vncserver_cmd = 'vncserver :1' +running_xvnc = False +debug_output = False +config_dir = "spec/tmp" +config_filename = os.path.join(config_dir, "config.yaml") + +if os.getenv('KASMVNC_SPEC_DEBUG_OUTPUT'): + debug_output = True + + +def write_config(config_text): + os.makedirs(config_dir, exist_ok=True) + + with open(config_filename, "w") as f: + f.write(config_text) + + +def clean_env(): + clean_kasm_users() + + home_dir = os.environ['HOME'] + vnc_dir = os.path.join(home_dir, ".vnc") + Path(vnc_dir).rmtree(ignore_errors=True) + + +def clean_kasm_users(): + home_dir = os.environ['HOME'] + password_file = os.path.join(home_dir, ".kasmpasswd") + Path(password_file).remove_p() + + +def start_xvnc_pexpect(extra_args="", **kwargs): + global running_xvnc + + # ":;" is a hack. Without it, Xvnc doesn't run. No idea what happens, but + # when I run top, Xvnc just isn't there. I suspect a race. + child = pexpect.spawn('/bin/bash', + ['-ic', f':;{vncserver_cmd} {extra_args}'], + timeout=5, encoding='utf-8', **kwargs) + if debug_output: + child.logfile_read = sys.stderr + + running_xvnc = True + + return child + + +def start_xvnc(extra_args="", **kwargs): + global running_xvnc + completed_process = run_cmd(f'{vncserver_cmd} {extra_args}', + print_stderr=False, **kwargs) + if completed_process.returncode == 0: + running_xvnc = True + + return completed_process + + +def run_cmd(cmd, print_stderr=True, **kwargs): + completed_process = subprocess.run(cmd, shell=True, text=True, + capture_output=True, + executable='/bin/bash', **kwargs) + if debug_output: + if len(completed_process.stderr) > 0: + print(completed_process.stderr) + if len(completed_process.stdout) > 0: + print(completed_process.stdout) + elif print_stderr: + if len(completed_process.stderr) > 0: + print(completed_process.stderr) + + return completed_process + + +def add_kasmvnc_user_docker(): + completed_process = run_cmd('echo -e "password\\npassword\\n" | vncpasswd -u docker -w') + expect(completed_process.returncode).to(equal(0)) + + +def kill_xvnc(): + global running_xvnc + if not running_xvnc: + return + + run_cmd('vncserver -kill :1', print_stderr=False) + running_xvnc = False diff --git a/spec/kasmvnc.yaml b/spec/kasmvnc.yaml new file mode 100644 index 0000000..365fcbb --- /dev/null +++ b/spec/kasmvnc.yaml @@ -0,0 +1,42 @@ +desktop: + resolution: + width: 1024 + height: 768 + allow_resize: true + pixel_depth: 24 + +security: + brute_force_protection: + blacklist_threshold: 1 + blacklist_timeout: 10 + +logging: + log_writer_name: all + log_dest: logfile + # 0 - silent(?), 100 - most verbose + level: 100 + +data_loss_prevention: + visible_region: + # top: 10 + # left: 10 + # right: 40 + # bottom: 40 + concealed_region: + allow_click_down: false + allow_click_release: false + clipboard: + delay_between_operations: none + # Cut buffers and CLIPBOARD selection. + server_to_client: + enabled: true + size: unlimited + primary_clipboard_enabled: false + +keyboard: + remap_keys: + - 0x22->0x40 + - 0x24->0x40 + +command_line: + prompt: true diff --git a/spec/vncserver_perl_warnings_spec.py b/spec/vncserver_perl_warnings_spec.py new file mode 100644 index 0000000..82a17c1 --- /dev/null +++ b/spec/vncserver_perl_warnings_spec.py @@ -0,0 +1,10 @@ +from mamba import description, context, it, fit, before, after +from expects import expect, equal, contain, match +from helper.spec_helper import start_xvnc, kill_xvnc, run_cmd, clean_env, \ + add_kasmvnc_user_docker, clean_kasm_users, start_xvnc_pexpect, \ + write_config, config_filename + +with description("Perl warnings"): + with it("treats Perl warning as error"): + completed_process = run_cmd("vncserver -dry-run") + expect(completed_process.stderr).not_to(match(r'line \d+\.$')) diff --git a/spec/vncserver_select_de_spec.py b/spec/vncserver_select_de_spec.py new file mode 100644 index 0000000..56e291a --- /dev/null +++ b/spec/vncserver_select_de_spec.py @@ -0,0 +1,155 @@ +import os +import sys +import pexpect +from mamba import description, context, it, fit, before, after +from expects import expect, equal + +from helper.spec_helper import start_xvnc, kill_xvnc, run_cmd, clean_env, \ + add_kasmvnc_user_docker, clean_kasm_users, start_xvnc_pexpect + +# WIP. Plan to move to start_xvnc_pexpect(), because pexpect provides a way to +# wait for vncserver output. start_xvnc() just blindly prints input to vncserver +# without knowing what it prints back. + + +def select_de(de_name): + try: + extra_args = f'-select-de {de_name}' + completed_process = start_xvnc(extra_args) + expect(completed_process.returncode).to(equal(0)) + finally: + kill_xvnc() + + +def check_de_was_setup_to_run(de_name): + completed_process = run_cmd(f'grep -q {de_name} ~/.vnc/xstartup') + expect(completed_process.returncode).to(equal(0)) + + +with description('vncserver') as self: + with before.each: + clean_env() + with after.each: + kill_xvnc() + + with context('on the first run'): + with before.each: + add_kasmvnc_user_docker() + + with it('asks user to select a DE'): + child = start_xvnc_pexpect() + child.expect(r'\[1\] Cinnamon.+Manually edit') + child.sendline("1\n") + child.wait() + + expect(child.exitstatus).to(equal(0)) + check_de_was_setup_to_run('cinnamon') + + with it("doesn't prompt user to select a DE if prompting is disabled"): + child = start_xvnc_pexpect("-prompt 0") + child.expect(r'Warning: the Desktop Environment.+wasn\'t selected') + child.wait() + + expect(child.exitstatus).to(equal(0)) + + with it('asks to select a DE, when ran with -select-de'): + child = start_xvnc_pexpect('-select-de') + child.expect(r'\[1\] Cinnamon.+Manually edit') + child.sendline("1\n") + child.wait() + + expect(child.exitstatus).to(equal(0)) + check_de_was_setup_to_run('cinnamon') + + with it("doesn't prompt user to select a DE if prompting is disabled"): + child = start_xvnc_pexpect("-select-de -prompt 0") + child.expect(r'Warning: the Desktop Environment.+wasn\'t selected') + child.wait() + + expect(child.exitstatus).to(equal(0)) + + with it('selects passed DE with -s'): + select_de('mate') + check_de_was_setup_to_run('mate') + + with it('selects manual xstartup editing, not a DE'): + select_de('manual') + check_de_was_setup_to_run('twm') + + with context('after DE was selected'): + with before.each: + add_kasmvnc_user_docker() + + with it("don't ask to choose DE by default"): + select_de('mate') + + completed_process = start_xvnc() + expect(completed_process.returncode).to(equal(0)) + + check_de_was_setup_to_run('mate') + + with it('asks to select a DE, when ran with -select-de'): + select_de('mate') + + choose_cinnamon_and_answer_yes = "1\ny\n" + completed_process = start_xvnc('-select-de', + input=choose_cinnamon_and_answer_yes) + expect(completed_process.returncode).to(equal(0)) + + check_de_was_setup_to_run('cinnamon') + + with it("doesn't ask user to select a DE if prompting is disabled"): + child = start_xvnc_pexpect("-select-de -prompt 0") + child.expect(r'Warning: the Desktop Environment.+wasn\'t selected') + child.wait() + + expect(child.exitstatus).to(equal(0)) + + with it('selects passed DE with -s'): + select_de('mate') + + completed_process = start_xvnc('-select-de cinnamon') + expect(completed_process.returncode).to(equal(0)) + + check_de_was_setup_to_run('cinnamon') + + with context('guided user creation'): + with it('asks to create a user if none exist'): + clean_kasm_users() + + child = start_xvnc_pexpect('-select-de cinnamon') + child.expect('Provide selection number:') + child.sendline('1') + child.expect('Enter username') + child.sendline() + child.expect('Password:') + child.sendline('password') + child.expect('Verify:') + child.sendline('password') + child.expect(pexpect.EOF) + child.close() + expect(child.exitstatus).to(equal(0)) + + home_dir = os.environ['HOME'] + user = os.environ['USER'] + completed_process = run_cmd(f'grep -qw {user} {home_dir}/.kasmpasswd') + expect(completed_process.returncode).to(equal(0)) + + with it('specify custom username'): + custom_username = 'custom_username' + child = start_xvnc_pexpect('-select-de cinnamon') + child.expect('Provide selection number:') + child.sendline('1') + child.expect('Enter username') + child.sendline(custom_username) + child.expect('Password:') + child.sendline('password') + child.expect('Verify:') + child.sendline('password') + child.expect(pexpect.EOF) + child.wait() + expect(child.exitstatus).to(equal(0)) + + home_dir = os.environ['HOME'] + completed_process = run_cmd(f'grep -qw {custom_username} {home_dir}/.kasmpasswd') + expect(completed_process.returncode).to(equal(0)) diff --git a/spec/vncserver_yaml_to_cli_spec.py b/spec/vncserver_yaml_to_cli_spec.py new file mode 100644 index 0000000..4b7571f --- /dev/null +++ b/spec/vncserver_yaml_to_cli_spec.py @@ -0,0 +1,297 @@ +import os +import re +import shutil +from os.path import expanduser +from mamba import description, context, fcontext, it, fit, before, after +from expects import expect, equal, contain, match + +from helper.spec_helper import start_xvnc, kill_xvnc, run_cmd, clean_env, \ + add_kasmvnc_user_docker, clean_kasm_users, start_xvnc_pexpect, \ + write_config, config_filename + +home_dir = expanduser("~") +vnc_dir = f'{home_dir}/.vnc' +user_config = f'{vnc_dir}/kasmvnc.yaml' + + +def run_vncserver(): + return run_cmd(f'vncserver -dry-run -config {config_filename}') + + +def pick_cli_option(cli_option, xvnc_cmd): + cli_option_regex = re.compile(f'\'?-{cli_option}\'?(?:\s+[^-][^\s]*|$)') + results = cli_option_regex.findall(xvnc_cmd) + if len(results) == 0: + return None + + return ' '.join(results) + + +def prepare_env(): + os.makedirs(vnc_dir, exist_ok=True) + shutil.copyfile('spec/kasmvnc.yaml', user_config) + + +with description('YAML to CLI') as self: + with before.all: + prepare_env() + + with context("convert a boolean key"): + with it("convert true to 1"): + write_config(''' + desktop: + allow_resize: true + ''') + completed_process = run_vncserver() + cli_option = pick_cli_option('AcceptSetDesktopSize', + completed_process.stdout) + expect(cli_option).to(equal("-AcceptSetDesktopSize '1'")) + + with it("convert false to 0"): + write_config(''' + desktop: + allow_resize: false + ''') + completed_process = run_vncserver() + cli_option = pick_cli_option('AcceptSetDesktopSize', + completed_process.stdout) + expect(cli_option).to(equal("-AcceptSetDesktopSize '0'")) + + with it("converts a numeric key to a CLI arg"): + write_config(''' + security: + brute_force_protection: + blacklist_threshold: 2 + ''') + completed_process = run_vncserver() + cli_option = pick_cli_option('BlacklistThreshold', + completed_process.stdout) + expect(cli_option).to(equal("-BlacklistThreshold '2'")) + + with it("converts an ANY key to a CLI arg"): + write_config(''' + network: + ssl: + pem_certificate: /etc/ssl/certs/ssl-cert-snakeoil.pem + ''') + completed_process = run_vncserver() + cli_option = pick_cli_option('cert', + completed_process.stdout) + expect(cli_option).to( + equal("-cert '/etc/ssl/certs/ssl-cert-snakeoil.pem'")) + + with it("converts an array key to a CLI arg"): + write_config(''' + keyboard: + remap_keys: + - 0x22->0x40 + - 0x24->0x40 + ''') + completed_process = run_vncserver() + cli_option = pick_cli_option('RemapKeys', + completed_process.stdout) + expect(cli_option).to( + equal("-RemapKeys '0x22->0x40,0x24->0x40'")) + + with it("converts a constant value to the corresponding numeric value"): + write_config(''' + data_loss_prevention: + clipboard: + server_to_client: + size: 20 + ''') + completed_process = run_vncserver() + cli_option = pick_cli_option('DLP_ClipSendMax', + completed_process.stdout) + expect(cli_option).to(equal("-DLP_ClipSendMax '20'")) + + with context("websocketPort"): + with it("converts 'auto' value to calculated value"): + write_config(''' + network: + websocket_port: auto + ''') + completed_process = run_vncserver() + cli_option = pick_cli_option('websocketPort', + completed_process.stdout) + expect(["-websocketPort '8444'", "-websocketPort '8445'"]). \ + to(contain(cli_option)) + + with it("passes numeric value to CLI option"): + write_config(''' + network: + websocket_port: 8555 + ''') + completed_process = run_vncserver() + cli_option = pick_cli_option('websocketPort', + completed_process.stdout) + expect(cli_option).to(equal("-websocketPort '8555'")) + + with it("no key - no CLI option"): + write_config(''' + ''') + completed_process = run_vncserver() + cli_option = pick_cli_option('websocketPort', + completed_process.stdout) + expect(cli_option).to(equal(None)) + + with context("option that can yield nothing"): + with it("converts a config value that yields nothing"): + write_config(''' + network: + protocol: http + ''') + completed_process = run_vncserver() + cli_option = pick_cli_option('noWebsocket', + completed_process.stdout) + expect(cli_option).to(equal(None)) + + with it("converts a config value that yields CLI option"): + write_config(''' + network: + protocol: vnc + ''') + completed_process = run_vncserver() + cli_option = pick_cli_option('noWebsocket', + completed_process.stdout) + expect(cli_option).to(equal("-noWebsocket '1'")) + + with it("interpolates env variable"): + write_config(''' + server: + advanced: + kasm_password_file: ${HOME}/.kasmpasswd + ''') + completed_process = run_vncserver() + cli_option = pick_cli_option('KasmPasswordFile', + completed_process.stdout) + expect(cli_option).to(equal("-KasmPasswordFile '/home/docker/.kasmpasswd'")) + + with it("converts logging options into one -Log"): + write_config(''' + logging: + log_writer_name: all + log_dest: logfile + level: 40 + ''') + completed_process = run_vncserver() + cli_option = pick_cli_option('Log', + completed_process.stdout) + expect(cli_option).to(equal("-Log '*:stdout:40'")) + + with it("converts DLP region options into one -DLP_Region"): + write_config(''' + data_loss_prevention: + visible_region: + top: -10 + left: 10 + right: 40% + bottom: 40 + ''') + completed_process = run_vncserver() + cli_option = pick_cli_option('DLP_Region', + completed_process.stdout) + expect(cli_option).to(equal("-DLP_Region '10,-10,40%,40'")) + + with context("converts x_font_path"): + with it("auto"): + write_config(''' + server: + advanced: + x_font_path: auto + ''') + completed_process = run_vncserver() + cli_option = pick_cli_option('fp', + completed_process.stdout) + expect(cli_option).to(match(r'/usr/share/fonts')) + + with it("none specified"): + write_config(''' + ''') + completed_process = run_vncserver() + cli_option = pick_cli_option('fp', + completed_process.stdout) + expect(cli_option).to(match(r'/usr/share/fonts')) + + with it("path specified"): + write_config(''' + server: + advanced: + x_font_path: /src + ''') + completed_process = run_vncserver() + cli_option = pick_cli_option('fp', completed_process.stdout) + expect(cli_option).to(equal("-fp '/src'")) + + with it("CLI override"): + write_config(''' + server: + advanced: + x_font_path: /src + ''') + completed_process = \ + run_cmd(f'vncserver -dry-run -fp /override -config {config_filename}') + cli_option = pick_cli_option('fp', completed_process.stdout) + expect(cli_option).to(equal("-fp '/override'")) + + with it("converts network.interface to -interface"): + write_config(''' + network: + interface: 0.0.0.0 + ''') + completed_process = run_vncserver() + cli_option = pick_cli_option('interface', + completed_process.stdout) + expect(cli_option).to(equal("-interface '0.0.0.0'")) + + with it("CLI option directly passed, overrides config"): + write_config(''' + encoding: + video_encoding_mode: + jpeg_quality: -1 + ''') + completed_process = \ + run_cmd(f'vncserver -dry-run -JpegVideoQuality 8 -config {config_filename}') + cli_option = pick_cli_option("JpegVideoQuality", + completed_process.stdout) + expect(cli_option).to(equal("'-JpegVideoQuality' '8'")) + + with it("converts 2 keys into a single CLI option"): + write_config(''' + desktop: + resolution: + width: 1024 + height: 768 + ''') + completed_process = run_vncserver() + cli_option = pick_cli_option('geometry', + completed_process.stdout) + expect(cli_option).to(equal("-geometry '1024x768'")) + + with it("ignores empty section override"): + write_config(''' + security: + ''') + completed_process = \ + run_cmd(f'vncserver -dry-run -config spec/fixtures/global_config.yaml,{config_filename}') + cli_option = pick_cli_option('BlacklistThreshold', + completed_process.stdout) + expect(cli_option).to(equal("-BlacklistThreshold '6'")) + + with it("overrides default config value with global config value"): + completed_process = run_cmd("vncserver -dry-run -config spec/fixtures/defaults_config.yaml,spec/fixtures/global_config.yaml") + cli_option = pick_cli_option('BlacklistThreshold', + completed_process.stdout) + expect(cli_option).to(equal("-BlacklistThreshold '6'")) + + with it("uses default config value even if section was overriden"): + completed_process = run_cmd("vncserver -dry-run -config spec/fixtures/defaults_config.yaml,spec/fixtures/global_config.yaml") + cli_option = pick_cli_option('BlacklistTimeout', + completed_process.stdout) + expect(cli_option).to(equal("-BlacklistTimeout '10'")) + + with it("overrides global config with user config value"): + completed_process = run_cmd("vncserver -dry-run -config spec/fixtures/defaults_config.yaml,spec/fixtures/global_config.yaml,spec/fixtures/user_config.yaml") + cli_option = pick_cli_option('BlacklistThreshold', + completed_process.stdout) + expect(cli_option).to(equal("-BlacklistThreshold '7'")) diff --git a/spec/vncserver_yaml_validation_spec.py b/spec/vncserver_yaml_validation_spec.py new file mode 100644 index 0000000..1517858 --- /dev/null +++ b/spec/vncserver_yaml_validation_spec.py @@ -0,0 +1,111 @@ +from mamba import description, context, fit, it, before, after +from expects import expect, equal, contain + +from helper.spec_helper import start_xvnc, kill_xvnc, run_cmd, clean_env, \ + add_kasmvnc_user_docker, clean_kasm_users, start_xvnc_pexpect, \ + config_filename, write_config + +with description('YAML validation') as self: + with it("produces error message for an incomplete data clump"): + write_config(''' + desktop: + resolution: + width: 1024 + ''') + completed_process = run_cmd(f'vncserver -dry-run -test-output-topic validation -config {config_filename}', print_stderr=False) + expect(completed_process.stderr).to(contain('desktop.resolution.width, desktop.resolution.height: either all keys or none must be present')) + + with it("produces error message if int key was set to a string"): + write_config(''' + desktop: + resolution: + width: 1024 + height: none + ''') + completed_process = run_cmd(f'vncserver -dry-run -test-output-topic validation -config {config_filename}', print_stderr=False) + expect(completed_process.stderr).to(contain("desktop.resolution.height 'none': must be an integer")) + + with it("produces no error for valid boolean values"): + write_config(''' + network: + use_ipv4: true + use_ipv6: false + ''') + completed_process = run_cmd(f'vncserver -dry-run -test-output-topic validation -config {config_filename}') + expect(completed_process.stderr).to(equal('')) + + with it("produces an error for invalid boolean values"): + write_config(''' + desktop: + allow_resize: 10 + ''') + + completed_process = run_cmd(f'vncserver -dry-run -test-output-topic validation -config {config_filename}', print_stderr=False) + expect(completed_process.stderr).to(contain("desktop.allow_resize '10': must be true or false")) + + with it("produces an error for invalid enum value"): + write_config(''' + desktop: + pixel_depth: none + ''') + + completed_process = run_cmd(f'vncserver -dry-run -test-output-topic validation -config {config_filename}', print_stderr=False) + expect(completed_process.stderr).to(contain("desktop.pixel_depth 'none': must be one of [16, 24, 32]")) + + with it("produces an error for invalid pattern enum value"): + write_config(''' + desktop: + pixel_depth: 16|24|32 + ''') + + completed_process = run_cmd(f'vncserver -dry-run -test-output-topic validation -config {config_filename}', print_stderr=False) + expect(completed_process.stderr).to(contain("desktop.pixel_depth '16|24|32': must be one of [16, 24, 32]")) + + with it("produces an error fo partially present enum value"): + write_config(''' + network: + protocol: vnc2 + ''') + + completed_process = run_cmd(f'vncserver -dry-run -test-output-topic validation -config {config_filename}', print_stderr=False) + expect(completed_process.stderr).to(contain("network.protocol 'vnc2': must be one of [http, vnc]")) + + with it("is silent for a valid enum value"): + write_config(''' + desktop: + pixel_depth: 16 + ''') + + completed_process = run_cmd(f'vncserver -dry-run -test-output-topic validation -config {config_filename}') + expect(completed_process.stderr).to(equal("")) + + with it("produces an error for an array value"): + write_config(''' + keyboard: + remap_keys: + - 0xzz->0x40 + - 0x24->0x40 + ''') + + completed_process = run_cmd(f'vncserver -dry-run -test-output-topic validation -config {config_filename}', print_stderr=False) + expect(completed_process.stderr).to(contain("keyboard.remap_keys '0xzz->0x40, 0x24->0x40': must be in the format 0x->0x")) + + with context("unsupported keys"): + with it("produces an error for an unsupported top-level key"): + write_config(''' + foo: 1 + ''') + + completed_process = run_cmd(f'vncserver -dry-run -test-output-topic validation -config {config_filename}', print_stderr=False) + expect(completed_process.stderr).to( + contain("Unsupported config keys found:\nfoo")) + + with it("produces an error for an unsupported 2nd-level key"): + write_config(''' + bar: + baz: 1 + ''') + + completed_process = run_cmd(f'vncserver -dry-run -test-output-topic validation -config {config_filename}', print_stderr=False) + expect(completed_process.stderr).to( + contain("Unsupported config keys found:\nbar.baz")) diff --git a/unix/KasmVNC/CliOption.pm b/unix/KasmVNC/CliOption.pm new file mode 100644 index 0000000..1088d4b --- /dev/null +++ b/unix/KasmVNC/CliOption.pm @@ -0,0 +1,199 @@ +package KasmVNC::CliOption; + +use strict; +use warnings; +use v5.10; +use List::Util qw(first); +use List::MoreUtils qw(all); +use Switch; +use Data::Dumper; + +use KasmVNC::DataClumpValidator; +use KasmVNC::Utils; + +our $fetchValueSub; +$KasmVNC::CliOption::dataClumpValidator = KasmVNC::DataClumpValidator->new(); +@KasmVNC::CliOption::isActiveCallbacks = (); + +sub new { + my ($class, $args) = @_; + my $self = bless { + name => $args->{name}, + configKeys => $args->{configKeys}, + deriveValueSub => $args->{deriveValueSub} || sub { + my $self = shift; + my @values = @{ listify($self->configValues()) }; + + @values = map { deriveBoolean($_) } @values; + + join ",", @values; + }, + isActiveSub => $args->{isActiveSub} || sub { + my $self = shift; + + scalar $self->configValues() > 0; + }, + errors => [] + }, $class; +} + +sub activate { + my $self = shift; + + $self->makeKeysWithValuesAccessible(); +} + +sub beforeIsActive { + my $callback = shift; + + push @KasmVNC::CliOption::isActiveCallbacks, $callback; +} + +sub isActiveByCallbacks { + my $self = shift; + + all { $_->($self) } @KasmVNC::CliOption::isActiveCallbacks; +} + +sub makeKeysWithValuesAccessible { + my $self = shift; + + foreach my $name (@{ $self->configKeyNames() }) { + my $value = $self->fetchValue($name); + $self->{$name} = $value if defined($value); + } +} + +sub isActive { + my $self = shift; + + $self->isActiveByCallbacks() && $self->{isActiveSub}->($self); +} + +sub toString { + my $self = shift; + + return unless $self->isActive(); + + my $derivedValue = $self->deriveValue(); + if (defined($derivedValue)) { + return "-$self->{name} " . "'$derivedValue'"; + } + + "-$self->{name}"; +} + +sub toValue { + my $self = shift; + + return unless $self->isActive(); + + $self->deriveValue(); +} + +sub deriveValue { + my $self = shift; + + my $value = $self->{deriveValueSub}->($self); + $self->interpolateEnvVars($value); +} + +sub interpolateEnvVars { + my $self = shift; + my $value = shift; + + return $value unless defined($value); + + while ($value =~ /\$\{(\w+)\}/) { + my $envValue = $ENV{$1}; + $value =~ s/\Q$&\E/$envValue/; + } + + $value; +} + +sub errorMessages { + my $self = shift; + + join "\n", @{ $self->{errors} }; +} + +# private + +sub isValid { + my $self = shift; + + $self->validate() unless $self->{validated}; + + $self->isNoErrorsPresent(); +} + +sub validate { + my $self = shift; + + $self->validateDataClump(); + $self->validateConfigValues(); + + $self->{validated} = 1; +} + +sub isNoErrorsPresent { + my $self = shift; + + scalar @{ $self->{errors} } == 0; +} + +sub validateDataClump { + my $self = shift; + + $KasmVNC::CliOption::dataClumpValidator->validate($self); +} + +sub configValues { + my $self = shift; + + map { $self->fetchValue($_->{name}) } @{ $self->{configKeys} }; +} + +sub configValue { + my $self = shift; + + die "Multiple or no config keys defined for $self->{name}" + if (scalar @{ $self->{configKeys} } != 1); + + @{ listify($self->configValues()) }[0]; +} + +sub configKeyNames { + my $self = shift; + + my @result = map { $_->{name} } @{ $self->{configKeys} }; + \@result; +} + +sub hasKey { + my $self = shift; + my $configKey = shift; + + first { $_ eq $configKey } @{ $self->configKeyNames() }; +} + +sub addErrorMessage { + my ($self, $errorMessage) = @_; + + push @{ $self->{errors} }, $errorMessage; +} + +sub validateConfigValues { + my $self = shift; + + map { $_->validate($self) } @{ $self->{configKeys} }; +} + +sub fetchValue { + my $self = shift; + + &$fetchValueSub(shift); +} + +1; diff --git a/unix/KasmVNC/Config.pm b/unix/KasmVNC/Config.pm new file mode 100644 index 0000000..7bcfdc2 --- /dev/null +++ b/unix/KasmVNC/Config.pm @@ -0,0 +1,86 @@ +package KasmVNC::Config; + +use strict; +use warnings; +use v5.10; +use YAML::Tiny; +use Data::Dumper; +use Hash::Merge::Simple; +use KasmVNC::Utils; + +sub merge { + my @configsToMerge = map { $_->{data} } @_; + my $mergedConfig = Hash::Merge::Simple::merge(@configsToMerge) // {}; + + KasmVNC::Config->new({ data => $mergedConfig }); +} + +sub new { + my ($class, $args) = @_; + my $self = bless { + filename => $args->{filename}, + data => $args->{data} + }, $class; + + $self->load() if $self->{filename}; + $self; +} + +sub load { + my $self = shift; + + failIfConfigNotReadable($self->{filename}); + + $self->{data} = YAML::Tiny->read($self->{filename})->[0]; +} + +sub get { + my ($self, $absoluteKey) = @_; + my $path = absoluteKeyToHashPath($absoluteKey); + my $config = $self->{data}; + + my $value = eval "\$config$path"; + return unless defined($value); + + $value; +} + +sub exists { + my ($self, $absoluteKey) = @_; + my $path = absoluteKeyToHashPath($absoluteKey); + my $config = $self->{data}; + + eval "exists \$config$path"; +} + +sub delete { + my ($self, $absoluteKey) = @_; + my $path = absoluteKeyToHashPath($absoluteKey); + my $config = $self->{data}; + + eval "delete \$config$path"; +} + +sub isEmpty { + my ($self, $absoluteKey) = @_; + my $path = absoluteKeyToHashPath($absoluteKey); + my $config = $self->{data}; + + $self->exists($absoluteKey) && isBlank($self->get($absoluteKey)); +} + +sub absoluteKeyToHashPath { + my $absoluteKey = shift; + + my @keyParts = split(/\./, $absoluteKey); + @keyParts = map { "->{\"$_\"}" } @keyParts; + join "", @keyParts; +} + +sub failIfConfigNotReadable { + my $config = shift; + + -r $config || die "Couldn't load config: $config"; +} + +1; diff --git a/unix/KasmVNC/ConfigKey.pm b/unix/KasmVNC/ConfigKey.pm new file mode 100644 index 0000000..f60fd63 --- /dev/null +++ b/unix/KasmVNC/ConfigKey.pm @@ -0,0 +1,123 @@ +package KasmVNC::ConfigKey; + +use strict; +use warnings; +use v5.10; +use Switch; +use Data::Dumper; + +use KasmVNC::Utils; + +our $fetchValueSub; + +use constant { + INT => 0, + STRING => 1, + BOOLEAN => 2, + ANY => 4 +}; + +sub new { + my ($class, $args) = @_; + my $self = bless { + name => $args->{name}, + type => $args->{type}, + validator => $args->{validator} + }, $class; +} + +sub validate { + my $self = shift; + $self->{cliOption} = shift; + + return if $self->isValueBlank(); + + if ($self->{validator}) { + $self->resolveValidatorFromFunction() if (ref $self->{validator} eq "CODE"); + + $self->{validator}->validate($self); + return; + } + + switch($self->{type}) { + case INT { + $self->validateInt(); + } + case BOOLEAN { + $self->validateBoolean(); + } + } +} + +sub resolveValidatorFromFunction { + my $self = shift; + + $self->{validator} = $self->{validator}(); +} + +sub addErrorMessage { + my $self = shift; + + my $errorMessage = $self->constructErrorMessage($_[0]); + $self->{cliOption}->addErrorMessage($errorMessage); +} + +# private + +sub validateBoolean { + my $self = shift; + + return if $self->isValidBoolean(); + $self->addErrorMessage("must be true or false"); +} + +sub validateInt { + my $self = shift; + + return if $self->isValidInt(); + + $self->addErrorMessage("must be an integer"); +} + +sub isValueBlank { + my $self = shift; + + my $value = $self->value(); + !defined($value) || $value eq ""; +} + +sub fetchValue { + my $self = shift; + + &$fetchValueSub(shift); +} + +sub constructErrorMessage { + my $self = shift; + my $staticErrorMessage = shift; + + my $name = $self->{name}; + my $value = join ", ", @{ listify($self->fetchValue($name)) }; + + "$name '$value': $staticErrorMessage"; +} + +sub isValidInt { + my $self = shift; + + $self->value() =~ /^(-)?\d+$/; +} + +sub isValidBoolean { + my $self = shift; + + $self->value() =~ /^true|false$/; +} + +sub value { + my $self = shift; + + $self->fetchValue($self->{name}); +} + +our @EXPORT_OK = ('INT', 'STRING', 'BOOLEAN'); diff --git a/unix/KasmVNC/DataClumpValidator.pm b/unix/KasmVNC/DataClumpValidator.pm new file mode 100644 index 0000000..210222e --- /dev/null +++ b/unix/KasmVNC/DataClumpValidator.pm @@ -0,0 +1,48 @@ +package KasmVNC::DataClumpValidator; + +use strict; +use warnings; +use v5.10; +use Data::Dumper; + +sub new { + my ($class, $args) = @_; + my $self = bless { + }, $class; +} + +sub validate { + my $self = shift; + $self->{cliOption} = shift; + + if ($self->isDataClump() && !$self->isWhole()) { + $self->{cliOption}->addErrorMessage($self->errorMessage()); + } +} + +# private + +sub isWhole { + my $self = shift; + + my $numberOfValues = scalar $self->{cliOption}->configValues(); + return 1 if $numberOfValues == 0; + + scalar @{ $self->{cliOption}->{configKeys} } == $numberOfValues; +} + +sub isDataClump { + my $self = shift; + + scalar(@{ $self->{cliOption}->{configKeys} }) > 1; +} + +sub errorMessage { + my $self = shift; + + my $configKeys = join ", ", @{ $self->{cliOption}->configKeyNames() }; + + "$configKeys: either all keys or none must be present"; +} + +1; diff --git a/unix/KasmVNC/EnumValidator.pm b/unix/KasmVNC/EnumValidator.pm new file mode 100644 index 0000000..861ff62 --- /dev/null +++ b/unix/KasmVNC/EnumValidator.pm @@ -0,0 +1,36 @@ +package KasmVNC::EnumValidator; + +use strict; +use warnings; +use v5.10; +use List::MoreUtils qw(any); +use Data::Dumper; +use KasmVNC::Utils; + +sub new { + my ($class, $args) = @_; + my $self = bless { + allowedValues => $args->{allowedValues} + }, $class; +} + +sub validate { + my $self = shift; + my $configKey = shift; + my @values = @{ listify($configKey->value()) }; + + foreach my $value (@values) { + unless (any { $_ eq $value } @{ $self->{allowedValues} }) { + $configKey->addErrorMessage($self->errorMessage()); + } + } +} + +sub errorMessage { + my $self = shift; + + my $allowedValuesText = join ", ", @{ $self->{allowedValues} }; + "must be one of [$allowedValuesText]" +} + +1; diff --git a/unix/KasmVNC/Logger.pm b/unix/KasmVNC/Logger.pm new file mode 100644 index 0000000..b8e64af --- /dev/null +++ b/unix/KasmVNC/Logger.pm @@ -0,0 +1,20 @@ +package KasmVNC::Logger; + +use strict; +use warnings; +use v5.10; +use Data::Dumper; + +sub new { + my ($class, $args) = @_; + my $self = bless { + }, $class; +} + +sub warn { + my $self = shift; + + say { *STDERR } @_; +} + +1; diff --git a/unix/KasmVNC/PatternValidator.pm b/unix/KasmVNC/PatternValidator.pm new file mode 100644 index 0000000..9e9c07c --- /dev/null +++ b/unix/KasmVNC/PatternValidator.pm @@ -0,0 +1,37 @@ +package KasmVNC::PatternValidator; + +use strict; +use warnings; +use v5.10; +use Data::Dumper; + +use KasmVNC::Utils; + +sub new { + my ($class, $args) = @_; + my $self = bless { + pattern => $args->{pattern}, + errorMessage => $args->{errorMessage} + }, $class; +} + +sub validate { + my $self = shift; + $self->{configKey} = shift; + my @values = @{ listify($self->{configKey}->value()) }; + + foreach my $value (@values) { + $self->validateValue($value); + } +} + +sub validateValue { + my $self = shift; + my $value = shift; + + unless ($value =~ $self->{pattern}) { + $self->{configKey}->addErrorMessage($self->{errorMessage}); + } +} + +1; diff --git a/unix/KasmVNC/TextOption.pm b/unix/KasmVNC/TextOption.pm new file mode 100644 index 0000000..34cf58d --- /dev/null +++ b/unix/KasmVNC/TextOption.pm @@ -0,0 +1,40 @@ +package KasmVNC::TextOption; + +use strict; +use warnings; +use v5.10; +use Data::Dumper; + +sub new { + my ($class, $args) = @_; + my $self = bless { + description => $args->{description}, + callback => $args->{callback} || sub {}, + }, $class; +} + +use overload fallback => 1, q("") => sub { + my $self = shift; + + $self->stringify(); +}; + +sub stringify { + my $self = shift; + + $self->{description}; +} + +sub description { + my $self = shift; + + $self->{description}; +} + +sub callback { + my $self = shift; + + $self->{callback}; +} + +1; diff --git a/unix/KasmVNC/TextUI.pm b/unix/KasmVNC/TextUI.pm new file mode 100644 index 0000000..87d2cd9 --- /dev/null +++ b/unix/KasmVNC/TextUI.pm @@ -0,0 +1,55 @@ +package KasmVNC::TextUI; + +use strict; +use warnings; +use v5.10; +use Data::Dumper; + +@KasmVNC::TextUI::ISA = qw(Exporter); + +our @EXPORT = ('Prompt', 'askUserToChooseOption'); + +sub askUserToChooseOption { + my %args = @_; + my $banner = $args{banner}; + my $prompt = $args{prompt}; + my $options = $args{options}; + + my $userInput; + my $i = 1; + my %numberedOptions = map { $i++ => $_ } @$options; + + while (1) { + say $banner; + + printOptions(\%numberedOptions); + + $userInput = Prompt($prompt . ": "); + last if $numberedOptions{$userInput}; + + say "Invalid choice: '$userInput'"; + } + + $numberedOptions{$userInput}; +} + +sub printOptions { + my $choices = shift; + + foreach my $choiceNumber (sort keys %$choices) { + say "[$choiceNumber] " . $choices->{$choiceNumber}; + } + print "\n"; +} + +sub Prompt { + my $prompt = shift; + + print($prompt); + my $userInput = ; + $userInput =~ s/^\s+|\s+$//g; + + return $userInput; +} + +1; diff --git a/unix/KasmVNC/User.pm b/unix/KasmVNC/User.pm new file mode 100644 index 0000000..eb90517 --- /dev/null +++ b/unix/KasmVNC/User.pm @@ -0,0 +1,54 @@ +package KasmVNC::User; + +use strict; +use warnings; +use v5.10; + +sub new { + my ($class, $args) = @_; + + my $self = bless { + name => $args->{name}, + permissions => $args->{permissions} + }, $class; +} + +sub permissionsExplanation { + my $self = shift; + + my %permissionExplanations = ("w" => "can use keyboard and mouse", + "o" => "can add/remove users", + "" => "can only view"); + foreach (qw(ow wo)) { + $permissionExplanations{$_} = "can use keyboard and mouse, add/remove users"; + } + + $self->{permissions} =~ s/r//g; + $permissionExplanations{$self->{permissions}}; +} + +sub name { + my $self = shift; + + $self->{name}; +} + +sub permissions { + my $self = shift; + + $self->{permissions}; +} + +sub isOwner { + my $self = shift; + + $self->permissions() =~ /o/; +} + +sub toString { + my $self = shift; + + $self->name() . " (" . $self->permissionsExplanation() . ")"; +} + +1; diff --git a/unix/KasmVNC/Users.pm b/unix/KasmVNC/Users.pm new file mode 100644 index 0000000..7a465fd --- /dev/null +++ b/unix/KasmVNC/Users.pm @@ -0,0 +1,169 @@ +package KasmVNC::Users; + +use strict; +use warnings; +use v5.10; +use Data::Dumper; +use List::MoreUtils qw(any); +use KasmVNC::User; + +our $vncPasswdBin; +our $logger; + +sub new { + my ($class, $args) = @_; + my $self = bless { + passwordFileName => $args->{passwordFileName}, + }, $class; +} + +sub loadFrom { + my ($self, $passwordFileName) = @_; + + my $users = KasmVNC::Users->new({ + passwordFileName => $passwordFileName, + vncPasswdBin => $vncPasswdBin + }); + $users->load(); + + $users; +} + +sub optionsToCliOptions { + my %options = @_; + my @cliOptons = (); + + push(@cliOptons, "-u \"@{[$options{username}]}\""); + if ($options{permissions}) { + push(@cliOptons, "-" . $options{permissions}); + } + if ($options{changePermissions}) { + push(@cliOptons, "-n"); + } + + join " ", @cliOptons; +} + +sub runKasmvncpasswd { + my ($self, $options) = @_; + my @cliOptions = optionsToCliOptions(%{ $options }); + + system("$vncPasswdBin " . join(" ", @cliOptions) . " " . $self->{passwordFileName}); + $? ? 0 : 1; +} + +sub findByPermissions { + my ($self, $permissions) = @_; + + any { $_->{permissions} =~ /$permissions/ } + (values %{ $self->{store} }); +} + +sub fetchUser { + my ($self, $username) = @_; + + $self->{store}->{$username}; +} + +sub userExists { + fetchUser @_; +} + +sub addUser { + my ($self, $username, $permissions) = @_; + + if ($self->userExists($username)) { + $logger->warn("User $username already exists"); + return; + } + + $self->runKasmvncpasswd({ username => $username, permissions => $permissions }); +} + +sub checkUserExists { + my ($self, $username) = @_; + + unless ($self->fetchUser($username)) { + die "User \"$username\" doesn't exist"; + } +} + +sub addPermissions { + my ($self, $username, $permissions) = @_; + + $self->checkUserExists($username); + + my $user = $self->fetchUser($username); + $permissions .= $user->{permissions}; + + $self->changePermissions($username, $permissions); +} + +sub changePermissions { + my ($self, $username, $permissions) = @_; + + $self->checkUserExists($username); + + $self->runKasmvncpasswd({ username => $username, permissions => $permissions, + changePermissions => 1 }); +} + +sub load { + my $self = shift; + + $self->{store} = $self->_load(); +} + +sub reload { + my $self = shift; + + $self->load(); +} + +sub count { + my $self = shift; + + return scalar(keys %{ $self->{store} }); +} + +sub is_empty { + my $self = shift; + + $self->count() eq 0; +} + +sub _load { + my $self = shift; + + my $store = {}; + + open(FH, '<', $self->{passwordFileName}) or return $store; + + while(){ + chomp $_; + my ($name, $__, $permissions) = split(':', $_); + $store->{$name} = KasmVNC::User->new({ + name => $name, + permissions => $permissions + }) + } + + close(FH); + + $store; +} + +sub users { + my $self = shift; + + values %{ $self->{store} } +} + +sub toString { + my $self = shift; + + my @userDescriptions = map { $_->toString() } $self->users(); + join "\n", @userDescriptions; +} + +1; diff --git a/unix/KasmVNC/Utils.pm b/unix/KasmVNC/Utils.pm new file mode 100644 index 0000000..2340d19 --- /dev/null +++ b/unix/KasmVNC/Utils.pm @@ -0,0 +1,66 @@ +package KasmVNC::Utils; + +use strict; +use warnings; +use v5.10; +use Data::Dumper; +use Switch; + +use Exporter; + +@KasmVNC::Utils::ISA = qw(Exporter); + +our @EXPORT = ('listify', 'flatten', 'isBlank', 'isPresent', 'deriveBoolean', + 'printStackTrace'); + +sub listify { + # Implementation based on Hyper::Functions + if (scalar @_ > 1) { + return [ @_ ]; + } elsif (defined $_[0]) { + my $ref_type = ref $_[0]; + return ($ref_type && $ref_type eq 'ARRAY') ? $_[0] : [ $_[0] ]; + } else { + return []; + } +} + +sub flatten { + map { ref $_ ? flatten(@{$_}) : $_ } @_; +} + +sub isBlank { + !isPresent(shift); +} + +sub isPresent { + my $value = shift; + if (ref($value) eq "HASH") { + return scalar(keys %$value) > 0; + } + + defined($value); +} + +sub deriveBoolean { + my $value = shift; + + switch($value) { + case 'true' { + return 1; + } + case 'false' { + return 0; + } + else { + return $value; + } + } +} + +sub printStackTrace { + my $trace = Devel::StackTrace->new; + print { *STDERR } $trace->as_string; +} + +1; diff --git a/unix/kasmvnc.yaml b/unix/kasmvnc.yaml new file mode 100644 index 0000000..e69de29 diff --git a/unix/kasmvnc_defaults.yaml b/unix/kasmvnc_defaults.yaml new file mode 100644 index 0000000..7fdd0d6 --- /dev/null +++ b/unix/kasmvnc_defaults.yaml @@ -0,0 +1,131 @@ +--- +desktop: + resolution: + width: 1024 + height: 768 + allow_resize: true + pixel_depth: 24 + +network: + protocol: http + interface: 0.0.0.0 + websocket_port: auto + use_ipv4: true + use_ipv6: true + udp: + public_ip: auto + port: auto + ssl: + pem_certificate: /etc/ssl/certs/ssl-cert-snakeoil.pem + pem_key: /etc/ssl/private/ssl-cert-snakeoil.key + require_ssl: true + +user_session: + # session_type: shared + new_session_disconnects_existing_exclusive_session: false + concurrent_connections_prompt: false + concurrent_connections_prompt_timeout: 10 + idle_timeout: never + +keyboard: + remap_keys: + # - 0x22->0x40 + ignore_numlock: false + raw_keyboard: false + +# Mouse, trackpad, etc. +pointer: + enabled: true + +runtime_configuration: + allow_client_to_override_kasm_server_settings: true + allow_override_standard_vnc_server_settings: true + allow_override_list: + - pointer.enabled + - data_loss_prevention.clipboard.server_to_client.enabled + - data_loss_prevention.clipboard.client_to_server.enabled + - data_loss_prevention.clipboard.server_to_client.primary_clipboard_enabled + +logging: + log_writer_name: all + log_dest: logfile + # 0 - minimal verbosity, 100 - most verbose + level: 30 + +security: + brute_force_protection: + blacklist_threshold: 5 + blacklist_timeout: 10 + +data_loss_prevention: + visible_region: + # top: 10 + # left: 10 + # right: 40 + # bottom: 40 + concealed_region: + allow_click_down: false + allow_click_release: false + clipboard: + delay_between_operations: none + allow_mimetypes: + - chromium/x-web-custom-data + - text/html + - image/png + # Add to docs: Cut buffers and CLIPBOARD selection. + server_to_client: + enabled: false + size: unlimited + primary_clipboard_enabled: false + client_to_server: + enabled: false + size: unlimited + keyboard: + enabled: true + rate_limit: unlimited + # "verbose" SETTING LOGS YOUR PRIVATE INFORMATION. Keypresses and clipboard + # content. + logging: + level: off + +encoding: + max_frame_rate: 60 + full_frame_updates: none + rect_encoding_mode: + min_quality: 7 + max_quality: 8 + consider_lossless_quality: 10 + rectangle_compress_threads: auto + + video_encoding_mode: + jpeg_quality: -1 + webp_quality: -1 + max_resolution: + width: 1920 + height: 1080 + enter_video_encoding_mode: + time_threshold: 5 + area_threshold: 45% + exit_video_encoding_mode: + time_threshold: 3 + logging: + level: off + scaling_algorithm: progressive_bilinear + + compare_framebuffer: auto + zrle_zlib_level: auto + hextile_improved_compression: true + +server: + advanced: + x_font_path: auto + httpd_directory: /usr/share/kasmvnc/www + kasm_password_file: ${HOME}/.kasmpasswd + x_authority_file: auto + auto_shutdown: + no_user_session_timeout: never + active_user_session_timeout: never + inactive_user_session_timeout: never + +command_line: + prompt: true diff --git a/unix/vncserver b/unix/vncserver old mode 100644 new mode 100755 index c6e6d86..649fbab --- a/unix/vncserver +++ b/unix/vncserver @@ -25,416 +25,66 @@ # vncserver - wrapper script to start an X VNC server. # -# First make sure we're operating in a sane environment. -$exedir = ""; -$slashndx = rindex($0, "/"); -if($slashndx>=0) { - $exedir = substr($0, 0, $slashndx+1); -} - -&SanityCheck(); - -# -# Global variables. You may want to configure some of these for -# your site -# - -$geometry = "1024x768"; -#$depth = 16; - -$vncUserDir = "$ENV{HOME}/.vnc"; -$vncUserConfig = "$vncUserDir/config"; -$vncUserName = `id -u -n`; -$vncUserName =~ s/^\s+|\s+$//g; - -$vncSystemConfigDir = "/etc/kasmvnc"; -$vncSystemConfigDefaultsFile = "$vncSystemConfigDir/vncserver-config-defaults"; -$vncSystemConfigMandatoryFile = "$vncSystemConfigDir/vncserver-config-mandatory"; - -$skipxstartup = 0; -$xauthorityFile = "$ENV{XAUTHORITY}" || "$ENV{HOME}/.Xauthority"; - -$xstartupFile = $vncUserDir . "/xstartup"; -$defaultXStartup - = ("#!/bin/sh\n\n". - "unset SESSION_MANAGER\n". - "unset DBUS_SESSION_BUS_ADDRESS\n". - "OS=`uname -s`\n". - "if [ \$OS = 'Linux' ]; then\n". - " case \"\$WINDOWMANAGER\" in\n". - " \*gnome\*)\n". - " if [ -e /etc/SuSE-release ]; then\n". - " PATH=\$PATH:/opt/gnome/bin\n". - " export PATH\n". - " fi\n". - " ;;\n". - " esac\n". - "fi\n". - "if [ -x /etc/X11/xinit/xinitrc ]; then\n". - " exec /etc/X11/xinit/xinitrc\n". - "fi\n". - "if [ -f /etc/X11/xinit/xinitrc ]; then\n". - " exec sh /etc/X11/xinit/xinitrc\n". - "fi\n". - "[ -r \$HOME/.Xresources ] && xrdb \$HOME/.Xresources\n". - "xsetroot -solid grey\n". - "xterm -geometry 80x24+10+10 -ls -title \"\$VNCDESKTOP Desktop\" &\n". - "twm\n"); - -$defaultConfig - = ("## Supported server options to pass to vncserver upon invocation can be listed\n". - "## in this file. See the following manpages for more: vncserver(1) Xvnc(1).\n". - "## Several common ones are shown below. Uncomment and modify to your liking.\n". - "##\n". - "# securitytypes=vncauth,tlsvnc\n". - "# desktop=sandbox\n". - "# geometry=2000x1200\n". - "# localhost\n". - "# alwaysshared\n". - "username=$vncUserName"); - -chop($host = `uname -n`); - -if (-d "/etc/X11/fontpath.d") { - $fontPath = "catalogue:/etc/X11/fontpath.d"; -} - -@fontpaths = ('/usr/share/X11/fonts', '/usr/share/fonts', '/usr/share/fonts/X11/'); -if (! -l "/usr/lib/X11") {push(@fontpaths, '/usr/lib/X11/fonts');} -if (! -l "/usr/X11") {push(@fontpaths, '/usr/X11/lib/X11/fonts');} -if (! -l "/usr/X11R6") {push(@fontpaths, '/usr/X11R6/lib/X11/fonts');} -push(@fontpaths, '/usr/share/fonts/default'); - -@fonttypes = ('misc', - '75dpi', - '100dpi', - 'Speedo', - 'Type1'); - -foreach $_fpath (@fontpaths) { - foreach $_ftype (@fonttypes) { - if (-f "$_fpath/$_ftype/fonts.dir") { - if (! -l "$_fpath/$_ftype") { - $defFontPath .= "$_fpath/$_ftype,"; - } - } - } -} - -if ($defFontPath) { - if (substr($defFontPath, -1, 1) == ',') { - chop $defFontPath; - } -} - -if ($fontPath eq "") { - $fontPath = $defFontPath; -} - -# Check command line options - -&ParseOptions("-geometry",1,"-depth",1,"-pixelformat",1,"-name",1,"-kill",1, - "-help",0,"-h",0,"--help",0,"-fp",1,"-list",0,"-fg",0,"-autokill",0,"-noxstartup",0,"-xstartup",1); - -&Usage() if ($opt{'-help'} || $opt{'-h'} || $opt{'--help'}); - -&Kill() if ($opt{'-kill'}); - -&List() if ($opt{'-list'}); - -# Uncomment this line if you want default geometry, depth and pixelformat -# to match the current X display: -# &GetXDisplayDefaults(); - -if ($opt{'-geometry'}) { - $geometry = $opt{'-geometry'}; -} -if ($opt{'-depth'}) { - $depth = $opt{'-depth'}; - $pixelformat = ""; -} -if ($opt{'-pixelformat'}) { - $pixelformat = $opt{'-pixelformat'}; -} -if ($opt{'-noxstartup'}) { - $skipxstartup = 1; -} -if ($opt{'-xstartup'}) { - $xstartupFile = $opt{'-xstartup'}; -} -if ($opt{'-fp'}) { - $fontPath = $opt{'-fp'}; - $fpArgSpecified = 1; -} - -&CheckGeometryAndDepth(); - -# Create the user's vnc directory if necessary. -if (!(-e $vncUserDir)) { - if (!mkdir($vncUserDir,0755)) { - die "$prog: Could not create $vncUserDir.\n"; - } -} - -# Find display number. -if ((@ARGV > 0) && ($ARGV[0] =~ /^:(\d+)$/)) { - $displayNumber = $1; - shift(@ARGV); - if (!&CheckDisplayNumber($displayNumber)) { - die "A VNC server is already running as :$displayNumber\n"; - } -} elsif ((@ARGV > 0) && ($ARGV[0] !~ /^-/) && ($ARGV[0] !~ /^\+/)) { - &Usage(); -} else { - $displayNumber = &GetDisplayNumber(); -} - -$vncPort = 5900 + $displayNumber; - -if ($opt{'-name'}) { - $desktopName = $opt{'-name'}; -} else { - $desktopName = "$host:$displayNumber ($ENV{USER})"; -} - -my %default_opts; -my %config; - -# We set some reasonable defaults. Config file settings -# override these where present. -$default_opts{desktop} = "edString($desktopName); -$default_opts{auth} = "edString($xauthorityFile); -$default_opts{geometry} = $geometry if ($geometry); -$default_opts{depth} = $depth if ($depth); -$default_opts{pixelformat} = $pixelformat if ($pixelformat); -$default_opts{rfbwait} = 30000; -$default_opts{rfbauth} = "$vncUserDir/passwd"; -$default_opts{rfbport} = $vncPort; -$default_opts{fp} = $fontPath if ($fontPath); -$default_opts{pn} = ""; - -# Load user-overrideable system defaults -LoadConfig($vncSystemConfigDefaultsFile); - -# Then the user's settings -LoadConfig($vncUserConfig); - -# And then override anything set above if mandatory settings exist. -# WARNING: "Mandatory" is used loosely here! As the man page says, -# there is nothing stopping someone from EASILY subverting the -# settings in $vncSystemConfigMandatoryFile by simply passing -# CLI args to vncserver, which trump config files! To properly -# hard force policy in a non-subvertible way would require major -# development work that touches Xvnc itself. -LoadConfig($vncSystemConfigMandatoryFile, 1); - -# -# Check whether VNC authentication is enabled, and if so, prompt the user to -# create a VNC password if they don't already have one. -# - -$securityTypeArgSpecified = 0; -$vncAuthEnabled = 0; -$passwordArgSpecified = 0; -@vncAuthStrings = ("vncauth", "tlsvnc", "x509vnc"); - -# ...first we check our configuration files' settings -#if ($config{'securitytypes'}) { -# $securityTypeArgSpecified = 1; -# foreach $arg2 (split(',', $config{'securitytypes'})) { -# if (grep {$_ eq lc($arg2)} @vncAuthStrings) { -# $vncAuthEnabled = 1; -# } -# } -#} - -# ...and finally we check CLI args, which in the case of the topic at -# hand (VNC auth or not), override anything found in configuration files -# (even so-called "mandatory" settings). -#for ($i = 0; $i < @ARGV; ++$i) { -# # -SecurityTypes can be followed by a space or "=" -# my @splitargs = split('=', $ARGV[$i]); -# if (@splitargs <= 1 && $i < @ARGV - 1) { -# push(@splitargs, $ARGV[$i + 1]); -# } -# if (lc(@splitargs[0]) eq "-securitytypes") { -# if (@splitargs > 1) { -# $securityTypeArgSpecified = 1; -# } -# foreach $arg2 (split(',', @splitargs[1])) { -# if (grep {$_ eq lc($arg2)} @vncAuthStrings) { -# $vncAuthEnabled = 1; -# } -# } -# } -# if ((lc(@splitargs[0]) eq "-password") -# || (lc(@splitargs[0]) eq "-passwordfile" -# || (lc(@splitargs[0]) eq "-rfbauth"))) { -# $passwordArgSpecified = 1; -# } -#} - -# Disable vnc auth, kasmvnc uses https basic auth -system("echo 'WrLNwLrcrxM=' | base64 -d > $vncUserDir/passwd"); - -$kasmAuthEnabled = 1; - -if ($kasmAuthEnabled) { - if (!(-e "$ENV{HOME}/.kasmpasswd")) { - warn "\nYou need to create a KasmVNC user to access your desktops.\n\n"; - system($exedir."kasmvncpasswd $ENV{HOME}/.kasmpasswd"); - if (($? >> 8) != 0) { - exit 1; - } - } -} - -$desktopLog = "$vncUserDir/$host:$displayNumber.log"; -unlink($desktopLog); - -# Make an X server cookie and set up the Xauthority file -# mcookie is a part of util-linux, usually only GNU/Linux systems have it. -$cookie = `mcookie`; -# Fallback for non GNU/Linux OS - use /dev/urandom on systems that have it, -# otherwise use perl's random number generator, seeded with the sum -# of the current time, our PID and part of the encrypted form of the password. -if ($cookie eq "" && open(URANDOM, '<', '/dev/urandom')) { - my $randata; - if (sysread(URANDOM, $randata, 16) == 16) { - $cookie = unpack 'h*', $randata; - } - close(URANDOM); -} -if ($cookie eq "") { - srand(time+$$+unpack("L",`cat $vncUserDir/passwd`)); - for (1..16) { - $cookie .= sprintf("%02x", int(rand(256)) % 256); - } -} - -open(XAUTH, "|xauth -f $xauthorityFile source -"); -print XAUTH "add $host:$displayNumber . $cookie\n"; -print XAUTH "add $host/unix:$displayNumber . $cookie\n"; -close(XAUTH); - -# Now start the X VNC Server +use v5.10; +use warnings; + +sub DEVENV() { $ENV{KASMVNC_DEVELOPMENT} }; +use if DEVENV, Devel::StackTrace; + +use Time::HiRes qw (sleep); +use Switch; +use File::Basename; +use List::Util qw(first); +use List::MoreUtils qw(any uniq); +use Data::Dumper; +use Try::Tiny; + +use KasmVNC::CliOption; +use KasmVNC::ConfigKey; +use KasmVNC::PatternValidator; +use KasmVNC::EnumValidator; +use KasmVNC::Config; +use KasmVNC::Users; +use KasmVNC::TextOption; +use KasmVNC::TextUI; +use KasmVNC::Utils; +use KasmVNC::Logger; + +use constant { + NO_ARG_VALUE => 0, + REQUIRED_ARG_VALUE => 1, + OPTIONAL_ARG_VALUE => 2 +}; + +InitLogger(); + +CheckWeCanRunInThisEnvironment(); + +DefineFilePathsAndStuff(); + +ParseAndProcessCliOptions(); + +PrepareLoggingAndXvncKillingFramework(); +CreateUserConfigIfNeeded(); +DefineConfigToCLIConversion(); +LoadConfigs(); +ActivateConfigToCLIConversion(); +SetAppSettingsFromConfigAndCli(); +DisableLegacyVncAuth(); +AllowXProgramsToConnectToXvnc(); +EnsureAtLeastOneKasmUserExists(); +ConfigureDeToRun(); + +StartXvncOrExit(); + +PrintLogFilenameAndConfiguredUsersAndStuff(); -# We build up our Xvnc command with options -$cmd = $exedir."Xvnc :$displayNumber"; - -foreach my $k (sort keys %config) { - $cmd .= " -$k $config{$k}"; - delete $default_opts{$k}; # file options take precedence -} - -foreach my $k (sort keys %default_opts) { - $cmd .= " -$k $default_opts{$k}"; -} - -# Add color database stuff here, e.g.: -# $cmd .= " -co /usr/lib/X11/rgb"; - -foreach $arg (@ARGV) { - $cmd .= " " . "edString($arg); -} -$cmd .= " >> " . "edString($desktopLog) . " 2>&1"; - -# Run $cmd and record the process ID. -$pidFile = "$vncUserDir/$host:$displayNumber.pid"; -system("$cmd & echo \$! >$pidFile"); - -# Give Xvnc a chance to start up - -sleep(3); -if ($fontPath ne $defFontPath) { - unless (kill 0, `cat $pidFile`) { - if ($fpArgSpecified) { - warn "\nWARNING: The first attempt to start Xvnc failed, probably because the font\n"; - warn "path you specified using the -fp argument is incorrect. Attempting to\n"; - warn "determine an appropriate font path for this system and restart Xvnc using\n"; - warn "that font path ...\n"; - } else { - warn "\nWARNING: The first attempt to start Xvnc failed, possibly because the font\n"; - warn "catalog is not properly configured. Attempting to determine an appropriate\n"; - warn "font path for this system and restart Xvnc using that font path ...\n"; - } - $cmd =~ s@-fp [^ ]+@@; - $cmd .= " -fp $defFontPath" if ($defFontPath); - system("$cmd & echo \$! >$pidFile"); - sleep(3); - } -} -unless (kill 0, `cat $pidFile`) { - warn "Could not start Xvnc.\n\n"; - unlink $pidFile; - open(LOG, "<$desktopLog"); - while () { print; } - close(LOG); - die "\n"; -} - -warn "\nNew '$desktopName' desktop is $host:$displayNumber\n"; -warn "\nUsername: $vncUserName\n\n"; - -# Create the user's xstartup script if necessary. if (! $skipxstartup) { - if (!(-e "$xstartupFile")) { - warn "Creating default startup script $xstartupFile\n"; - open(XSTARTUP, ">$xstartupFile"); - print XSTARTUP $defaultXStartup; - close(XSTARTUP); - chmod 0755, "$xstartupFile"; - } -} - -# Create the user's config file if necessary. -if (!(-e "$vncUserDir/config")) { - warn "Creating default config $vncUserDir/config\n"; - open(VNCUSERCONFIG, ">$vncUserDir/config"); - print VNCUSERCONFIG $defaultConfig; - close(VNCUSERCONFIG); - chmod 0644, "$vncUserDir/config"; -} - -# Run the X startup script. -if (! $skipxstartup) { - warn "Starting applications specified in $xstartupFile\n"; -} -warn "Log file is $desktopLog\n\n"; - -# If the unix domain socket exists then use that (DISPLAY=:n) otherwise use -# TCP (DISPLAY=host:n) - -if (-e "/tmp/.X11-unix/X$displayNumber" || - -e "/usr/spool/sockets/X11/$displayNumber") -{ - $ENV{DISPLAY}= ":$displayNumber"; -} else { - $ENV{DISPLAY}= "$host:$displayNumber"; + CreateXstartupIfNeeded(); + RunXstartup(); } -$ENV{VNCDESKTOP}= $desktopName; -if ($opt{'-fg'}) { - if (! $skipxstartup) { - system("$xstartupFile >> " . "edString($desktopLog) . " 2>&1"); - } - if (kill 0, `cat $pidFile`) { - $opt{'-kill'} = ':'.$displayNumber; - &Kill(); - } -} else { - if ($opt{'-autokill'}) { - if (! $skipxstartup) { - system("($xstartupFile; $0 -kill :$displayNumber) >> " - . "edString($desktopLog) . " 2>&1 &"); - } - } else { - if (! $skipxstartup) { - system("$xstartupFile >> " . "edString($desktopLog) - . " 2>&1 &"); - } - } -} +PrintBrowserUrl(); exit; @@ -443,128 +93,87 @@ exit; ############################################################################### # -# Populate the global %config hash with settings from a specified -# vncserver configuration file if it exists -# -# Args: 1. file path -# 2. optional boolean flag to enable warning when a previously -# set configuration setting is being overridden -# -sub LoadConfig { - local ($configFile, $warnoverride) = @_; - local ($toggle) = undef; - - if (stat($configFile)) { - if (open(IN, $configFile)) { - while () { - next if /^#/; - if (my ($k, $v) = /^\s*(\w+)\s*=\s*(.+)$/) { - $k = lc($k); # must normalize key case - if ($warnoverride && $config{$k}) { - print("Warning: $configFile is overriding previously defined '$k' to be '$v'\n"); - } - # change username option to basicAuth and add colon as required by Xvnc, password will be taken from file - if ($k = "username") { - next; - } else { - $config{$k} = $v; - } - } elsif ($_ =~ m/^\s*(\S+)/) { - # We can't reasonably warn on override of toggles (e.g. AlwaysShared) - # because it would get crazy to do so. We'd have to check if the - # current config file being loaded defined the logical opposite setting - # (NeverShared vs. AlwaysShared, etc etc). - $toggle = lc($1); # must normalize key case - $config{$toggle} = $k; - } - } - close(IN); - } - } -} - -# -# CheckGeometryAndDepth simply makes sure that the geometry and depth values -# are sensible. +# CheckGeometryAndDepthAreSensible simply makes sure that the geometry and depth +# values are sensible. # -sub CheckGeometryAndDepth +sub CheckGeometryAndDepthAreSensible { - if ($geometry =~ /^(\d+)x(\d+)$/) { - $width = $1; $height = $2; + if ($geometry =~ /^(\d+)x(\d+)$/) { + $width = $1; $height = $2; - if (($width<1) || ($height<1)) { - die "$prog: geometry $geometry is invalid\n"; - } - - $geometry = "${width}x$height"; - } else { - die "$prog: geometry $geometry is invalid\n"; + if (($width<1) || ($height<1)) { + die "$prog: geometry $geometry is invalid\n"; } - if ($depth && (($depth < 8) || ($depth > 32))) { - die "Depth must be between 8 and 32\n"; - } + $geometry = "${width}x$height"; + } else { + die "$prog: geometry $geometry is invalid\n"; + } + + if ($depth && (($depth < 8) || ($depth > 32))) { + die "Depth must be between 8 and 32\n"; + } } # -# GetDisplayNumber gets the lowest available display number. A display number -# n is taken if something is listening on the VNC server port (5900+n) or the -# X server port (6000+n). +# GetLowestAvailableDisplayNumber gets the lowest available display number. A +# display number n is taken if something is listening on the VNC server port +# (5900+n) or the X server port (6000+n). # -sub GetDisplayNumber +sub GetLowestAvailableDisplayNumber { - foreach $n (1..99) { - if (&CheckDisplayNumber($n)) { - return $n+0; # Bruce Mah's workaround for bug in perl 5.005_02 - } + foreach $n (1..99) { + if (CheckVncIsntRunningOnDisplay($n)) { + return $n+0; # Bruce Mah's workaround for bug in perl 5.005_02 } + } - die "$prog: no free display number on $host.\n"; + die "$prog: no free display number on $host.\n"; } # -# CheckDisplayNumber checks if the given display number is available. A +# CheckVncIsntRunningOnDisplay checks if the given display number is available. A # display number n is taken if something is listening on the VNC server port # (5900+n) or the X server port (6000+n). # -sub CheckDisplayNumber +sub CheckVncIsntRunningOnDisplay { - local ($n) = @_; + local ($n) = @_; - socket(S, $AF_INET, $SOCK_STREAM, 0) || die "$prog: socket failed: $!\n"; - eval 'setsockopt(S, &SOL_SOCKET, &SO_REUSEADDR, pack("l", 1))'; - if (!bind(S, pack('S n x12', $AF_INET, 6000 + $n))) { - close(S); - return 0; - } + socket(S, $AF_INET, $SOCK_STREAM, 0) || die "$prog: socket failed: $!\n"; + eval 'setsockopt(S, &SOL_SOCKET, &SO_REUSEADDR, pack("l", 1))'; + if (!bind(S, pack('S n x12', $AF_INET, 6000 + $n))) { close(S); + return 0; + } + close(S); - socket(S, $AF_INET, $SOCK_STREAM, 0) || die "$prog: socket failed: $!\n"; - eval 'setsockopt(S, &SOL_SOCKET, &SO_REUSEADDR, pack("l", 1))'; - if (!bind(S, pack('S n x12', $AF_INET, 5900 + $n))) { - close(S); - return 0; - } + socket(S, $AF_INET, $SOCK_STREAM, 0) || die "$prog: socket failed: $!\n"; + eval 'setsockopt(S, &SOL_SOCKET, &SO_REUSEADDR, pack("l", 1))'; + if (!bind(S, pack('S n x12', $AF_INET, 5900 + $n))) { close(S); + return 0; + } + close(S); - if (-e "/tmp/.X$n-lock") { - warn "\nWarning: $host:$n is taken because of /tmp/.X$n-lock\n"; - warn "Remove this file if there is no X server $host:$n\n"; - return 0; - } + if (-e "/tmp/.X$n-lock") { + $logger->warn("\nWarning: $host:$n is taken because of /tmp/.X$n-lock"); + $logger->warn("Remove this file if there is no X server $host:$n"); + return 0; + } - if (-e "/tmp/.X11-unix/X$n") { - warn "\nWarning: $host:$n is taken because of /tmp/.X11-unix/X$n\n"; - warn "Remove this file if there is no X server $host:$n\n"; - return 0; - } + if (-e "/tmp/.X11-unix/X$n") { + $logger->warn("\nWarning: $host:$n is taken because of /tmp/.X11-unix/X$n"); + $logger->warn("Remove this file if there is no X server $host:$n"); + return 0; + } - return 1; + return 1; } @@ -580,67 +189,67 @@ sub CheckDisplayNumber sub GetXDisplayDefaults { - local (@lines, @matchlines, $width, $height, $defaultVisualId, $i, - $red, $green, $blue); + local (@lines, @matchlines, $width, $height, $defaultVisualId, $i, + $red, $green, $blue); + + $wmDecorationWidth = 4; # a guess at typical size for window manager + $wmDecorationHeight = 24; # decoration size - $wmDecorationWidth = 4; # a guess at typical size for window manager - $wmDecorationHeight = 24; # decoration size + return if (!defined($ENV{DISPLAY})); - return if (!defined($ENV{DISPLAY})); + @lines = `xdpyinfo 2>/dev/null`; - @lines = `xdpyinfo 2>/dev/null`; + return if ($? != 0); - return if ($? != 0); + @matchlines = grep(/dimensions/, @lines); + if (@matchlines) { + ($width, $height) = ($matchlines[0] =~ /(\d+)x(\d+) pixels/); - @matchlines = grep(/dimensions/, @lines); - if (@matchlines) { - ($width, $height) = ($matchlines[0] =~ /(\d+)x(\d+) pixels/); + $width -= $wmDecorationWidth; + $height -= $wmDecorationHeight; - $width -= $wmDecorationWidth; - $height -= $wmDecorationHeight; + $geometry = "${width}x$height"; + } - $geometry = "${width}x$height"; + @matchlines = grep(/default visual id/, @lines); + if (@matchlines) { + ($defaultVisualId) = ($matchlines[0] =~ /id:\s+(\S+)/); + + for ($i = 0; $i < @lines; $i++) { + if ($lines[$i] =~ /^\s*visual id:\s+$defaultVisualId$/) { + if (($lines[$i+1] !~ /TrueColor/) || + ($lines[$i+2] !~ /depth/) || + ($lines[$i+4] !~ /red, green, blue masks/)) + { + return; + } + last; + } } - @matchlines = grep(/default visual id/, @lines); - if (@matchlines) { - ($defaultVisualId) = ($matchlines[0] =~ /id:\s+(\S+)/); - - for ($i = 0; $i < @lines; $i++) { - if ($lines[$i] =~ /^\s*visual id:\s+$defaultVisualId$/) { - if (($lines[$i+1] !~ /TrueColor/) || - ($lines[$i+2] !~ /depth/) || - ($lines[$i+4] !~ /red, green, blue masks/)) - { - return; - } - last; - } - } - - return if ($i >= @lines); - - ($depth) = ($lines[$i+2] =~ /depth:\s+(\d+)/); - ($red,$green,$blue) - = ($lines[$i+4] - =~ /masks:\s+0x([0-9a-f]+), 0x([0-9a-f]+), 0x([0-9a-f]+)/); - - $red = hex($red); - $green = hex($green); - $blue = hex($blue); - - if ($red > $blue) { - $red = int(log($red) / log(2)) - int(log($green) / log(2)); - $green = int(log($green) / log(2)) - int(log($blue) / log(2)); - $blue = int(log($blue) / log(2)) + 1; - $pixelformat = "rgb$red$green$blue"; - } else { - $blue = int(log($blue) / log(2)) - int(log($green) / log(2)); - $green = int(log($green) / log(2)) - int(log($red) / log(2)); - $red = int(log($red) / log(2)) + 1; - $pixelformat = "bgr$blue$green$red"; - } + return if ($i >= @lines); + + ($depth) = ($lines[$i+2] =~ /depth:\s+(\d+)/); + ($red,$green,$blue) + = ($lines[$i+4] + =~ /masks:\s+0x([0-9a-f]+), 0x([0-9a-f]+), 0x([0-9a-f]+)/); + + $red = hex($red); + $green = hex($green); + $blue = hex($blue); + + if ($red > $blue) { + $red = int(log($red) / log(2)) - int(log($green) / log(2)); + $green = int(log($green) / log(2)) - int(log($blue) / log(2)); + $blue = int(log($blue) / log(2)) + 1; + $pixelformat = "rgb$red$green$blue"; + } else { + $blue = int(log($blue) / log(2)) - int(log($green) / log(2)); + $green = int(log($green) / log(2)) - int(log($red) / log(2)); + $red = int(log($red) / log(2)) + 1; + $pixelformat = "bgr$blue$green$red"; } + } } @@ -651,11 +260,11 @@ sub GetXDisplayDefaults sub quotedString { - local ($in) = @_; + local ($in) = @_; - $in =~ s/\'/\'\"\'\"\'/g; + $in =~ s/\'/\'\"\'\"\'/g; - return "'$in'"; + return "'$in'"; } @@ -665,11 +274,11 @@ sub quotedString sub removeSlashes { - local ($in) = @_; + local ($in) = @_; - $in =~ s|/|_|g; + $in =~ s|/|_|g; - return "$in"; + return "$in"; } @@ -679,17 +288,17 @@ sub removeSlashes sub Usage { - die("\nusage: $prog [:] [-name ] [-depth ]\n". - " [-geometry x]\n". - " [-pixelformat rgbNNN|bgrNNN]\n". - " [-fp ]\n". - " [-fg]\n". - " [-autokill]\n". - " [-noxstartup]\n". - " [-xstartup ]\n". - " ...\n\n". - " $prog -kill \n\n". - " $prog -list\n\n"); + die("\nusage: $prog [:] [-desktop ] [-depth ]\n". + " [-geometry x]\n". + " [-pixelformat rgbNNN|bgrNNN]\n". + " [-fp ]\n". + " [-fg]\n". + " [-autokill]\n". + " [-noxstartup]\n". + " [-xstartup ]\n". + " ...\n\n". + " $prog -kill \n\n". + " $prog -list\n\n"); } @@ -699,22 +308,22 @@ sub Usage sub List { - opendir(dir, $vncUserDir); - my @filelist = readdir(dir); - closedir(dir); - print "\nKasmVNC server sessions:\n\n"; - print "X DISPLAY #\tPROCESS ID\n"; - foreach my $file (@filelist) { - if ($file =~ /$host:(\d+)$\.pid/) { - chop($tmp_pid = `cat $vncUserDir/$file`); - if (kill 0, $tmp_pid) { - print ":".$1."\t\t".`cat $vncUserDir/$file`; - } else { - unlink ($vncUserDir . "/" . $file); - } - } + opendir(dirHandle, $vncUserDir); + my @filelist = readdir(dirHandle); + closedir(dirHandle); + print "\nKasmVNC server sessions:\n\n"; + print "X DISPLAY #\tPROCESS ID\n"; + foreach my $file (@filelist) { + if ($file =~ /$host:(\d+).pid/) { + chop($tmp_pid = `cat $vncUserDir/$file`); + if (IsProcessRunning($tmp_pid)) { + print ":".$1."\t\t".`cat $vncUserDir/$file`; + } else { + unlink ($vncUserDir . "/" . $file); + } } - exit 1; + } + exit 1; } @@ -724,180 +333,2396 @@ sub List sub Kill { - $opt{'-kill'} =~ s/(:\d+)\.\d+$/$1/; # e.g. turn :1.0 into :1 - - if ($opt{'-kill'} =~ /^:\d+$/) { - $pidFile = "$vncUserDir/$host$opt{'-kill'}.pid"; - } else { - if ($opt{'-kill'} !~ /^$host:/) { - die "\nCan't tell if $opt{'-kill'} is on $host\n". - "Use -kill : instead\n\n"; - } - $pidFile = "$vncUserDir/$opt{'-kill'}.pid"; + $opt{'-kill'} =~ s/(:\d+)\.\d+$/$1/; # e.g. turn :1.0 into :1 + + if ($opt{'-kill'} =~ /^:\d+$/) { + $pidFile = "$vncUserDir/$host$opt{'-kill'}.pid"; + } else { + if ($opt{'-kill'} !~ /^$host:/) { + die "\nCan't tell if $opt{'-kill'} is on $host\n". + "Use -kill : instead\n\n"; } + $pidFile = "$vncUserDir/$opt{'-kill'}.pid"; + } - if (! -r $pidFile) { - die "\nCan't find file $pidFile\n". - "You'll have to kill the Xvnc process manually\n\n"; - } + if (! -r $pidFile) { + die "\nCan't find file $pidFile\n". + "You'll have to kill the Xvnc process manually\n\n"; + } - $SIG{'HUP'} = 'IGNORE'; - chop($pid = `cat $pidFile`); - warn "Killing Xvnc process ID $pid\n"; + $SIG{'HUP'} = 'IGNORE'; + chop($pid = `cat $pidFile`); + $logger->warn("Killing Xvnc process ID $pid"); + + if (IsProcessRunning($pid)) { + system("kill $pid"); + WaitForTimeLimitOrSubReturningTrue(1, sub { + !IsProcessRunning($pid) + }); + if (IsProcessRunning($pid)) { + print "Xvnc seems to be deadlocked. Kill the process manually and then re-run\n"; + print " ".$0." -kill ".$opt{'-kill'}."\n"; + print "to clean up the socket files.\n"; + exit + } - if (kill 0, $pid) { - system("kill $pid"); - sleep(1); - if (kill 0, $pid) { - print "Xvnc seems to be deadlocked. Kill the process manually and then re-run\n"; - print " ".$0." -kill ".$opt{'-kill'}."\n"; - print "to clean up the socket files.\n"; - exit - } + } else { + $logger->warn("Xvnc process ID $pid already killed"); + $opt{'-kill'} =~ s/://; - } else { - warn "Xvnc process ID $pid already killed\n"; - $opt{'-kill'} =~ s/://; - - if (-e "/tmp/.X11-unix/X$opt{'-kill'}") { - print "Xvnc did not appear to shut down cleanly."; - print " Removing /tmp/.X11-unix/X$opt{'-kill'}\n"; - unlink "/tmp/.X11-unix/X$opt{'-kill'}"; - } - if (-e "/tmp/.X$opt{'-kill'}-lock") { - print "Xvnc did not appear to shut down cleanly."; - print " Removing /tmp/.X$opt{'-kill'}-lock\n"; - unlink "/tmp/.X$opt{'-kill'}-lock"; - } + if (-e "/tmp/.X11-unix/X$opt{'-kill'}") { + print "Xvnc did not appear to shut down cleanly."; + print " Removing /tmp/.X11-unix/X$opt{'-kill'}\n"; + unlink "/tmp/.X11-unix/X$opt{'-kill'}"; } + if (-e "/tmp/.X$opt{'-kill'}-lock") { + print "Xvnc did not appear to shut down cleanly."; + print " Removing /tmp/.X$opt{'-kill'}-lock\n"; + unlink "/tmp/.X$opt{'-kill'}-lock"; + } + } - unlink $pidFile; - exit; + unlink $pidFile; + exit; } # -# ParseOptions takes a list of possible options and a boolean indicating -# whether the option has a value following, and sets up an associative array -# %opt of the values of the options given on the command line. It removes all -# the arguments it uses from @ARGV and returns them in @optArgs. +# ParseOptionsAndRemoveMatchesFromARGV takes a list of possible options. Each +# option has a matching argument, indicating whether the option has a value +# following (can be required or optional), and sets up an associative array %opt +# of the values of the options given on the command line. It removes all the +# arguments it uses from @ARGV and returns them in @optArgs. # -sub ParseOptions +sub ParseOptionsAndRemoveMatchesFromARGV { - local (@optval) = @_; - local ($opt, @opts, %valFollows, @newargs); + local (@optval) = @_; + local ($opt, @opts, %valFollows, @newargs); - while (@optval) { - $opt = shift(@optval); - push(@opts,$opt); - $valFollows{$opt} = shift(@optval); - } + while (@optval) { + $opt = shift(@optval); + push(@opts,$opt); + $valFollows{$opt} = shift(@optval); + } + + @optArgs = (); + %opt = (); + + arg: while (defined($arg = shift(@ARGV))) { + foreach $opt (@opts) { + if ($arg eq $opt) { + push(@optArgs, $arg); + switch($valFollows{$opt}) { + case NO_ARG_VALUE { + $opt{$opt} = 1; + next arg; + } + case REQUIRED_ARG_VALUE { + if (@ARGV == 0) { + Usage(); + } + + $opt{$opt} = shift(@ARGV); + push(@optArgs, $opt{$opt}); + + next arg; + } + case OPTIONAL_ARG_VALUE { + if (scalar @ARGV == 0 || $ARGV[0] =~ /^-/) { + $opt{$opt} = 1; + next arg; + } + + $opt{$opt} = shift(@ARGV); + push(@optArgs, $opt{$opt}); - @optArgs = (); - %opt = (); - - arg: while (defined($arg = shift(@ARGV))) { - foreach $opt (@opts) { - if ($arg eq $opt) { - push(@optArgs, $arg); - if ($valFollows{$opt}) { - if (@ARGV == 0) { - &Usage(); - } - $opt{$opt} = shift(@ARGV); - push(@optArgs, $opt{$opt}); - } else { - $opt{$opt} = 1; - } - next arg; - } - } - push(@newargs,$arg); + next arg; + } + } + } } + push(@newargs,$arg); + } - @ARGV = @newargs; + @ARGV = @newargs; } # Routine to make sure we're operating in a sane environment. -sub SanityCheck +sub CheckRequiredDependenciesArePresent { - local ($cmd); - - # Get the program name - ($prog) = ($0 =~ m|([^/]+)$|); - - # - # Check we have all the commands we'll need on the path. - # - - cmd: - foreach $cmd ("uname","xauth") { - for (split(/:/,$ENV{PATH})) { - if (-x "$_/$cmd") { - next cmd; - } - } - die "$prog: couldn't find \"$cmd\" on your PATH.\n"; + local ($cmd); + + # Get the program name + ($prog) = ($0 =~ m|([^/]+)$|); + + # + # Check we have all the commands we'll need on the path. + # + + cmd: + foreach $cmd ("uname","xauth","hostname","whoami") { + for (split(/:/,$ENV{PATH})) { + if (-x "$_/$cmd") { + next cmd; + } } + die "$prog: couldn't find \"$cmd\" on your PATH.\n"; + } - if($exedir eq "") { - cmd2: - foreach $cmd ("Xvnc","vncpasswd") { - for (split(/:/,$ENV{PATH})) { - if (-x "$_/$cmd") { - next cmd2; - } - } - die "$prog: couldn't find \"$cmd\" on your PATH.\n"; - } + if($exedir eq "") { + cmd2: + foreach $cmd ("Xvnc","vncpasswd") { + for (split(/:/,$ENV{PATH})) { + if (-x "$_/$cmd") { + next cmd2; + } + } + die "$prog: couldn't find \"$cmd\" on your PATH.\n"; } - else { - cmd3: - foreach $cmd ($exedir."Xvnc",$exedir."vncpasswd") { - for (split(/:/,$ENV{PATH})) { - if (-x "$cmd") { - next cmd3; - } - } - die "$prog: couldn't find \"$cmd\".\n"; - } + } + else { + cmd3: + foreach $cmd ($exedir."Xvnc",$exedir."vncpasswd") { + for (split(/:/,$ENV{PATH})) { + if (-x "$cmd") { + next cmd3; + } + } + die "$prog: couldn't find \"$cmd\".\n"; } + } + + if (!defined($ENV{HOME})) { + die "$prog: The HOME environment variable is not set.\n"; + } - if (!defined($ENV{HOME})) { - die "$prog: The HOME environment variable is not set.\n"; + # + # Find socket constants. 'use Socket' is a perl5-ism, so we wrap it in an + # eval, and if it fails we try 'require "sys/socket.ph"'. If this fails, + # we just guess at the values. If you find perl moaning here, just + # hard-code the values of AF_INET and SOCK_STREAM. You can find these out + # for your platform by looking in /usr/include/sys/socket.h and related + # files. + # + + chop($os = `uname`); + chop($osrev = `uname -r`); + + eval 'use Socket'; + if ($@) { + eval 'require "sys/socket.ph"'; + if ($@) { + if (($os eq "SunOS") && ($osrev !~ /^4/)) { + $AF_INET = 2; + $SOCK_STREAM = 2; + } else { + $AF_INET = 2; + $SOCK_STREAM = 1; + } + } else { + $AF_INET = &AF_INET; + $SOCK_STREAM = &SOCK_STREAM; } + } else { + $AF_INET = &AF_INET; + $SOCK_STREAM = &SOCK_STREAM; + } +} - # - # Find socket constants. 'use Socket' is a perl5-ism, so we wrap it in an - # eval, and if it fails we try 'require "sys/socket.ph"'. If this fails, - # we just guess at the values. If you find perl moaning here, just - # hard-code the values of AF_INET and SOCK_STREAM. You can find these out - # for your platform by looking in /usr/include/sys/socket.h and related - # files. - # +sub CheckSslCertReadable { + return if IsDryRun(); - chop($os = `uname`); - chop($osrev = `uname -r`); + CheckUserHasAccessToSslCertOnDebian(); + CheckUserHasAccessToSslCertOnCentOS(); +} - eval 'use Socket'; - if ($@) { - eval 'require "sys/socket.ph"'; - if ($@) { - if (($os eq "SunOS") && ($osrev !~ /^4/)) { - $AF_INET = 2; - $SOCK_STREAM = 2; - } else { - $AF_INET = 2; - $SOCK_STREAM = 1; - } - } else { - $AF_INET = &AF_INET; - $SOCK_STREAM = &SOCK_STREAM; - } +sub IsDebian { + return -f "/etc/debian_version"; +} + +sub CheckUserHasAccessToSslCertOnDebian { + if (!IsDebian()) { + return; + } + + if (DoesCertKeyRequireSslCertGroup()) { + RequireUserToHaveSslCertGroup(); + } else { + RequireSslCertsToBeReadable(); + } +} + +sub RequireSslCertsToBeReadable { + my $certFilename = DerivedValue("network.ssl.pem_certificate"); + my $certKeyFilename = DerivedValue("network.ssl.pem_key"); + + my @unreadableCertFiles = map { -r $_ ? () : $_ } + uniq($certFilename, $certKeyFilename); + return if (scalar @unreadableCertFiles == 0); + + $unreadableCertFiles = join "\n", @unreadableCertFiles; + $logger->warn(<warn(</dev/null 2>&1") == 0; +} + +sub CheckUserHasAccessToSslCertOnCentOS { + if (!IsRpmSystem()) { + return; + } + + if (DoesCertKeyRequireKasmvncCertGroup()) { + RequireUserToHaveKasmvncCertGroup(); + } else { + RequireSslCertsToBeReadable(); + } +} + +sub RequireUserToHaveKasmvncCertGroup { + my $certGroup = 'kasmvnc-cert'; + if (system("groups | grep -qw $certGroup") != 0) { + $logger->warn(<warn("Creating default startup script $xstartupFile"); + open(XSTARTUP, ">$xstartupFile"); + print XSTARTUP $defaultXStartup; + close(XSTARTUP); + chmod 0755, "$xstartupFile"; +} + +sub DetectAndExportDisplay { + # If the unix domain socket exists then use that (DISPLAY=:n) otherwise use + # TCP (DISPLAY=host:n) + + if (-e "/tmp/.X11-unix/X$displayNumber" || + -e "/usr/spool/sockets/X11/$displayNumber") + { + $ENV{DISPLAY}= ":$displayNumber"; + } else { + $ENV{DISPLAY}= "$host:$displayNumber"; + } +} + +sub RunXstartup { + $logger->warn("Starting applications specified in $xstartupFile"); + DetectAndExportDisplay(); + $ENV{VNCDESKTOP}= $desktopName; + + if ($opt{'-fg'}) { + if (! $skipxstartup) { + system("$xstartupFile >> " . quotedString($desktopLog) . " 2>&1"); + } + if (IsXvncRunning()) { + $opt{'-kill'} = ':'.$displayNumber; + Kill(); + } + } else { + if ($opt{'-autokill'}) { + if (! $skipxstartup) { + system("($xstartupFile; $0 -kill :$displayNumber) >> " + . quotedString($desktopLog) . " 2>&1 &"); + } } else { - $AF_INET = &AF_INET; - $SOCK_STREAM = &SOCK_STREAM; + if (! $skipxstartup) { + system("$xstartupFile >> " . quotedString($desktopLog) + . " 2>&1 &"); + } + } + } +} + +sub DetectBinariesDir { + my $result = ""; + my $slashndx = rindex($0, "/"); + + if($slashndx>=0) { + $result = substr($0, 0, $slashndx+1); + } + if ($result =~ m!unix/!) { + $result = "/usr/bin/"; + } + + return $result; +} + +sub DetectFontPath { + if (-d "/etc/X11/fontpath.d") { + $fontPath = "catalogue:/etc/X11/fontpath.d"; + } + + @fontpaths = ('/usr/share/X11/fonts', '/usr/share/fonts', '/usr/share/fonts/X11/'); + if (! -l "/usr/lib/X11") {push(@fontpaths, '/usr/lib/X11/fonts');} + if (! -l "/usr/X11") {push(@fontpaths, '/usr/X11/lib/X11/fonts');} + if (! -l "/usr/X11R6") {push(@fontpaths, '/usr/X11R6/lib/X11/fonts');} + push(@fontpaths, '/usr/share/fonts/default'); + + @fonttypes = ('misc', + '75dpi', + '100dpi', + 'Speedo', + 'Type1'); + + foreach $_fpath (@fontpaths) { + foreach $_ftype (@fonttypes) { + if (-f "$_fpath/$_ftype/fonts.dir") { + if (! -l "$_fpath/$_ftype") { + $defFontPath .= "$_fpath/$_ftype,"; + } + } + } + } + + if ($defFontPath) { + if (substr($defFontPath, -1, 1) eq ',') { + chop $defFontPath; } + } + + if (!defined($fontPath) || $fontPath eq "") { + $fontPath = $defFontPath; + } +} + +sub ProcessCliOptions { + Usage() if ($opt{'-help'} || $opt{'-h'} || $opt{'--help'}); + + Kill() if ($opt{'-kill'}); + + List() if ($opt{'-list'}); + + # Uncomment this line if you want default geometry, depth and pixelformat + # to match the current X display: + # GetXDisplayDefaults(); + + if ($opt{'-geometry'}) { + $geometry = $opt{'-geometry'}; + } + if ($opt{'-noxstartup'}) { + $skipxstartup = 1; + } + if ($opt{'-xstartup'}) { + $xstartupFile = $opt{'-xstartup'}; + } + if ($opt{'-fp'}) { + $fontPath = $opt{'-fp'}; + $fpArgSpecified = 1; + } + if ($opt{'-debug'}) { + $debug = 1; + delete $opt{'-debug'}; + $opt{'-Log'} = '*:stderr:100'; + } + if ($opt{'-config'}) { + @configFiles = split ",", $opt{'-config'}; + delete $opt{'-config'}; + } + $testOutputTopic = $opt{'-test-output-topic'}; +} + +sub CreateDotVncDir { + if (!(-e $vncUserDir)) { + if (!mkdir($vncUserDir,0755)) { + die "$prog: Could not create $vncUserDir.\n"; + } + } +} + +sub DeWasSelectedEarlier { + -e $de_was_selected_file; +} + +sub DeWasSpecifiedOnCommandLine { + defined($opt{'-select-de'}) && $opt{'-select-de'} ne "1"; +} + +sub PromptingForDeWasRequestedOnCommandLine { + return unless defined($opt{'-select-de'}); + + $opt{'-select-de'} == 1; +} + +sub WarnIfShouldPromptForDe { + return unless shouldPromptUserToSelectDe(); + + $logger->warn(<userExists($userToCreate)) { + say "User already exists: \"$userToCreate\""; + next; + } + last; + }; + + $userToCreate; +} + +sub GuideUserToSetupKasmPasswdUser { + my $userToCreate = GuideUserToEnterUserToCreate(); + + unless ($users->addUser($userToCreate, "w")) { + die("\nFailed to setup user \"$userToCreate\"\n"); + } + print("Created user \"$userToCreate\"\n"); +} + + +sub AtLeastOneUserConfigured { + $users->count() > 0; +} + +sub MakeXCookie { + # Make an X server cookie and set up the Xauthority file + # mcookie is a part of util-linux, usually only GNU/Linux systems have it. + my $cookie = `mcookie`; + # Fallback for non GNU/Linux OS - use /dev/urandom on systems that have it, + # otherwise use perl's random number generator, seeded with the sum + # of the current time, our PID and part of the encrypted form of the password. + if ($cookie eq "" && open(URANDOM, '<', '/dev/urandom')) { + my $randata; + if (sysread(URANDOM, $randata, 16) == 16) { + $cookie = unpack 'h*', $randata; + } + close(URANDOM); + } + if ($cookie eq "") { + srand(time+$$+unpack("L",`cat $vncUserDir/passwd`)); + for (1..16) { + $cookie .= sprintf("%02x", int(rand(256)) % 256); + } + } + + return $cookie; +} + +sub SetupXauthorityFile { + my $cookie = MakeXCookie(); + + open(XAUTH, "|xauth -f $xauthorityFile source -"); + print XAUTH "add $host:$displayNumber . $cookie\n"; + print XAUTH "add $host/unix:$displayNumber . $cookie\n"; + close(XAUTH); +} + +sub UserSpecifiedArgsToCmd { + my $cmd = ""; + + foreach my $arg (@ARGV) { + $cmd .= " " . quotedString($arg); + noteXvncOption($arg) if $arg =~ /^-/; + } + + $cmd; +} + +sub ConstructXvncCmd { + my $cmd = $exedir."Xvnc :$displayNumber"; + + $cmd .= UserSpecifiedArgsToCmd(); + $cmd .= SwallowedArgsToCmd(); + $cmd .= ConfigToCmd(); + $cmd .= LegacyModeArgsToCmd(); + + $cmd .= " >> " . quotedString($desktopLog) . " 2>&1"; + + return $cmd; +} + +sub LegacyModeArgsToCmd { + my %legacyOptions = ( + -rfbauth => "$vncUserDir/passwd", + -rfbport => 5901, + -rfbwait => 30000 + ); + my @cmd = (); + + while(my($optionName, $optionArg) = each %legacyOptions) { + next if WasOptionSpecifiedViaCli($optionName); + + my $optionText = "$optionName " . quotedString($optionArg); + push(@cmd, $optionText); + noteXvncOption($optionName); + } + + my $legacyCmd = join " ", @cmd; + " $legacyCmd"; +} + +sub noteXvncOption { + my $optionName = shift; + + $addedXvncOptions{$optionName} = 1; +} + +sub WasOptionSpecifiedViaCli { + my $optionName = shift; + + $addedXvncOptions{$optionName}; +} + +sub SwallowedArgsToCmd { + my @swallowedOptions = qw(-fp -interface -websocketPort -Log); + my @optionsInCliFormat = map { SwallowedOptionToCLI($_) } @swallowedOptions; + " " . join " ", @optionsInCliFormat; +} + +sub SwallowedOptionToCLI { + my $optionName = shift; + + return unless ($opt{$optionName}); + + noteXvncOption($optionName); + "$optionName " . quotedString($opt{$optionName}); +} + +sub StartXvncAndRecordPID { + system("$cmd & echo \$! >$pidFile"); +} + +sub DeleteLogLeftFromPreviousXvncRun { + unlink($desktopLog); +} + +sub StartXvncWithSafeFontPath { + if ($fpArgSpecified) { + $logger->warn("\nWARNING: The first attempt to start Xvnc failed, probably because the font"); + $logger->warn("path you specified using the -fp argument is incorrect. Attempting to"); + $logger->warn("determine an appropriate font path for this system and restart Xvnc using"); + $logger->warn("that font path ..."); + } else { + $logger->warn("\nWARNING: The first attempt to start Xvnc failed, possibly because the font"); + $logger->warn("catalog is not properly configured. Attempting to determine an appropriate"); + $logger->warn("font path for this system and restart Xvnc using that font path ..."); + } + $cmd =~ s@-fp [^ ]+@@; + $cmd .= " -fp $defFontPath" if ($defFontPath); + StartXvncAndRecordPID(); +} + +sub IsXvncRunning { + IsProcessRunning(`cat $pidFile`); +} + +sub WarnUserXvncNotStartedAndExit { + $logger->warn("Could not start Xvnc.\n"); + unlink $pidFile; + open(LOG, "<$desktopLog"); + while () { print; } + close(LOG); + die "\n"; +} + +sub WaitForXvncToRespond { + my $sleepSlice = 0.1; + my $sleptFor = 0; + my $sleepLimit = 3; + + until (IsXvncResponding() || $sleptFor >= $sleepLimit) { + sleep($sleepSlice); + $sleptFor += $sleepSlice; + } +} + +sub IsXvncResponding { + `xdpyinfo -display :$displayNumber >/dev/null 2>&1`; + $? == 0; +} + +sub UsingSafeFontPath { + $fontPath eq $defFontPath +} + +sub CreateUserConfigIfNeeded { + my $configFilename = "$vncUserDir/kasmvnc.yaml"; + if (-e $configFilename) { + return; + } + + $logger->warn("Creating default config $configFilename"); + open(VNCUSERCONFIG, ">$configFilename"); + print VNCUSERCONFIG $defaultConfig; + close(VNCUSERCONFIG); + chmod 0644, "$configFilename"; +} + +sub PrintKasmUsers { + $logger->warn("\nUsers configured:"); + $logger->warn($users->toString()); + $logger->warn(""); +} + +sub CheckWeCanRunInThisEnvironment { + $exedir = DetectBinariesDir(); + CheckRequiredDependenciesArePresent(); +} + +sub DefineFilePathsAndStuff { + # + # Global variables. You may want to configure some of these for + # your site + # + + $geometry = "1024x768"; + + $vncUserDir = "$ENV{HOME}/.vnc"; + $vncUserConfig = "$vncUserDir/config"; + $kasmPasswdFile = "$ENV{HOME}/.kasmpasswd"; + + $selectDeBin = DetectSelectDeBin(); + $de_was_selected_file="$ENV{HOME}/.vnc/.de-was-selected"; + + $KasmVNC::Users::vncPasswdBin = $exedir . "kasmvncpasswd"; + $KasmVNC::Users::logger = $logger; + + $vncSystemConfigDir = "/etc/kasmvnc"; + if ($ENV{KASMVNC_DEVELOPMENT}) { + $vncDefaultsConfig = "/src/unix/kasmvnc_defaults.yaml"; + $vncSystemConfig = "/src/unix/kasmvnc.yaml"; + } else { + $vncDefaultsConfig = "/usr/share/kasmvnc/kasmvnc_defaults.yaml"; + $vncSystemConfig = "$vncSystemConfigDir/kasmvnc.yaml"; + } + + $vncUserConfig = "$ENV{HOME}/.vnc/kasmvnc.yaml"; + @configFiles = ($vncDefaultsConfig, $vncSystemConfig, $vncUserConfig); + $defaultWebsocketPort = 8443; + + $skipxstartup = 0; + $xauthorityFile = $ENV{XAUTHORITY} // "$ENV{HOME}/.Xauthority"; + + $xstartupFile = $vncUserDir . "/xstartup"; + $defaultConfig = <hasKey($_) } @allowedVncModeOptions; +} + +sub DefineConfigToCLIConversion { + $KasmVNC::CliOption::fetchValueSub = \&ConfigValue; + $KasmVNC::ConfigKey::fetchValueSub = \&ConfigValue; + + my $regionValidator = KasmVNC::PatternValidator->new({ + pattern => qr/^(-)?\d+(%)?$/, + errorMessage => "must be an integer or percentage" + }); + my $clipboardSizeValidator = KasmVNC::PatternValidator->new({ + pattern => qr/^(unlimited|\d+)$/, + errorMessage => "must be 'unlimited' or a number" + }); + my $autoNumberValidator = KasmVNC::PatternValidator->new({ + pattern => qr/^(auto|\d+)$/, + errorMessage => "must be 'auto' or a number" + }); + my $secondsValidator = KasmVNC::PatternValidator->new({ + pattern => qr/^(never|\d+)$/, + errorMessage => "must be a number or 'never'" + }); + my $allConfigKeysValidatorSub = sub { + my @allConfigKeys = map { $_->configKeyNames() } @xvncOptions; + + KasmVNC::EnumValidator->new({ + allowedValues => [flatten(@allConfigKeys)] + }) + }; + KasmVNC::CliOption::beforeIsActive(\&limitVncModeOptions); + my $ipv4_regexp = '((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(\.|$)){4}'; + my $ipv6_regexp = '(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))'; + + @xvncOptions = ( + KasmVNC::CliOption->new({ + name => 'geometry', + configKeys => [ + KasmVNC::ConfigKey->new({ + name => "desktop.resolution.width", + type => KasmVNC::ConfigKey::INT + }), + KasmVNC::ConfigKey->new({ + name => "desktop.resolution.height", + type => KasmVNC::ConfigKey::INT + }) + ], + deriveValueSub => sub { + $self = shift; + + my $width = $self->{'desktop.resolution.width'}; + my $height = $self->{'desktop.resolution.height'}; + if (defined($width) && defined($height)) { + return $width . "x" . $height; + } + + $geometry; + } + }), + KasmVNC::CliOption->new({ + name => 'AcceptSetDesktopSize', + configKeys => [ + KasmVNC::ConfigKey->new({ + name => "desktop.allow_resize", + type => KasmVNC::ConfigKey::BOOLEAN + }) + ] + }), + KasmVNC::CliOption->new({ + name => 'depth', + configKeys => [ + KasmVNC::ConfigKey->new({ + name => "desktop.pixel_depth", + validator => KasmVNC::EnumValidator->new({ + allowedValues => [qw(16 24 32)] + }) + }) + ], + deriveValueSub => sub { + my $self = shift; + my $value = $self->configValue(); + + $value || $depth; + } + }), + KasmVNC::CliOption->new({ + name => 'noWebsocket', + configKeys => [ + KasmVNC::ConfigKey->new({ + name => "network.protocol", + validator => KasmVNC::EnumValidator->new({ + allowedValues => [qw(http vnc)] + }) + }) + ], + isActiveSub => sub { + my $self = shift; + my $protocol = $self->{"network.protocol"}; + return unless defined($protocol); + + $protocol eq "vnc"; + }, + deriveValueSub => sub { 1 } + }), + KasmVNC::CliOption->new({ + name => 'websocketPort', + configKeys => [ + KasmVNC::ConfigKey->new({ + name => "network.websocket_port", + validator => KasmVNC::PatternValidator->new({ + pattern => qr/^(auto|\d+)$/, + errorMessage => "must be one a number or 'auto'" + }), + }) + ], + deriveValueSub => sub { + my $self = shift; + + my $value = $self->configValue(); + if ($value eq 'auto' || !defined($value)) { + return GenerateWebsocketPortFromDisplayNumber(); + } + + $value; + } + }), + KasmVNC::CliOption->new({ + name => 'interface', + configKeys => [ + KasmVNC::ConfigKey->new({ + name => "network.interface", + type => KasmVNC::ConfigKey::ANY + }) + ] + }), + KasmVNC::CliOption->new({ + name => 'UseIPv4', + configKeys => [ + KasmVNC::ConfigKey->new({ + name => "network.use_ipv4", + type => KasmVNC::ConfigKey::BOOLEAN + }) + ] + }), + KasmVNC::CliOption->new({ + name => 'UseIPv6', + configKeys => [ + KasmVNC::ConfigKey->new({ + name => "network.use_ipv6", + type => KasmVNC::ConfigKey::BOOLEAN + }) + ] + }), + KasmVNC::CliOption->new({ + name => 'cert', + configKeys => [ + KasmVNC::ConfigKey->new({ + name => "network.ssl.pem_certificate", + type => KasmVNC::ConfigKey::ANY + }) + ] + }), + KasmVNC::CliOption->new({ + name => 'key', + configKeys => [ + KasmVNC::ConfigKey->new({ + name => "network.ssl.pem_key", + type => KasmVNC::ConfigKey::ANY + }) + ] + }), + KasmVNC::CliOption->new({ + name => 'sslOnly', + configKeys => [ + KasmVNC::ConfigKey->new({ + name => "network.ssl.require_ssl", + type => KasmVNC::ConfigKey::BOOLEAN + }) + ] + }), + KasmVNC::CliOption->new({ + name => 'AlwaysShared', + configKeys => [ + KasmVNC::ConfigKey->new({ + name => "user_session.session_type", + validator => KasmVNC::EnumValidator->new({ + allowedValues => [qw(shared exclusive)] + }) + }) + ], + deriveValueSub => sub { + 1; + }, + isActiveSub => sub { + my $self = shift; + my $sessionType = $self->configValue(); + return unless defined($sessionType); + + $sessionType eq "shared"; + } + }), + KasmVNC::CliOption->new({ + name => 'DisconnectClients', + configKeys => [ + KasmVNC::ConfigKey->new({ + name => "user_session.new_session_disconnects_existing_exclusive_session", + type => KasmVNC::ConfigKey::ANY + }) + ], + deriveValueSub => sub { + my $self = shift; + + $self->configValue() eq "true" ? 1 : 0; + } + }), + KasmVNC::CliOption->new({ + name => 'NeverShared', + configKeys => [ + KasmVNC::ConfigKey->new({ + name => "user_session.session_type", + type => KasmVNC::ConfigKey::ANY + }) + ], + deriveValueSub => sub { + 1; + }, + isActiveSub => sub { + my $self = shift; + my $sessionType = $self->configValue(); + return unless defined($sessionType); + + $sessionType eq "exclusive"; + }, + }), + KasmVNC::CliOption->new({ + name => 'QueryConnect', + configKeys => [ + KasmVNC::ConfigKey->new({ + name => "user_session.concurrent_connections_prompt", + type => KasmVNC::ConfigKey::BOOLEAN + }) + ] + }), + KasmVNC::CliOption->new({ + name => 'IdleTimeout', + configKeys => [ + KasmVNC::ConfigKey->new({ + name => "user_session.idle_timeout", + validator => $secondsValidator + }) + ], + deriveValueSub => \&deriveSeconds + }), + KasmVNC::CliOption->new({ + name => 'RemapKeys', + configKeys => [ + KasmVNC::ConfigKey->new({ + name => "keyboard.remap_keys", + validator => KasmVNC::PatternValidator->new({ + pattern => qr/^0x[[:xdigit:]]+->0x[[:xdigit:]]+$/, + errorMessage => "must be in the format 0x->0x" + }), + }) + ] + }), + KasmVNC::CliOption->new({ + name => 'AvoidShiftNumLock', + configKeys => [ + KasmVNC::ConfigKey->new({ + name => "keyboard.ignore_numlock", + type => KasmVNC::ConfigKey::BOOLEAN + }) + ] + }), + KasmVNC::CliOption->new({ + name => 'RawKeyboard', + configKeys => [ + KasmVNC::ConfigKey->new({ + name => "keyboard.raw_keyboard", + type => KasmVNC::ConfigKey::BOOLEAN + }) + ] + }), + KasmVNC::CliOption->new({ + name => 'AcceptPointerEvents', + configKeys => [ + KasmVNC::ConfigKey->new({ + name => "pointer.enabled", + type => KasmVNC::ConfigKey::BOOLEAN + }) + ] + }), + KasmVNC::CliOption->new({ + name => 'Log', + configKeys => [ + KasmVNC::ConfigKey->new({ + name => "logging.log_writer_name", + type => KasmVNC::ConfigKey::ANY + }), + KasmVNC::ConfigKey->new({ + name => "logging.log_dest", + validator => KasmVNC::EnumValidator->new({ + allowedValues => [qw(logfile syslog)] + }) + }), + KasmVNC::ConfigKey->new({ + name => "logging.level", + type => KasmVNC::ConfigKey::INT + }) + ], + deriveValueSub => sub { + my $self = shift; + + my $writerName = $self->{"logging.log_writer_name"}; + if ($writerName eq "all") { + $writerName = "*"; + } + + my $log_dest = $self->{"logging.log_dest"}; + if ($log_dest eq "logfile") { + $log_dest = "stdout"; + } + my $level = $self->{"logging.level"}; + + "$writerName:$log_dest:$level"; + } + }), + KasmVNC::CliOption->new({ + name => 'BlacklistThreshold', + configKeys => [ + KasmVNC::ConfigKey->new({ + name => "security.brute_force_protection.blacklist_threshold", + type => KasmVNC::ConfigKey::INT + }) + ] + }), + KasmVNC::CliOption->new({ + name => 'BlacklistTimeout', + configKeys => [ + KasmVNC::ConfigKey->new({ + name => "security.brute_force_protection.blacklist_timeout", + type => KasmVNC::ConfigKey::INT + }) + ] + }), + KasmVNC::CliOption->new({ + name => 'DLP_Region', + configKeys => [ + KasmVNC::ConfigKey->new({ + name => "data_loss_prevention.visible_region.top", + validator => $regionValidator + }), + KasmVNC::ConfigKey->new({ + name => "data_loss_prevention.visible_region.left", + validator => $regionValidator + }), + KasmVNC::ConfigKey->new({ + name => "data_loss_prevention.visible_region.right", + validator => $regionValidator + }), + KasmVNC::ConfigKey->new({ + name => "data_loss_prevention.visible_region.bottom", + validator => $regionValidator + }), + ], + deriveValueSub => sub { + my $self = shift; + + join ",", ($self->{"data_loss_prevention.visible_region.left"}, + $self->{"data_loss_prevention.visible_region.top"}, + $self->{"data_loss_prevention.visible_region.right"}, + $self->{"data_loss_prevention.visible_region.bottom"} + ); + } + }), + KasmVNC::CliOption->new({ + name => 'DLP_RegionAllowClick', + configKeys => [ + KasmVNC::ConfigKey->new({ + name => "data_loss_prevention.visible_region.concealed_region.allow_click_down", + type => KasmVNC::ConfigKey::BOOLEAN + }) + ] + }), + KasmVNC::CliOption->new({ + name => 'DLP_RegionAllowRelease', + configKeys => [ + KasmVNC::ConfigKey->new({ + name => "data_loss_prevention.visible_region.concealed_region.allow_click_release", + type => KasmVNC::ConfigKey::BOOLEAN + }) + ] + }), + KasmVNC::CliOption->new({ + name => 'DLP_ClipDelay', + configKeys => [ + KasmVNC::ConfigKey->new({ + name => "data_loss_prevention.clipboard.delay_between_operations", + validator => KasmVNC::PatternValidator->new({ + pattern => qr/^(none|\d+)$/, + errorMessage => "must be 'none' or a number in milliseconds" + }), + }) + ], + deriveValueSub => sub { + my $self = shift; + my $value = $self->configValue(); + + if ($value eq "none") { + $value = 0; + } + $value; + } + }), + KasmVNC::CliOption->new({ + name => 'SendCutText', + configKeys => [ + KasmVNC::ConfigKey->new({ + name => "data_loss_prevention.clipboard.server_to_client.enabled", + type => KasmVNC::ConfigKey::BOOLEAN + }) + ] + }), + KasmVNC::CliOption->new({ + name => 'DLP_ClipSendMax', + configKeys => [ + KasmVNC::ConfigKey->new({ + name => "data_loss_prevention.clipboard.server_to_client.size", + validator => $clipboardSizeValidator + }) + ], + deriveValueSub => sub { + my $self = shift; + my $value = $self->configValue(); + + if ($value eq "unlimited") { + $value = 0; + } + $value; + } + }), + KasmVNC::CliOption->new({ + name => 'SendPrimary', + configKeys => [ + KasmVNC::ConfigKey->new({ + name => "data_loss_prevention.clipboard.server_to_client.primary_clipboard_enabled", + type => KasmVNC::ConfigKey::BOOLEAN + }) + ] + }), + KasmVNC::CliOption->new({ + name => 'AcceptCutText', + configKeys => [ + KasmVNC::ConfigKey->new({ + name => "data_loss_prevention.clipboard.client_to_server.enabled", + type => KasmVNC::ConfigKey::BOOLEAN + }) + ] + }), + KasmVNC::CliOption->new({ + name => 'DLP_ClipAcceptMax', + configKeys => [ + KasmVNC::ConfigKey->new({ + name => "data_loss_prevention.clipboard.client_to_server.size", + validator => $clipboardSizeValidator + }) + ], + deriveValueSub => sub { + my $self = shift; + my $value = $self->configValue(); + + if ($value eq "unlimited") { + $value = 0; + } + $value; + } + }), + KasmVNC::CliOption->new({ + name => 'AcceptKeyEvents', + configKeys => [ + KasmVNC::ConfigKey->new({ + name => "data_loss_prevention.keyboard.enabled", + type => KasmVNC::ConfigKey::BOOLEAN + }) + ] + }), + KasmVNC::CliOption->new({ + name => 'DLP_KeyRateLimit', + configKeys => [ + KasmVNC::ConfigKey->new({ + name => "data_loss_prevention.keyboard.rate_limit", + validator => $clipboardSizeValidator + }) + ], + deriveValueSub => sub { + my $self = shift; + my $value = $self->configValue(); + + if ($value eq "unlimited") { + $value = 0; + } + $value; + } + }), + KasmVNC::CliOption->new({ + name => 'DLP_Log', + configKeys => [ + KasmVNC::ConfigKey->new({ + name => "data_loss_prevention.logging.level", + validator => KasmVNC::EnumValidator->new({ + allowedValues => [qw(off info verbose)] + }) + }) + ] + }), + KasmVNC::CliOption->new({ + name => 'FrameRate', + configKeys => [ + KasmVNC::ConfigKey->new({ + name => "encoding.max_frame_rate", + type => KasmVNC::ConfigKey::INT + }) + ] + }), + KasmVNC::CliOption->new({ + name => 'DynamicQualityMin', + configKeys => [ + KasmVNC::ConfigKey->new({ + name => "encoding.rect_encoding_mode.min_quality", + type => KasmVNC::ConfigKey::INT + }) + ] + }), + KasmVNC::CliOption->new({ + name => 'DynamicQualityMax', + configKeys => [ + KasmVNC::ConfigKey->new({ + name => "encoding.rect_encoding_mode.max_quality", + type => KasmVNC::ConfigKey::INT + }) + ] + }), + KasmVNC::CliOption->new({ + name => 'TreatLossless', + configKeys => [ + KasmVNC::ConfigKey->new({ + name => "encoding.rect_encoding_mode.consider_lossless_quality", + type => KasmVNC::ConfigKey::INT + }) + ] + }), + KasmVNC::CliOption->new({ + name => 'RectThreads', + configKeys => [ + KasmVNC::ConfigKey->new({ + name => "encoding.rect_encoding_mode.rectangle_compress_threads", + validator => KasmVNC::PatternValidator->new({ + pattern => qr/^(auto|\d+)$/, + errorMessage => "must be 'auto' or a number in milliseconds" + }), + }) + ], + deriveValueSub => sub { + my $self = shift; + my $value = $self->configValue(); + + if ($value eq "auto") { + $value = 0; + } + $value; + } + }), + KasmVNC::CliOption->new({ + name => 'JpegVideoQuality', + configKeys => [ + KasmVNC::ConfigKey->new({ + name => "encoding.video_encoding_mode.jpeg_quality", + type => KasmVNC::ConfigKey::INT + }) + ] + }), + KasmVNC::CliOption->new({ + name => 'WebpVideoQuality', + configKeys => [ + KasmVNC::ConfigKey->new({ + name => "encoding.video_encoding_mode.webp_quality", + type => KasmVNC::ConfigKey::INT + }) + ] + }), + KasmVNC::CliOption->new({ + name => 'MaxVideoResolution', + configKeys => [ + KasmVNC::ConfigKey->new({ + name => "encoding.video_encoding_mode.max_resolution.width", + type => KasmVNC::ConfigKey::INT + }), + KasmVNC::ConfigKey->new({ + name => "encoding.video_encoding_mode.max_resolution.height", + type => KasmVNC::ConfigKey::INT + }) + ], + deriveValueSub => sub { + $self = shift; + + $self->{'encoding.video_encoding_mode.max_resolution.width'} . "x" + . $self->{'encoding.video_encoding_mode.max_resolution.height'}; + } + }), + KasmVNC::CliOption->new({ + name => 'VideoTime', + configKeys => [ + KasmVNC::ConfigKey->new({ + name => "encoding.video_encoding_mode.enter_video_encoding_mode.time_threshold", + type => KasmVNC::ConfigKey::INT + }) + ] + }), + KasmVNC::CliOption->new({ + name => 'VideoArea', + configKeys => [ + KasmVNC::ConfigKey->new({ + name => "encoding.video_encoding_mode.enter_video_encoding_mode.area_threshold", + validator => KasmVNC::PatternValidator->new({ + pattern => qr/^(\d+%)$/, + errorMessage => "must be a number, followed by %" + }), + }) + ], + deriveValueSub => sub { + $self = shift; + + my $value = $self->configValue(); + $value =~ s/%$//; + + $value; + } + }), + KasmVNC::CliOption->new({ + name => 'VideoOutTime', + configKeys => [ + KasmVNC::ConfigKey->new({ + name => "encoding.video_encoding_mode.exit_video_encoding_mode.time_threshold", + type => KasmVNC::ConfigKey::INT + }) + ] + }), + KasmVNC::CliOption->new({ + name => 'PrintVideoArea', + configKeys => [ + KasmVNC::ConfigKey->new({ + name => "encoding.video_encoding_mode.logging.level", + validator => KasmVNC::EnumValidator->new({ + allowedValues => [qw(off info)] + }) + }) + ], + deriveValueSub => sub { + $self = shift; + + my $value = $self->configValue(); + switch($value) { + case 'off' { return 0 } + case 'info' { return 1 } + } + + $value; + } + }), + KasmVNC::CliOption->new({ + name => 'VideoScaling', + configKeys => [ + KasmVNC::ConfigKey->new({ + name => "encoding.video_encoding_mode.scaling_algorithm", + validator => KasmVNC::EnumValidator->new({ + allowedValues => [qw(nearest bilinear progressive_bilinear)] + }) + }) + ], + deriveValueSub => sub { + $self = shift; + + my $value = $self->configValue(); + switch($value) { + case 'nearest' { return 0 } + case 'bilinear' { return 1 } + case 'progressive_bilinear' { return 2 } + } + + $value; + } + }), + KasmVNC::CliOption->new({ + name => 'CompareFB', + configKeys => [ + KasmVNC::ConfigKey->new({ + name => "encoding.compare_framebuffer", + validator => KasmVNC::EnumValidator->new({ + allowedValues => [qw(off always auto)] + }) + }) + ], + deriveValueSub => sub { + $self = shift; + + my $value = $self->configValue(); + switch($value) { + case 'off' { return 0 } + case 'always' { return 1 } + case 'auto' { return 2 } + } + + $value; + } + }), + KasmVNC::CliOption->new({ + name => 'ZlibLevel', + configKeys => [ + KasmVNC::ConfigKey->new({ + name => "encoding.zrle_zlib_level", + validator => KasmVNC::PatternValidator->new({ + pattern => qr/^(auto|[0-9])$/, + errorMessage => "must be 'auto' or a number in 0..9" + }), + }) + ], + isActiveSub => sub { + $self = shift; + + my $value = $self->configValue(); + isPresent($value) && $value ne "auto"; + } + }), + KasmVNC::CliOption->new({ + name => 'ImprovedHextile', + configKeys => [ + KasmVNC::ConfigKey->new({ + name => "encoding.hextile_improved_compression", + type => KasmVNC::ConfigKey::BOOLEAN + }) + ] + }), + KasmVNC::CliOption->new({ + name => 'httpd', + configKeys => [ + KasmVNC::ConfigKey->new({ + name => "server.advanced.httpd_directory", + type => KasmVNC::ConfigKey::ANY + }) + ] + }), + KasmVNC::CliOption->new({ + name => 'IgnoreClientSettingsKasm', + configKeys => [ + KasmVNC::ConfigKey->new({ + name => "runtime_configuration.allow_client_to_override_kasm_server_settings", + type => KasmVNC::ConfigKey::BOOLEAN + }) + ], + deriveValueSub => sub { + $self = shift; + + my $value = $self->configValue(); + + switch($value) { + case 'true' { + $value = 0; + } + case 'false' { + $value = 1; + } + } + + $value; + } + }), + KasmVNC::CliOption->new({ + name => 'fp', + configKeys => [ + KasmVNC::ConfigKey->new({ + name => "server.advanced.x_font_path", + type => KasmVNC::ConfigKey::ANY + }) + ], + deriveValueSub => sub { + $self = shift; + + my $value = $self->configValue(); + return $value if isPresent($value) && $value ne 'auto'; + + $fontPath; + }, + isActiveSub => sub { + my $self = shift; + + my $value = $self->configValue(); + return 1 if isPresent($value) && $value ne 'auto'; + + $fontPath; + } + }), + KasmVNC::CliOption->new({ + name => 'KasmPasswordFile', + configKeys => [ + KasmVNC::ConfigKey->new({ + name => "server.advanced.kasm_password_file", + type => KasmVNC::ConfigKey::ANY + }) + ] + }), + KasmVNC::CliOption->new({ + name => 'MaxDisconnectionTime', + configKeys => [ + KasmVNC::ConfigKey->new({ + name => "server.auto_shutdown.no_user_session_timeout", + validator => $secondsValidator + }) + ], + deriveValueSub => \&deriveSeconds + }), + KasmVNC::CliOption->new({ + name => 'MaxConnectionTime', + configKeys => [ + KasmVNC::ConfigKey->new({ + name => "server.auto_shutdown.active_user_session_timeout", + validator => $secondsValidator + }) + ], + deriveValueSub => \&deriveSeconds + }), + KasmVNC::CliOption->new({ + name => 'MaxIdleTime', + configKeys => [ + KasmVNC::ConfigKey->new({ + name => "server.auto_shutdown.inactive_user_session_timeout", + validator => $secondsValidator + }) + ], + deriveValueSub => \&deriveSeconds + }), + KasmVNC::CliOption->new({ + name => 'auth', + configKeys => [ + KasmVNC::ConfigKey->new({ + name => "server.advanced.x_authority_file", + type => KasmVNC::ConfigKey::ANY + }) + ], + deriveValueSub => sub { + $self = shift; + + my $value = $self->configValue(); + return $value if isPresent($value) && $value ne 'auto'; + + $xauthorityFile; + }, + isActiveSub => sub { 1; } + }), + KasmVNC::CliOption->new({ + name => 'desktop', + configKeys => [ + KasmVNC::ConfigKey->new({ + name => "legacy.desktop_name", + type => KasmVNC::ConfigKey::ANY + }) + ], + deriveValueSub => sub { + my $self = shift; + my $value = $self->configValue(); + + if (isBlank($value) || $value eq "default") { + $desktopName = DefaultDesktopName(); + return $desktopName; + } + + $desktopName = $value; + }, + isActiveSub => sub { 1; } + }), + KasmVNC::CliOption->new({ + name => 'AllowOverride', + configKeys => [ + KasmVNC::ConfigKey->new({ + name => "runtime_configuration.allow_override_standard_vnc_server_settings", + type => KasmVNC::ConfigKey::BOOLEAN + }), + KasmVNC::ConfigKey->new({ + name => "runtime_configuration.allow_override_list", + validator => $allConfigKeysValidatorSub + }) + ], + deriveValueSub => sub { + $self = shift; + + my @overrideList = @{ + listify($self->{'runtime_configuration.allow_override_list'}) + }; + + my @cliOptionList = map { cliOptionForConfigKey($_) } @overrideList; + @cliOptionList = map { $_->{name} } @cliOptionList; + + join ",", @cliOptionList; + }, + isActiveSub => sub { + $self = shift; + + my $allowOverride = $self->{'runtime_configuration.allow_override_standard_vnc_server_settings'}; + return unless defined($allowOverride); + + $allowOverride eq "true"; + } + }), + KasmVNC::CliOption->new({ + name => 'DLP_ClipTypes', + configKeys => [ + KasmVNC::ConfigKey->new({ + name => "data_loss_prevention.clipboard.allow_mimetypes", + type => KasmVNC::ConfigKey::ANY + }) + ] + }), + KasmVNC::CliOption->new({ + name => 'QueryConnectTimeout', + configKeys => [ + KasmVNC::ConfigKey->new({ + name => "user_session.concurrent_connections_prompt_timeout", + type => KasmVNC::ConfigKey::INT + }) + ] + }), + KasmVNC::CliOption->new({ + name => 'PublicIP', + configKeys => [ + KasmVNC::ConfigKey->new({ + name => "network.udp.public_ip", + validator => KasmVNC::PatternValidator->new({ + pattern => qr/^(auto|$ipv4_regexp|$ipv6_regexp)$/, + errorMessage => "must be 'auto' or a valid IPv4 or IPv6 address" + }), + }) + ], + isActiveSub => sub { + $self = shift; + + my $value = $self->configValue(); + isPresent($value) && $value ne 'auto'; + } + }), + KasmVNC::CliOption->new({ + name => 'udpFullFrameFrequency', + configKeys => [ + KasmVNC::ConfigKey->new({ + name => "encoding.full_frame_updates", + validator => KasmVNC::PatternValidator->new({ + pattern => qr/^(none|\d+)$/, + errorMessage => "must be 'none' or an integer" + }), + }) + ], + deriveValueSub => sub { + my $self = shift; + my $value = $self->configValue(); + + if ($value eq "none") { + $value = 0; + } + $value; + } + }), + KasmVNC::CliOption->new({ + name => 'udpPort', + configKeys => [ + KasmVNC::ConfigKey->new({ + name => "network.udp.port", + validator => KasmVNC::PatternValidator->new({ + pattern => qr/^(auto|\d+)$/, + errorMessage => "must be 'auto' or an integer" + }), + }) + ], + isActiveSub => sub { + $self = shift; + + my $value = $self->configValue(); + isPresent($value) && $value ne 'auto'; + } + }), + ); + + %cliArgMap = map { ("-" . $_->{name}) => $_ } @xvncOptions; + %configKeyToXvncOptionMap = map { + my $option = $_; + + map { $_->{name} => $option } @{ $option->{configKeys} }; + } @xvncOptions; + # my $xvncDoc = "./Xvnc.md"; + # open(FH, '<', $xvncDoc) or die $!; + + # while(){ + # if (m/\* \*\*-(\w+)/) { + # my $optionName = $1; + # if ($optionName) { + # $optionName = "-$optionName"; + # my $cliOption = $cliArgMap{$optionName}; + # if ($cliOption) { + # my @keys = @{ $cliOption->configKeyNames() }; + # say '### ' . join(", ", @keys); + # } + # } + # } + # print $_; + # } + + # close(FH); + # exit 0; +} + +sub PromptingAllowed { + $appSettings{prompt} +} + +sub PromptingDisabled { + !PromptingAllowed(); +} + +sub cliOptionForConfigKey { + my $configKey = shift; + + my $cliOptionForConfigKey = first { $_->hasKey($configKey) } @xvncOptions; +} + +sub deriveSeconds { + my $self = shift; + + my $value = $self->configValue(); + return 0 if $value eq 'never'; + + $value; +} + +sub deriveFromConfigAndLocalCli { + my $self = shift; + + my $cliOptionName = "-" . $self->{name}; + my $cliOptionValue = $opt{$cliOptionName}; + my $configValue = deriveBoolean($self->configValue()); + + return $configValue unless defined($cliOptionValue); + + $cliOptionValue; +} + +sub ParseAndProcessCliOptions { + my @supportedOptions = ("-geometry",1,"-kill",1,"-help",0,"-h",0,"--help",0,"-fp",1,"-list",0,"-fg",0,"-autokill",0,"-noxstartup",0,"-xstartup",1,"-select-de",OPTIONAL_ARG_VALUE, "-interface", REQUIRED_ARG_VALUE, '-debug', NO_ARG_VALUE, '-websocketPort', REQUIRED_ARG_VALUE, "-dry-run", NO_ARG_VALUE, '-config', REQUIRED_ARG_VALUE, '-test-output-topic', REQUIRED_ARG_VALUE, '-prompt', REQUIRED_ARG_VALUE); + @vncserverOptions = ( + KasmVNC::CliOption->new({ + name => 'prompt', + configKeys => [ + KasmVNC::ConfigKey->new({ + name => "command_line.prompt", + type => KasmVNC::ConfigKey::BOOLEAN + }) + ], + deriveValueSub => \&deriveFromConfigAndLocalCli + }) + ); + ParseOptionsAndRemoveMatchesFromARGV(@supportedOptions); + ProcessCliOptions(); + CheckGeometryAndDepthAreSensible(); + + $displayNumber = DetectDisplayNumberFromCliArgs(); + if (!defined($displayNumber)) { + $displayNumber = GetLowestAvailableDisplayNumber(); + } + CheckCliOptionsForBeingValid(); +} + +sub CheckBrowserHostDefined { + return if IsDryRun(); + + scalar DeduceBrowserHosts() > 0 || \ + die "-interface has no default value and wasn't passed by user"; +} + +sub DefaultDesktopName { + "$host:$displayNumber ($systemUser)"; +} + +sub GenerateWebsocketPortFromDisplayNumber { + $defaultWebsocketPort + $displayNumber; +} + +sub LoadUsers { + $users = KasmVNC::Users->loadFrom($kasmPasswdFile); +} + +sub EnsureAtLeastOneKasmUserExists { + return if IsDryRun(); + + LoadUsers(); + + unless (PromptingAllowed()) { + return unless $users->is_empty(); + + $logger->warn(<reload(); +} + +sub GuideUserToAddWritePermissionsToExistingUser { + my @options = (); + + my @users = sort $users->users(); + foreach my $user (@users) { + my $name = $user->name(); + push(@options, KasmVNC::TextOption->new({ + description => + "Provide user '$name' with write access", + callback => sub { + $users->addPermissions($name, "w"); + say "Added write permissions for user '$name'"; + } + })); + } + push(@options, KasmVNC::TextOption->new({ + description => "Create a new user with write access", + callback => sub { + GuideUserToSetupKasmPasswdUser(); + $users->reload(); + } + })); + push(@options, KasmVNC::TextOption->new({ + description => "Start KasmVNC without a user with write access" + })); + + my $banner = <<"NEEDTOADDWRITEPERMISSIONS"; + +In order to control your desktop, you need a KasmVNC user with write +permissions. Select what action to take: +NEEDTOADDWRITEPERMISSIONS + + my $option = askUserToChooseOption( + banner => $banner, + prompt => 'Provide selection number', + options => \@options, + ); + + &{ $option->callback() }(); +} + +sub AtLeastOneUserWithWriteAccessConfigured { + $users->findByPermissions("w") > 0; +} + +sub ShouldPrintTopic { + my $topic = shift; + + return 1 unless ($testOutputTopic); + return 1 if ($testOutputTopic eq "all"); + + $topic eq $testOutputTopic; +} + +sub SupportedAbsoluteKeys { + my @supportedAbsoluteKeys = + map { $_->configKeyNames() } @allCliOptions; + @supportedAbsoluteKeys = flatten(@supportedAbsoluteKeys); + my %result = map { $_ => 1 } @supportedAbsoluteKeys; + + \%result; +} + +sub SupportedSectionsFromAbsoluteKey { + my $absoluteKey = shift; + my @sections = (); + + return @sections unless ($absoluteKey =~ /\./); + + while ($absoluteKey =~ /\./) { + $absoluteKey =~ s/\.[^\.]+$//; + push @sections, $absoluteKey; + } + push @sections, $absoluteKey; + + @sections; +} + +sub StartXvncOrExit { + $cmd = ConstructXvncCmd(); + CheckForUnsupportedConfigKeys(); + CheckSslCertReadable(); + say $cmd if ($debug || IsDryRun()) && ShouldPrintTopic("xvnc-cmd"); + + exit(0) if IsDryRun(); + + CheckBrowserHostDefined(); + DeleteLogLeftFromPreviousXvncRun(); + StartXvncAndRecordPID(); + WaitForXvncToRespond(); + + if (!IsXvncRunning() && !UsingSafeFontPath()) { + StartXvncWithSafeFontPath(); + WaitForXvncToRespond(); + } + + unless (IsXvncRunning()) { + WarnUserXvncNotStartedAndExit(); + } +} + +sub WaitForTimeLimitOrSubReturningTrue { + my ($timeLimit, $sub) = @_; + my $sleepSlice = 0.05; + my $sleptFor = 0; + + until (&$sub() || $sleptFor >= $timeLimit) { + sleep($sleepSlice); + $sleptFor += $sleepSlice; + } +} + +sub IsProcessRunning { + my $pid = shift; + unless ($pid) { return 0 }; + + kill 0, $pid; +} + +sub DefineLogAndPidFilesForDisplayNumber { + $desktopLog = "$vncUserDir/$host:$displayNumber.log"; + $pidFile = "$vncUserDir/$host:$displayNumber.pid"; +} + +sub PrepareLoggingAndXvncKillingFramework { + CreateDotVncDir(); + DefineLogAndPidFilesForDisplayNumber(); +} + +sub AllowXProgramsToConnectToXvnc { + SetupXauthorityFile(); +} + +sub PrintLogFilenameAndConfiguredUsersAndStuff { + $logger->warn("\nNew '$desktopName' desktop is $host:$displayNumber"); + PrintKasmUsers(); + $logger->warn("Log file is $desktopLog\n"); +} + +sub PrintBrowserUrl { + my $browserUrls = ConstructBrowserUrl(); + $logger->warn("\nPaste this url in your browser:\n$browserUrls"); +} + +sub IsAllInterfaces { + my $interface = shift; + + $interface eq "0.0.0.0"; +} + +sub DeduceBrowserHosts { + my @browserHosts; + my $interface = $opt{"-interface"} || $optFromConfig{"-interface"}; + + if (IsAllInterfaces($interface)) { + @browserHosts = @hostIPs; + } else { + @browserHosts = ($interface); + } + + @browserHosts; +} + +sub ConstructBrowserUrl { + my @browserHosts = DeduceBrowserHosts(); + my $browserPort = $opt{"-websocketPort"} || $optFromConfig{"-websocketPort"}; + + my @urls = map { "https://$_:$browserPort" } @browserHosts; + join "\n", @urls; +} + +sub IsThisSystemBinary { + $0 =~ m!^/usr!; +} + +sub DetectSelectDeBin { + if (IsThisSystemBinary()) { + "/usr/lib/kasmvncserver/select-de.sh"; + } else { + LocalSelectDePath(); + } +} + +sub LocalSelectDePath { + my $dirname = dirname($0); + "$dirname/../builder/startup/deb/select-de.sh"; +} + +sub IsDryRun { + $opt{"-dry-run"}; +} + +sub LoadConfig { + my $filename = shift; + + return if IsConfigOptionalAndNotReadable($filename); + + my $config = KasmVNC::Config->new({ filename => $filename }); + + $config; +} + +sub FailIfConfigNotReadable { + my $config = shift; + + -r $config || die "Couldn't load config: $config"; +} + +sub IsConfigOptionalAndNotReadable { + my $config = shift; + + $config eq $vncUserConfig && ! -r $config; +} + +sub TrimEmptyNodes { + my $config = shift; + my @supportedSections = @{ listify(SupportedSections()) }; + my @sectionsToCheck = reverse sort @supportedSections; + + foreach my $section (@sectionsToCheck) { + if ($config->isEmpty($section)) { + $config->delete($section); + } + } +} + +sub ConfigValue { + my ($absoluteKey, $configRef) = @_; + $configRef ||= $mergedConfig; + + return $configRef->get($absoluteKey); +} + +sub DerivedValue { + my $absoluteKey = shift; + + $configKeyToXvncOptionMap{$absoluteKey}->toValue(); +} + +sub LoadConfigs { + @allCliOptions = (@xvncOptions, @vncserverOptions); + + my @configs = map { LoadConfig $_ } @configFiles; + foreach my $config (@configs) { + TrimEmptyNodes($config); + } + $mergedConfig = KasmVNC::Config::merge(@configs); +} + +sub SupportedSections { + my %supportedAbsoluteKeys = %{ SupportedAbsoluteKeys() }; + my @supportedSections = map { SupportedSectionsFromAbsoluteKey($_) } + (keys %supportedAbsoluteKeys); + @supportedSections = uniq(flatten(@supportedSections)); + + @supportedSections; +} + +sub EmptySectionsDefinedInConfig { + my @supportedSections = @{ listify(SupportedSections()) }; + my %configAbsoluteKeys = %{ ConfigAbsoluteKeys() }; + + my @emptySections = grep($configAbsoluteKeys{$_} && isBlank(ConfigValue($_)), + @supportedSections); + uniq @emptySections; +} + +sub ConfigAbsoluteKeys { + my %configAbsoluteKeys = map { $_ => 1 } + (ConfigToAbsoluteKeyList("", $mergedConfig->{data})); + \%configAbsoluteKeys; +} + +sub CheckForUnsupportedConfigKeys { + my %supportedAbsoluteKeys = %{ SupportedAbsoluteKeys() }; + my @configAbsoluteKeys = ConfigToAbsoluteKeyList("", $mergedConfig->{data}); + my @unsupportedAbsoluteKeys = + grep(!defined($supportedAbsoluteKeys{$_}), @configAbsoluteKeys); + + return if (scalar @unsupportedAbsoluteKeys == 0); + + if (ShouldPrintTopic("validation")) { + $logger->warn("Unsupported config keys found:"); + $logger->warn(join("\n", @unsupportedAbsoluteKeys)); + $logger->warn(); + } + + exit 1; +} + +sub ConstructOptFromConfig{ + my %result; + + foreach my $cliOption (values %cliArgMap) { + my $cliArg = "-$cliOption->{name}"; + next if WasOptionSpecifiedViaCli($cliArg); + + my $optionValue = $cliOption->toValue(); + next unless defined($cliOption->toString()); + + $result{$cliArg} = $optionValue; + } + + \%result; +} + +sub ConfigToCmd { + ValidateConfig(); + %optFromConfig = %{ ConstructOptFromConfig() }; + + my @cmd = map { $cliArgMap{$_}->toString() } (keys %optFromConfig); + " " . join " ", @cmd; +} + +sub ValidateConfig { + foreach my $cliOption (@allCliOptions) { + ValidateCliOption($cliOption); + } +} + +sub ValidateCliOption { + my $cliOption = $_[0]; + + return if ($cliOption->isValid()); + + if (ShouldPrintTopic("validation")) { + $logger->warn("config errors:"); + $logger->warn($cliOption->errorMessages()); + $logger->warn(); + } + + exit 1; +} + +sub ConfigToAbsoluteKeyList { + my $keyPrefix = $_[0]; + my %configPart = %{ $_[1] }; + + my @absoluteKeys; + + foreach my $key (keys %configPart) { + my $absoluteKey; + if ($keyPrefix) { + $absoluteKey = "$keyPrefix.$key"; + } else { + $absoluteKey = $key; + } + + if (!defined($configPart{$key})) { + push @absoluteKeys, $absoluteKey; + next; + } + + if (ref $configPart{$key} ne "HASH") { + push @absoluteKeys, $absoluteKey; + next; + } + + push @absoluteKeys, + ConfigToAbsoluteKeyList($absoluteKey, \% { $configPart{$key} }); + } + + @absoluteKeys; +} + +sub ActivateConfigToCLIConversion { + foreach my $option (@xvncOptions){ + $option->activate(); + } +} + +sub SetAppSettingsFromConfigAndCli { + foreach my $option (@vncserverOptions) { + my $value = $option->deriveValue(); + + $appSettings{$option->{name}} = $value; + } +} + +sub InitLogger { + $logger = KasmVNC::Logger->new(); } diff --git a/unix/vncserver.man b/unix/vncserver.man index aae748d..460ae05 100644 --- a/unix/vncserver.man +++ b/unix/vncserver.man @@ -4,8 +4,6 @@ vncserver \- start or stop a VNC server .SH SYNOPSIS .B vncserver .RI [: display# ] -.RB [ \-name -.IR desktop-name ] .RB [ \-geometry .IR width x height ] .RB [ \-depth @@ -17,7 +15,7 @@ vncserver \- start or stop a VNC server .RB [ \-fg ] .RB [ \-autokill ] .RB [ \-noxstartup ] -.RB [ \-xstartup +.RB [ \-xstartup .IR script ] .RI [ Xvnc-options... ] .br @@ -53,16 +51,15 @@ In addition to the options listed below, any unrecognised options will be passed to Xvnc - see the Xvnc man page, or "Xvnc \-help", for details. .TP -.B \-name \fIdesktop-name\fP -Each VNC desktop has a name which may be displayed by the viewer. The desktop -name defaults to "\fIhost\fP:\fIdisplay#\fP (\fIusername\fP)", but you can -change it with this option. The desktop name option is passed to the xstartup -script via the $VNCDESKTOP environment variable, which allows you to run a -different set of applications depending on the name of the desktop. -. +.B \-select-de [\fIde_name\fP] +Select Desktop Enviromnent to run. Cinnamon, Mate, LXDE, LXQT, KDE, Gnome, XFCE +are supported. If \fIde_name\fP isn't specified, a text UI prompt to select a +Desktop Enviromnent will be shown. +Warning: $HOME/.vnc/xstartup will be overwritten. + .TP .B \-geometry \fIwidth\fPx\fIheight\fP -Specify the size of the VNC desktop to be created. Default is 1024x768. +Specify the size of the VNC desktop to be created. Default is 1024x768. . .TP .B \-depth \fIdepth\fP @@ -138,45 +135,28 @@ Xvnc. This is useful to run full-screen applications. .TP .B \-list Lists all VNC desktops started by vncserver. +.TP +.B \-dry-run +Print full command VNC server would be run with and exit. Won't run VNC server. .SH FILES -Several VNC-related files are found in the directory $HOME/.vnc: +.TP +/etc/kasmvnc/kasmvnc.yaml +System-wide KasmVNC config. By default, all settings are commented out. The +commented out settings are the defaults used. Uncomment if you want to change +them. Otherwise, there's no need. +.TP +$HOME/.vnc/kasmvnc.yaml +An optional user-level server config. Settings here override the system-wide +config. .TP $HOME/.vnc/xstartup A shell script specifying X applications to be run when a VNC desktop is started. If this file does not exist, then vncserver will create a default xstartup script which attempts to launch your chosen window manager. .TP -/etc/kasmvnc/vncserver-config-defaults -The optional system-wide equivalent of $HOME/.vnc/config. If this file exists -and defines options to be passed to Xvnc, they will be used as defaults for -users. The user's $HOME/.vnc/config overrides settings configured in this file. -The overall configuration file load order is: this file, $HOME/.vnc/config, -and then /etc/kasmvnc/vncserver-config-mandatory. None are required to exist. -.TP -/etc/kasmvnc/vncserver-config-mandatory -The optional system-wide equivalent of $HOME/.vnc/config. If this file exists -and defines options to be passed to Xvnc, they will override any of the same -options defined in a user's $HOME/.vnc/config. This file offers a mechanism -to establish some basic form of system-wide policy. WARNING! There is -nothing stopping users from constructing their own vncserver-like script -that calls Xvnc directly to bypass any options defined in -/etc/kasmvnc/vncserver-config-mandatory. Likewise, any CLI arguments passed -to vncserver will override ANY config file setting of the same name. The -overall configuration file load order is: -/etc/kasmvnc/vncserver-config-defaults, $HOME/.vnc/config, and then this file. -None are required to exist. -.TP -$HOME/.vnc/config -An optional server config file wherein options to be passed to Xvnc are listed -to avoid hard-coding them to the physical invocation. List options in this file -one per line. For those requiring an argument, simply separate the option from -the argument with an equal sign, for example: "geometry=2000x1200". Options -without an argument are simply listed as a single word, for example: "localhost" -or "alwaysshared". -.TP -$HOME/.vnc/passwd -The VNC password file. +$HOME/.kasmpasswd +The KasmVNC password file. .TP $HOME/.vnc/\fIhost\fP:\fIdisplay#\fP.log The log file for Xvnc and applications started in xstartup. diff --git a/unix/xserver/hw/vnc/Xvnc.man b/unix/xserver/hw/vnc/Xvnc.man index 54f1361..a24d51f 100644 --- a/unix/xserver/hw/vnc/Xvnc.man +++ b/unix/xserver/hw/vnc/Xvnc.man @@ -1,9 +1,9 @@ .TH Xvnc 1 "" "KasmVNC" "Virtual Network Computing" .SH NAME -Xvnc \- the X VNC server +Xvnc \- the X VNC server .SH SYNOPSIS .B Xvnc -.RI [ options ] +.RI [ options ] .RI : display# .SH DESCRIPTION .B Xvnc @@ -50,7 +50,7 @@ depth 16 is RGB565 and for depth 24 and 32 is RGB888. Listen on interface. By default Xvnc listens on all available interfaces. . .TP -.B \-inetd +.B \-inetd This significantly changes Xvnc's behaviour so that it can be launched from inetd. See the section below on usage with inetd. . @@ -145,11 +145,6 @@ Which port to use for UDP. Default same as websocket. Accept clipboard updates from clients. Default is on. . .TP -.B \-MaxCutText \fIbytes\fP -The maximum size of a clipboard update that will be accepted from a client. -Default is \fB262144\fP. -. -.TP .B \-SendCutText Send clipboard changes to clients. Default is on. .