mirror of
https://github.com/kasmtech/KasmVNC.git
synced 2024-11-21 15:43:28 +01:00
2957 lines
76 KiB
Perl
Executable File
2957 lines
76 KiB
Perl
Executable File
#!/usr/bin/env perl
|
|
#
|
|
# Copyright (C) 2009-2010 D. R. Commander. All Rights Reserved.
|
|
# Copyright (C) 2005-2006 Sun Microsystems, Inc. All Rights Reserved.
|
|
# Copyright (C) 2002-2003 Constantin Kaplinsky. All Rights Reserved.
|
|
# Copyright (C) 2002-2005 RealVNC Ltd.
|
|
# Copyright (C) 1999 AT&T Laboratories Cambridge. All Rights Reserved.
|
|
#
|
|
# This is free software; you can redistribute it and/or modify
|
|
# it under the terms of the GNU General Public License as published by
|
|
# the Free Software Foundation; either version 2 of the License, or
|
|
# (at your option) any later version.
|
|
#
|
|
# This software is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License along
|
|
# with this program; if not, write to the Free Software Foundation, Inc.,
|
|
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
|
#
|
|
|
|
#
|
|
# vncserver - wrapper script to start an X VNC server.
|
|
#
|
|
|
|
use v5.10;
|
|
use warnings;
|
|
use utf8;
|
|
|
|
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 DateTime;
|
|
use DateTime::TimeZone;
|
|
|
|
use KasmVNC::CliOption;
|
|
use KasmVNC::ConfigKey;
|
|
use KasmVNC::PatternValidator;
|
|
use KasmVNC::EnumValidator;
|
|
use KasmVNC::CallbackValidator;
|
|
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
|
|
};
|
|
|
|
UseUtfStdio();
|
|
InitLogger();
|
|
|
|
CheckWeCanRunInThisEnvironment();
|
|
|
|
DefineFilePathsAndStuff();
|
|
|
|
ParseAndProcessCliOptions();
|
|
|
|
PrepareLoggingAndXvncKillingFramework();
|
|
CreateUserConfigIfNeeded();
|
|
DefineConfigToCLIConversion();
|
|
LoadConfigs();
|
|
ActivateConfigToCLIConversion();
|
|
SetAppSettingsFromConfigAndCli();
|
|
DisableLegacyVncAuth();
|
|
AllowXProgramsToConnectToXvnc();
|
|
EnsureAtLeastOneKasmUserExists();
|
|
ConfigureDeToRun();
|
|
|
|
StartXvncOrExit();
|
|
|
|
PrintLogFilenameAndConfiguredUsersAndStuff();
|
|
|
|
if (! $skipxstartup) {
|
|
CreateXstartupIfNeeded();
|
|
RunXstartup();
|
|
}
|
|
|
|
PrintBrowserUrl();
|
|
|
|
exit;
|
|
|
|
###############################################################################
|
|
# Functions
|
|
###############################################################################
|
|
|
|
#
|
|
# CheckGeometryAndDepthAreSensible simply makes sure that the geometry and depth
|
|
# values are sensible.
|
|
#
|
|
|
|
sub CheckGeometryAndDepthAreSensible
|
|
{
|
|
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 ($depth && (($depth < 8) || ($depth > 32))) {
|
|
die "Depth must be between 8 and 32\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 GetLowestAvailableDisplayNumber
|
|
{
|
|
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";
|
|
}
|
|
|
|
|
|
#
|
|
# 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 CheckVncIsntRunningOnDisplay
|
|
{
|
|
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;
|
|
}
|
|
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;
|
|
}
|
|
close(S);
|
|
|
|
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") {
|
|
$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;
|
|
}
|
|
|
|
|
|
#
|
|
# GetXDisplayDefaults uses xdpyinfo to find out the geometry, depth and pixel
|
|
# format of the current X display being used. If successful, it sets the
|
|
# options as appropriate so that the X VNC server will use the same settings
|
|
# (minus an allowance for window manager decorations on the geometry). Using
|
|
# the same depth and pixel format means that the VNC server won't have to
|
|
# translate pixels when the desktop is being viewed on this X display (for
|
|
# TrueColor displays anyway).
|
|
#
|
|
|
|
sub GetXDisplayDefaults
|
|
{
|
|
local (@lines, @matchlines, $width, $height, $defaultVisualId, $i,
|
|
$red, $green, $blue);
|
|
|
|
$wmDecorationWidth = 4; # a guess at typical size for window manager
|
|
$wmDecorationHeight = 24; # decoration size
|
|
|
|
return if (!defined($ENV{DISPLAY}));
|
|
|
|
@lines = `xdpyinfo 2>/dev/null`;
|
|
|
|
return if ($? != 0);
|
|
|
|
@matchlines = grep(/dimensions/, @lines);
|
|
if (@matchlines) {
|
|
($width, $height) = ($matchlines[0] =~ /(\d+)x(\d+) pixels/);
|
|
|
|
$width -= $wmDecorationWidth;
|
|
$height -= $wmDecorationHeight;
|
|
|
|
$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;
|
|
}
|
|
}
|
|
|
|
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";
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
#
|
|
# quotedString returns a string which yields the original string when parsed
|
|
# by a shell.
|
|
#
|
|
|
|
sub quotedString
|
|
{
|
|
local ($in) = @_;
|
|
|
|
$in =~ s/\'/\'\"\'\"\'/g;
|
|
|
|
return "'$in'";
|
|
}
|
|
|
|
|
|
#
|
|
# removeSlashes turns slashes into underscores for use as a file name.
|
|
#
|
|
|
|
sub removeSlashes
|
|
{
|
|
local ($in) = @_;
|
|
|
|
$in =~ s|/|_|g;
|
|
|
|
return "$in";
|
|
}
|
|
|
|
|
|
#
|
|
# Usage
|
|
#
|
|
|
|
sub Usage
|
|
{
|
|
die("\nusage: $prog [:<number>] [-desktop <desktop-name>] [-depth <depth>]\n".
|
|
" [-geometry <width>x<height>]\n".
|
|
" [-pixelformat rgbNNN|bgrNNN]\n".
|
|
" [-fp <font-path>]\n".
|
|
" [-fg]\n".
|
|
" [-autokill]\n".
|
|
" [-noxstartup]\n".
|
|
" [-xstartup <file>]\n".
|
|
" <Xvnc-options>...\n\n".
|
|
" $prog -kill <X-display>\n\n".
|
|
" $prog -list\n\n");
|
|
}
|
|
|
|
|
|
#
|
|
# List
|
|
#
|
|
|
|
sub List
|
|
{
|
|
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;
|
|
}
|
|
|
|
|
|
#
|
|
# Kill
|
|
#
|
|
|
|
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 :<number> 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";
|
|
}
|
|
|
|
$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
|
|
}
|
|
|
|
} else {
|
|
$logger->warn("Xvnc process ID $pid already killed");
|
|
$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";
|
|
}
|
|
}
|
|
|
|
unlink $pidFile;
|
|
exit;
|
|
}
|
|
|
|
|
|
#
|
|
# 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 ParseOptionsAndRemoveMatchesFromARGV
|
|
{
|
|
local (@optval) = @_;
|
|
local ($opt, @opts, %valFollows, @newargs);
|
|
|
|
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});
|
|
|
|
next arg;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
push(@newargs,$arg);
|
|
}
|
|
|
|
@ARGV = @newargs;
|
|
}
|
|
|
|
|
|
# Routine to make sure we're operating in a sane environment.
|
|
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","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";
|
|
}
|
|
}
|
|
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";
|
|
}
|
|
|
|
#
|
|
# 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;
|
|
}
|
|
}
|
|
|
|
sub CheckSslCertReadable {
|
|
return if IsDryRun();
|
|
|
|
RequireSslCertsToBeReadable();
|
|
}
|
|
|
|
sub IsDebian {
|
|
return -f "/etc/debian_version";
|
|
}
|
|
|
|
sub RequireSslCertsToBeReadable {
|
|
my $certFilename = DerivedValue("network.ssl.pem_certificate");
|
|
my $certKeyFilename = DerivedValue("network.ssl.pem_key");
|
|
|
|
@certs = ($certFilename, $certKeyFilename);
|
|
@certs = grep defined, @certs;
|
|
@certs = uniq @certs;
|
|
|
|
my @unreadableCertFiles = map { -r $_ ? () : $_ } @certs;
|
|
return if (scalar @unreadableCertFiles == 0);
|
|
|
|
foreach my $unreadableCert (@unreadableCertFiles) {
|
|
GuideUserToMakeCertFileReadable($unreadableCert);
|
|
}
|
|
exit 1;
|
|
}
|
|
|
|
sub FileGroupName {
|
|
my $file = shift;
|
|
my $grpId = (stat($file))[5];
|
|
|
|
getgrgid($grpId);
|
|
}
|
|
|
|
sub AddUserToGroupCmd {
|
|
my $certGroup = shift;
|
|
|
|
if (IsRpmSystem()) {
|
|
"usermod -a -G $certGroup \$USER"
|
|
} else {
|
|
"addgroup \$USER $certGroup"
|
|
}
|
|
}
|
|
|
|
sub GuideUserToMakeCertFileReadable {
|
|
my $certFile = shift;
|
|
if (! -f $certFile) {
|
|
$logger->warn("$certFile: certificate file doesn't exist or isn't a file");
|
|
return;
|
|
}
|
|
|
|
my $certGroup = FileGroupName $certFile;
|
|
my $addUserToGroupCmd = AddUserToGroupCmd $certGroup;
|
|
|
|
$logger->warn(<<EOF);
|
|
$certFile: certificate isn't readable.
|
|
Make the certificate readable by adding your user to group "$certGroup":
|
|
'$addUserToGroupCmd'
|
|
EOF
|
|
}
|
|
|
|
sub IsRpmSystem {
|
|
system("command -v rpm >/dev/null 2>&1") == 0;
|
|
}
|
|
|
|
sub RequireUserToHaveKasmvncCertGroup {
|
|
my $certGroup = 'kasmvnc-cert';
|
|
if (system("groups | grep -qw $certGroup") != 0) {
|
|
$logger->warn(<<EOF);
|
|
Can't access TLS certificate.
|
|
Please add your user to $certGroup via 'usermod -a -G $certGroup \$USER'
|
|
EOF
|
|
exit(1);
|
|
}
|
|
}
|
|
|
|
sub CreateXstartupIfNeeded
|
|
{
|
|
if ((-e "$xstartupFile")) {
|
|
return;
|
|
}
|
|
|
|
my $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");
|
|
|
|
$logger->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;
|
|
|
|
my $xstartupCmd = "{ echo 'Running $xstartupFile'; $xstartupFile; }";
|
|
if ($opt{'-fg'}) {
|
|
if (! $skipxstartup) {
|
|
system("$xstartupCmd >> " . quotedString($desktopLog) . " 2>&1");
|
|
}
|
|
if (IsXvncRunning()) {
|
|
$opt{'-kill'} = ':'.$displayNumber;
|
|
Kill();
|
|
}
|
|
} else {
|
|
if ($opt{'-autokill'}) {
|
|
if (! $skipxstartup) {
|
|
system("($xstartupCmd; $0 -kill :$displayNumber) >> "
|
|
. quotedString($desktopLog) . " 2>&1 &");
|
|
}
|
|
} else {
|
|
if (! $skipxstartup) {
|
|
system("$xstartupCmd >> " . 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(<<WARNING);
|
|
|
|
Warning: the Desktop Environment to run wasn't selected, but prompting to select
|
|
a Desktop Environment was disabled.
|
|
WARNING
|
|
}
|
|
|
|
sub DeWasntSelectedYet() {
|
|
!DeWasSelectedEarlier();
|
|
}
|
|
|
|
sub shouldPromptUserToSelectDe() {
|
|
return 1 if DeWasntSelectedYet();
|
|
|
|
PromptingForDeWasRequestedOnCommandLine();
|
|
}
|
|
|
|
sub SelectDe {
|
|
$selectDeCmd = ConstructSelectDeCmd();
|
|
system($selectDeCmd) == 0 || die("Failed to execute $selectDeCmd\n");
|
|
}
|
|
|
|
sub ConfigureDeToRun {
|
|
if (DeWasSpecifiedOnCommandLine()) {
|
|
SelectDe();
|
|
return;
|
|
}
|
|
|
|
AskUserToChooseDeOrManualXstartup();
|
|
}
|
|
|
|
sub AskUserToChooseDeOrManualXstartup {
|
|
return if IsDryRun();
|
|
|
|
if (PromptingDisabled()) {
|
|
WarnIfShouldPromptForDe();
|
|
return;
|
|
}
|
|
return unless shouldPromptUserToSelectDe();
|
|
|
|
ForgetSelectedDe();
|
|
SelectDe();
|
|
}
|
|
|
|
sub ConstructSelectDeCmd {
|
|
my $cmd = "$selectDeBin";
|
|
my $specifiedDe = $opt{'-select-de'};
|
|
|
|
if ($specifiedDe) {
|
|
$cmd .= " --select-de";
|
|
if ($specifiedDe ne "1") {
|
|
$cmd .= " $specifiedDe";
|
|
}
|
|
}
|
|
|
|
$cmd;
|
|
}
|
|
|
|
sub ForgetSelectedDe {
|
|
unlink $de_was_selected_file;
|
|
}
|
|
|
|
sub DetectDisplayNumberFromCliArgs {
|
|
if (@ARGV == 0) {
|
|
return;
|
|
}
|
|
|
|
my $displayNumber;
|
|
if ($ARGV[0] =~ /^:(\d+)$/) {
|
|
$displayNumber = $1;
|
|
shift(@ARGV);
|
|
if (!CheckVncIsntRunningOnDisplay($displayNumber)) {
|
|
die "A VNC server is already running as :$displayNumber\n";
|
|
}
|
|
}
|
|
|
|
$displayNumber;
|
|
}
|
|
|
|
sub CheckCliOptionsForBeingValid {
|
|
if (@ARGV == 0) {
|
|
return;
|
|
}
|
|
|
|
if (! IsCliOption($ARGV[0])) {
|
|
Usage();
|
|
}
|
|
}
|
|
|
|
sub IsCliOption {
|
|
my $arg = shift;
|
|
|
|
($arg =~ /^-/) || ($arg =~ /^\+/);
|
|
}
|
|
|
|
sub DisableLegacyVncAuth {
|
|
# Disable vnc auth, kasmvnc uses https basic auth
|
|
system("echo 'WrLNwLrcrxM=' | base64 -d > $vncUserDir/passwd");
|
|
}
|
|
|
|
sub TellUserToSetupUserAndPassword {
|
|
if (AtLeastOneUserConfigured()) {
|
|
return;
|
|
}
|
|
|
|
$logger->warn("\nYou need to create a KasmVNC user to access your desktops.\n");
|
|
system($exedir."kasmvncpasswd $kasmPasswdFile");
|
|
if (($? >> 8) != 0) {
|
|
exit 1;
|
|
}
|
|
}
|
|
|
|
sub DefaultKasmUsername {
|
|
my $defaultUsername = $systemUser;
|
|
return if ($users->userExists($defaultUsername));
|
|
|
|
$defaultUsername;
|
|
}
|
|
|
|
sub PromptForUsernameToCreate {
|
|
my $defaultKasmUsername = DefaultKasmUsername();
|
|
my $prompt;
|
|
|
|
if ($defaultKasmUsername) {
|
|
$prompt = "Enter username (default: $defaultKasmUsername): ";
|
|
} else {
|
|
$prompt = "Enter username: ";
|
|
}
|
|
|
|
Prompt($prompt) || $defaultKasmUsername;
|
|
}
|
|
|
|
sub GuideUserToEnterUserToCreate {
|
|
my $userToCreate;
|
|
|
|
print(<<"NEEDTOCREATEUSER");
|
|
|
|
Let's create a user.
|
|
|
|
NEEDTOCREATEUSER
|
|
|
|
while (1) {
|
|
$userToCreate = PromptForUsernameToCreate();
|
|
next if !defined($userToCreate) || $userToCreate =~ /^\s+$/;
|
|
|
|
if ($users->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 (<LOG>) { 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;
|
|
$KasmVNC::Config::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 = <<TEXT;
|
|
logging:
|
|
log_writer_name: all
|
|
log_dest: logfile
|
|
level: 100
|
|
TEXT
|
|
%appSettings = ();
|
|
%addedXvncOptions = ();
|
|
|
|
chop($host = `uname -n`);
|
|
chop($hostIPs = `hostname -i`);
|
|
@hostIPs = split ' ', $hostIPs;
|
|
|
|
chop($systemUser = `whoami`);
|
|
|
|
DetectFontPath();
|
|
}
|
|
|
|
sub limitVncModeOptions {
|
|
my $self = shift;
|
|
my $protocol = ConfigValue("network.protocol");
|
|
|
|
return 1 if isBlank($protocol) || $protocol eq "http";
|
|
|
|
my @allowedVncModeOptions = qw(network.protocol
|
|
server.advanced.x_authority_file legacy.desktop_name
|
|
server.advanced.x_font_path desktop.resolution.width);
|
|
first { $self->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 => 'UnixRelay',
|
|
configKeys => [
|
|
KasmVNC::ConfigKey->new({
|
|
name => "network.unix_relay.name",
|
|
type => KasmVNC::ConfigKey::ANY
|
|
}),
|
|
KasmVNC::ConfigKey->new({
|
|
name => "network.unix_relay.path",
|
|
type => KasmVNC::ConfigKey::ANY
|
|
})
|
|
],
|
|
deriveValueSub => sub {
|
|
my $self = shift;
|
|
|
|
$self->{"network.unix_relay.name"} . ":" . $self->{"network.unix_relay.path"};
|
|
},
|
|
}),
|
|
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<hex_number>->0x<hex_number>"
|
|
}),
|
|
})
|
|
]
|
|
}),
|
|
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_WatermarkImage',
|
|
configKeys => [
|
|
KasmVNC::ConfigKey->new({
|
|
name => "data_loss_prevention.watermark.image",
|
|
type => KasmVNC::ConfigKey::ANY
|
|
})
|
|
]
|
|
}),
|
|
KasmVNC::CliOption->new({
|
|
name => 'DLP_WatermarkLocation',
|
|
configKeys => [
|
|
KasmVNC::ConfigKey->new({
|
|
name => "data_loss_prevention.watermark.location",
|
|
type => KasmVNC::ConfigKey::ANY,
|
|
validator => KasmVNC::PatternValidator->new({
|
|
pattern => qr/^\d+,\d+$/,
|
|
errorMessage => "Must be an x and y offset separated by a comma: 10,10"
|
|
})
|
|
})
|
|
]
|
|
}),
|
|
KasmVNC::CliOption->new({
|
|
name => 'DLP_WatermarkTint',
|
|
configKeys => [
|
|
KasmVNC::ConfigKey->new({
|
|
name => "data_loss_prevention.watermark.tint",
|
|
type => KasmVNC::ConfigKey::ANY,
|
|
validator => KasmVNC::PatternValidator->new({
|
|
pattern => qr/^\d{1,3},\d{1,3},\d{1,3},\d{1,3}$/,
|
|
errorMessage => "Must be RBGA formatted: 255,255,255,128"
|
|
})
|
|
})
|
|
]
|
|
}),
|
|
KasmVNC::CliOption->new({
|
|
name => 'DLP_WatermarkRepeatSpace',
|
|
configKeys => [
|
|
KasmVNC::ConfigKey->new({
|
|
name => "data_loss_prevention.watermark.repeat_spacing",
|
|
type => KasmVNC::ConfigKey::INT
|
|
})
|
|
]
|
|
}),
|
|
KasmVNC::CliOption->new({
|
|
name => 'DLP_WatermarkText',
|
|
configKeys => [
|
|
KasmVNC::ConfigKey->new({
|
|
name => "data_loss_prevention.watermark.text.template",
|
|
validator => KasmVNC::CallbackValidator->new({
|
|
isValidCallback => sub {
|
|
my $value = shift;
|
|
|
|
isBlank(ConfigValue("data_loss_prevention.watermark.image"));
|
|
},
|
|
errorMessage => "Watermark image and text can't be used at the same time"
|
|
}),
|
|
})
|
|
]
|
|
}),
|
|
KasmVNC::CliOption->new({
|
|
name => 'DLP_WatermarkFont',
|
|
configKeys => [
|
|
KasmVNC::ConfigKey->new({
|
|
name => "data_loss_prevention.watermark.text.font",
|
|
type => KasmVNC::ConfigKey::ANY
|
|
})
|
|
],
|
|
isActiveSub => sub {
|
|
$self = shift;
|
|
|
|
my $value = $self->configValue();
|
|
isPresent($value) && $value ne "auto";
|
|
}
|
|
}),
|
|
KasmVNC::CliOption->new({
|
|
name => 'DLP_WatermarkFontSize',
|
|
configKeys => [
|
|
KasmVNC::ConfigKey->new({
|
|
name => "data_loss_prevention.watermark.text.font_size",
|
|
validator => KasmVNC::CallbackValidator->new({
|
|
isValidCallback => sub {
|
|
my $value = shift;
|
|
|
|
return 0 unless $value =~ /^\d+$/;
|
|
|
|
$value >= 8 && $value <= 256;
|
|
},
|
|
errorMessage => "must be in range 8..256"
|
|
}),
|
|
})
|
|
]
|
|
}),
|
|
KasmVNC::CliOption->new({
|
|
name => 'DLP_WatermarkTimeOffsetMinutes',
|
|
configKeys => [
|
|
KasmVNC::ConfigKey->new({
|
|
name => "data_loss_prevention.watermark.text.timezone_name",
|
|
validator => KasmVNC::CallbackValidator->new({
|
|
isValidCallback => sub {
|
|
my $timezone_name = shift;
|
|
|
|
DateTime::TimeZone->is_valid_name($timezone_name);
|
|
},
|
|
errorMessage => "must be a valid timezone name like Australia/Adelaide"
|
|
})
|
|
})
|
|
],
|
|
deriveValueSub => sub {
|
|
my $self = shift;
|
|
my $timezone_name = $self->configValue();
|
|
my $dt = DateTime->now(time_zone => $timezone_name);
|
|
my $offset_in_seconds = $dt->offset();
|
|
|
|
$offset_in_seconds/60;
|
|
}
|
|
}),
|
|
KasmVNC::CliOption->new({
|
|
name => 'DLP_WatermarkTextAngle',
|
|
configKeys => [
|
|
KasmVNC::ConfigKey->new({
|
|
name => "data_loss_prevention.watermark.text.angle",
|
|
validator => KasmVNC::CallbackValidator->new({
|
|
isValidCallback => sub {
|
|
my $value = shift;
|
|
|
|
return 0 unless $value =~ /^-?\d+$/;
|
|
|
|
$value >= -359 && $value <= 359;
|
|
},
|
|
errorMessage => "must be in range -359..359"
|
|
}),
|
|
})
|
|
]
|
|
}),
|
|
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.http.httpd_directory",
|
|
type => KasmVNC::ConfigKey::ANY
|
|
})
|
|
]
|
|
}),
|
|
KasmVNC::CliOption->new({
|
|
name => 'http-header',
|
|
configKeys => [
|
|
KasmVNC::ConfigKey->new({
|
|
name => "server.http.headers",
|
|
type => KasmVNC::ConfigKey::ANY
|
|
})
|
|
],
|
|
toStringSub => sub {
|
|
$self = shift;
|
|
|
|
my @values = @{ listify($self->configValues()) };
|
|
|
|
my $valuesStr = "";
|
|
foreach $value (@values) {
|
|
$valuesStr = $valuesStr . "-http-header '$value' "
|
|
}
|
|
chop($valuesStr);
|
|
return $valuesStr;
|
|
}
|
|
}),
|
|
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';
|
|
}
|
|
}),
|
|
KasmVNC::CliOption->new({
|
|
name => 'StunServer',
|
|
configKeys => [
|
|
KasmVNC::ConfigKey->new({
|
|
name => "network.udp.stun_server",
|
|
validator => KasmVNC::PatternValidator->new({
|
|
pattern => qr/^(auto|\S+)$/,
|
|
errorMessage => "must be 'auto' or an IP address/hostname"
|
|
}),
|
|
})
|
|
],
|
|
isActiveSub => sub {
|
|
$self = shift;
|
|
|
|
my $value = $self->configValue();
|
|
isPresent($value) && $value ne 'auto';
|
|
}
|
|
}),
|
|
KasmVNC::CliOption->new({
|
|
name => 'hw3d',
|
|
configKeys => [
|
|
KasmVNC::ConfigKey->new({
|
|
name => "desktop.gpu.hw3d",
|
|
type => KasmVNC::ConfigKey::BOOLEAN
|
|
})
|
|
],
|
|
toStringSub => sub {
|
|
$self = shift;
|
|
my $value = $self->configValue();
|
|
|
|
switch($value) {
|
|
case 'true' {
|
|
$valuesStr = '-hw3d ';
|
|
}
|
|
case 'false' {
|
|
$valuesStr = ' ';
|
|
}
|
|
}
|
|
|
|
return $valuesStr;
|
|
}
|
|
}),
|
|
KasmVNC::CliOption->new({
|
|
name => 'drinode',
|
|
configKeys => [
|
|
KasmVNC::ConfigKey->new({
|
|
name => "desktop.gpu.drinode",
|
|
type => KasmVNC::ConfigKey::ANY
|
|
})
|
|
]
|
|
}),
|
|
);
|
|
|
|
%cliArgMap = map { ("-" . $_->{name}) => $_ } @xvncOptions;
|
|
%configKeyToXvncOptionMap = map {
|
|
my $option = $_;
|
|
|
|
map { $_->{name} => $option } @{ $option->{configKeys} };
|
|
} @xvncOptions;
|
|
# my $xvncDoc = "./Xvnc.md";
|
|
# open(FH, '<', $xvncDoc) or die $!;
|
|
|
|
# while(<FH>){
|
|
# 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(<<TEXT);
|
|
No users configured and prompting is prohitibed, exiting.
|
|
Use vncpasswd(1) to add a user or enable command_line.prompt in config.
|
|
TEXT
|
|
exit 1;
|
|
}
|
|
|
|
return if AtLeastOneUserWithWriteAccessConfigured();
|
|
|
|
GuideUserToAddWritePermissionsToExistingUser();
|
|
$users->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);
|
|
my $cmdStr = " " . join " ", @cmd;
|
|
|
|
return $cmdStr;
|
|
}
|
|
|
|
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 {
|
|
my $debugEnabled = any { $_ eq "-debug" } @ARGV;
|
|
$logger = KasmVNC::Logger->new({ level => $debugEnabled ? "debug" : "warn" });
|
|
}
|
|
|
|
sub UseUtfStdio {
|
|
use open qw( :std :encoding(UTF-8) );
|
|
}
|