diff --git a/.gitignore b/.gitignore index 3f92e8f..026ae1e 100644 --- a/.gitignore +++ b/.gitignore @@ -21,6 +21,9 @@ builder/build/ builder/www/ spec/tmp +Benchmark.xml +SelfBench.xml + # Deb building artefacts debian/.debhelper/ debian/files diff --git a/spec/helper/spec_helper.py b/spec/helper/spec_helper.py index c31692e..d746235 100644 --- a/spec/helper/spec_helper.py +++ b/spec/helper/spec_helper.py @@ -1,4 +1,5 @@ import os +import re import sys import shutil import subprocess @@ -15,6 +16,16 @@ config_filename = os.path.join(config_dir, "config.yaml") if os.getenv('KASMVNC_SPEC_DEBUG_OUTPUT'): debug_output = True +def run_vncserver_to_print_xvnc_cli_options(env=os.environ): + return run_cmd(f'vncserver -dry-run -config {config_filename}', env=env) + +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 write_config(config_text): os.makedirs(config_dir, exist_ok=True) diff --git a/spec/vncserver_env_var_to_cli_spec.py b/spec/vncserver_env_var_to_cli_spec.py new file mode 100644 index 0000000..2a73004 --- /dev/null +++ b/spec/vncserver_env_var_to_cli_spec.py @@ -0,0 +1,133 @@ +import os +from mamba import description, context, fcontext, it, fit, _it, 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, pick_cli_option, \ + run_vncserver_to_print_xvnc_cli_options + +with description('Env var config override') as self: + with context("env var override is turned off"): + with it("doesn't override, when setting is defined in config"): + write_config(''' + desktop: + allow_resize: true + server: + allow_environment_variables_to_override_config_settings: false + ''') + env = os.environ.copy() + env["KVNC_DESKTOP_ALLOW_RESIZE"] = "false" + + completed_process = run_vncserver_to_print_xvnc_cli_options(env=env) + cli_option = pick_cli_option('AcceptSetDesktopSize', + completed_process.stdout) + expect(cli_option).to(equal("-AcceptSetDesktopSize '1'")) + + with it("doesn't override, when setting is not defined in config"): + write_config(''' + desktop: + allow_resize: true + ''') + env = os.environ.copy() + env["KVNC_DESKTOP_ALLOW_RESIZE"] = "false" + + completed_process = run_vncserver_to_print_xvnc_cli_options(env=env) + cli_option = pick_cli_option('AcceptSetDesktopSize', + completed_process.stdout) + expect(cli_option).to(equal("-AcceptSetDesktopSize '1'")) + + with context("env var override is turned on"): + with it("converts env var to CLI option"): + write_config(''' + desktop: + allow_resize: true + server: + allow_environment_variables_to_override_config_settings: true + ''') + env = os.environ.copy() + env["KVNC_DESKTOP_ALLOW_RESIZE"] = "false" + + completed_process = run_vncserver_to_print_xvnc_cli_options(env=env) + cli_option = pick_cli_option('AcceptSetDesktopSize', + completed_process.stdout) + expect(cli_option).to(equal("-AcceptSetDesktopSize '0'")) + + with it("produces error message if env var has invalid value"): + write_config(''' + desktop: + allow_resize: true + server: + allow_environment_variables_to_override_config_settings: true + ''') + env = os.environ.copy() + env["KVNC_DESKTOP_ALLOW_RESIZE"] = "none" + + completed_process = run_cmd(f'vncserver -dry-run -test-output-topic validation -config {config_filename}', print_stderr=False, env=env) + expect(completed_process.stderr).to(contain("desktop.allow_resize 'none': must be true or false")) + + with it("produces error message and exits if env var name is unsupported"): + write_config(''' + foo: true + desktop: + allow_resize: true + server: + allow_environment_variables_to_override_config_settings: true + ''') + env = os.environ.copy() + env["KVNC_FOO"] = "none" + + completed_process = run_cmd(f'vncserver -test-output-topic validation -config {config_filename}', print_stderr=False, env=env) + expect(completed_process.stderr).to(contain("Unsupported config env vars found:\nKVNC_FOO")) + + with context("config setting server.allow_environment_variables_to_override_config_settings"): + with it("produces error message if config has invalid value"): + write_config(''' + server: + allow_environment_variables_to_override_config_settings: 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("server.allow_environment_variables_to_override_config_settings 'none': must be true or false")) + + with it("doesn't interpolate env vars into value"): + write_config(''' + server: + allow_environment_variables_to_override_config_settings: ${ALLOW_OVERRIDE} + ''') + + env = os.environ.copy() + env["ALLOW_OVERRIDE"] = "true" + + completed_process = run_cmd(f'vncserver -dry-run -test-output-topic validation -config {config_filename}',\ + print_stderr=False, env=env) + expect(completed_process.stderr).\ + to(contain("server.allow_environment_variables_to_override_config_settings '${ALLOW_OVERRIDE}': must be true or false")) + + with it("doesn't allow to override it with the corresponding env var if set to false"): + write_config(''' + server: + allow_environment_variables_to_override_config_settings: false + ''') + + env = os.environ.copy() + env["KVNC_SERVER_ALLOW_ENVIRONMENT_VARIABLES_TO_OVERRIDE_CONFIG_SETTINGS"] = "${ALLOW_OVERRIDE}" + + completed_process = run_cmd(f'vncserver -dry-run -test-output-topic validation -config {config_filename}',\ + print_stderr=False, env=env) + expect(completed_process.stderr).\ + not_to(contain("server.allow_environment_variables_to_override_config_settings '${ALLOW_OVERRIDE}': must be true or false")) + + with it("doesn't allow to override it with the env var if set to true"): + write_config(''' + server: + allow_environment_variables_to_override_config_settings: true + ''') + + env = os.environ.copy() + env["KVNC_SERVER_ALLOW_ENVIRONMENT_VARIABLES_TO_OVERRIDE_CONFIG_SETTINGS"] = "${ALLOW_OVERRIDE}" + + completed_process = run_cmd(f'vncserver -dry-run -test-output-topic validation -config {config_filename}',\ + print_stderr=False, env=env) + expect(completed_process.stderr).\ + not_to(contain("server.allow_environment_variables_to_override_config_settings '${ALLOW_OVERRIDE}': must be true or false")) diff --git a/spec/vncserver_yaml_to_cli_spec.py b/spec/vncserver_yaml_to_cli_spec.py index 2e8a516..44bf598 100644 --- a/spec/vncserver_yaml_to_cli_spec.py +++ b/spec/vncserver_yaml_to_cli_spec.py @@ -1,48 +1,19 @@ -import os -import re -import shutil -from os.path import expanduser from mamba import description, context, fcontext, it, fit, _it, 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) - + write_config, config_filename, pick_cli_option, \ + run_vncserver_to_print_xvnc_cli_options 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() + completed_process = run_vncserver_to_print_xvnc_cli_options() cli_option = pick_cli_option('AcceptSetDesktopSize', completed_process.stdout) expect(cli_option).to(equal("-AcceptSetDesktopSize '1'")) @@ -52,7 +23,7 @@ with description('YAML to CLI') as self: desktop: allow_resize: false ''') - completed_process = run_vncserver() + completed_process = run_vncserver_to_print_xvnc_cli_options() cli_option = pick_cli_option('AcceptSetDesktopSize', completed_process.stdout) expect(cli_option).to(equal("-AcceptSetDesktopSize '0'")) @@ -63,7 +34,7 @@ with description('YAML to CLI') as self: brute_force_protection: blacklist_threshold: 2 ''') - completed_process = run_vncserver() + completed_process = run_vncserver_to_print_xvnc_cli_options() cli_option = pick_cli_option('BlacklistThreshold', completed_process.stdout) expect(cli_option).to(equal("-BlacklistThreshold '2'")) @@ -74,7 +45,7 @@ with description('YAML to CLI') as self: ssl: pem_certificate: /etc/ssl/certs/ssl-cert-snakeoil.pem ''') - completed_process = run_vncserver() + completed_process = run_vncserver_to_print_xvnc_cli_options() cli_option = pick_cli_option('cert', completed_process.stdout) expect(cli_option).to( @@ -87,7 +58,7 @@ with description('YAML to CLI') as self: - 0x22->0x40 - 0x24->0x40 ''') - completed_process = run_vncserver() + completed_process = run_vncserver_to_print_xvnc_cli_options() cli_option = pick_cli_option('RemapKeys', completed_process.stdout) expect(cli_option).to( @@ -100,7 +71,7 @@ with description('YAML to CLI') as self: server_to_client: size: 20 ''') - completed_process = run_vncserver() + completed_process = run_vncserver_to_print_xvnc_cli_options() cli_option = pick_cli_option('DLP_ClipSendMax', completed_process.stdout) expect(cli_option).to(equal("-DLP_ClipSendMax '20'")) @@ -111,7 +82,7 @@ with description('YAML to CLI') as self: network: websocket_port: auto ''') - completed_process = run_vncserver() + completed_process = run_vncserver_to_print_xvnc_cli_options() cli_option = pick_cli_option('websocketPort', completed_process.stdout) expect(["-websocketPort '8444'", "-websocketPort '8445'"]). \ @@ -122,7 +93,7 @@ with description('YAML to CLI') as self: network: websocket_port: 8555 ''') - completed_process = run_vncserver() + completed_process = run_vncserver_to_print_xvnc_cli_options() cli_option = pick_cli_option('websocketPort', completed_process.stdout) expect(cli_option).to(equal("-websocketPort '8555'")) @@ -130,7 +101,7 @@ with description('YAML to CLI') as self: with it("no key - no CLI option"): write_config(''' ''') - completed_process = run_vncserver() + completed_process = run_vncserver_to_print_xvnc_cli_options() cli_option = pick_cli_option('websocketPort', completed_process.stdout) expect(cli_option).to(equal(None)) @@ -141,7 +112,7 @@ with description('YAML to CLI') as self: network: protocol: http ''') - completed_process = run_vncserver() + completed_process = run_vncserver_to_print_xvnc_cli_options() cli_option = pick_cli_option('noWebsocket', completed_process.stdout) expect(cli_option).to(equal(None)) @@ -151,7 +122,7 @@ with description('YAML to CLI') as self: network: protocol: vnc ''') - completed_process = run_vncserver() + completed_process = run_vncserver_to_print_xvnc_cli_options() cli_option = pick_cli_option('noWebsocket', completed_process.stdout) expect(cli_option).to(equal("-noWebsocket '1'")) @@ -162,7 +133,7 @@ with description('YAML to CLI') as self: advanced: kasm_password_file: ${HOME}/.kasmpasswd ''') - completed_process = run_vncserver() + completed_process = run_vncserver_to_print_xvnc_cli_options() cli_option = pick_cli_option('KasmPasswordFile', completed_process.stdout) expect(cli_option).to(equal("-KasmPasswordFile '/home/docker/.kasmpasswd'")) @@ -174,7 +145,7 @@ with description('YAML to CLI') as self: log_dest: logfile level: 40 ''') - completed_process = run_vncserver() + completed_process = run_vncserver_to_print_xvnc_cli_options() cli_option = pick_cli_option('Log', completed_process.stdout) expect(cli_option).to(equal("-Log '*:stdout:40'")) @@ -188,7 +159,7 @@ with description('YAML to CLI') as self: right: 40% bottom: 40 ''') - completed_process = run_vncserver() + completed_process = run_vncserver_to_print_xvnc_cli_options() cli_option = pick_cli_option('DLP_Region', completed_process.stdout) expect(cli_option).to(equal("-DLP_Region '10,-10,40%,40'")) @@ -200,7 +171,7 @@ with description('YAML to CLI') as self: advanced: x_font_path: auto ''') - completed_process = run_vncserver() + completed_process = run_vncserver_to_print_xvnc_cli_options() cli_option = pick_cli_option('fp', completed_process.stdout) expect(cli_option).to(match(r'/usr/share/fonts')) @@ -208,7 +179,7 @@ with description('YAML to CLI') as self: with it("none specified"): write_config(''' ''') - completed_process = run_vncserver() + completed_process = run_vncserver_to_print_xvnc_cli_options() cli_option = pick_cli_option('fp', completed_process.stdout) expect(cli_option).to(match(r'/usr/share/fonts')) @@ -219,7 +190,7 @@ with description('YAML to CLI') as self: advanced: x_font_path: /src ''') - completed_process = run_vncserver() + completed_process = run_vncserver_to_print_xvnc_cli_options() cli_option = pick_cli_option('fp', completed_process.stdout) expect(cli_option).to(equal("-fp '/src'")) @@ -239,7 +210,7 @@ with description('YAML to CLI') as self: network: interface: 0.0.0.0 ''') - completed_process = run_vncserver() + completed_process = run_vncserver_to_print_xvnc_cli_options() cli_option = pick_cli_option('interface', completed_process.stdout) expect(cli_option).to(equal("-interface '0.0.0.0'")) @@ -263,7 +234,7 @@ with description('YAML to CLI') as self: width: 1024 height: 768 ''') - completed_process = run_vncserver() + completed_process = run_vncserver_to_print_xvnc_cli_options() cli_option = pick_cli_option('geometry', completed_process.stdout) expect(cli_option).to(equal("-geometry '1024x768'")) @@ -275,7 +246,7 @@ with description('YAML to CLI') as self: text: template: "星街すいせい" ''') - completed_process = run_vncserver() + completed_process = run_vncserver_to_print_xvnc_cli_options() cli_option = pick_cli_option('DLP_WatermarkText', completed_process.stdout) expect(cli_option).to(equal("-DLP_WatermarkText '星街すいせい'")) diff --git a/unix/KasmVNC/CliOption.pm b/unix/KasmVNC/CliOption.pm index ae61bb0..4b48728 100644 --- a/unix/KasmVNC/CliOption.pm +++ b/unix/KasmVNC/CliOption.pm @@ -10,10 +10,13 @@ use Data::Dumper; use KasmVNC::DataClumpValidator; use KasmVNC::Utils; +use KasmVNC::SettingValidation; -our $fetchValueSub; -$KasmVNC::CliOption::dataClumpValidator = KasmVNC::DataClumpValidator->new(); -@KasmVNC::CliOption::isActiveCallbacks = (); +our @ISA = ('KasmVNC::SettingValidation'); + +our $ConfigValue; +our $dataClumpValidator = KasmVNC::DataClumpValidator->new(); +our @isActiveCallbacks = (); sub new { my ($class, $args) = @_; @@ -56,20 +59,20 @@ sub activate { sub beforeIsActive { my $callback = shift; - push @KasmVNC::CliOption::isActiveCallbacks, $callback; + push @isActiveCallbacks, $callback; } sub isActiveByCallbacks { my $self = shift; - all { $_->($self) } @KasmVNC::CliOption::isActiveCallbacks; + all { $_->($self) } @isActiveCallbacks; } sub makeKeysWithValuesAccessible { my $self = shift; foreach my $name (@{ $self->configKeyNames() }) { - my $value = $self->fetchValue($name); + my $value = $ConfigValue->($name); $self->{$name} = $value if defined($value); } } @@ -100,39 +103,11 @@ 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} }; + interpolateEnvVars($value); } # private -sub isValid { - my $self = shift; - - $self->validate() unless $self->{validated}; - - $self->isNoErrorsPresent(); -} - sub validate { my $self = shift; @@ -142,22 +117,16 @@ sub validate { $self->{validated} = 1; } -sub isNoErrorsPresent { - my $self = shift; - - scalar @{ $self->{errors} } == 0; -} - sub validateDataClump { my $self = shift; - $KasmVNC::CliOption::dataClumpValidator->validate($self); + $dataClumpValidator->validate($self); } sub configValues { my $self = shift; - map { $self->fetchValue($_->{name}) } @{ $self->{configKeys} }; + map { $ConfigValue->($_->{name}) } @{ $self->{configKeys} }; } sub configValue { @@ -183,22 +152,10 @@ sub hasKey { 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 index 78d49d3..96e5a63 100644 --- a/unix/KasmVNC/Config.pm +++ b/unix/KasmVNC/Config.pm @@ -53,6 +53,14 @@ sub get { $value; } +sub set { + my ($self, $absoluteKey, $value) = @_; + my $path = absoluteKeyToHashPath($absoluteKey); + my $config = $self->{data}; + + eval "\$config$path = \$value"; +} + sub exists { my ($self, $absoluteKey) = @_; my $path = absoluteKeyToHashPath($absoluteKey); diff --git a/unix/KasmVNC/ConfigEnvVars.pm b/unix/KasmVNC/ConfigEnvVars.pm new file mode 100644 index 0000000..cc9abb0 --- /dev/null +++ b/unix/KasmVNC/ConfigEnvVars.pm @@ -0,0 +1,119 @@ +package KasmVNC::ConfigEnvVars; + +use strict; +use warnings; +use v5.10; +use Data::Dumper; + +use Exporter; + +@KasmVNC::ConfigEnvVars::ISA = qw(Exporter); + +our @EXPORT = ( + 'OverrideConfigWithConfigEnvVars', + 'CheckForUnsupportedConfigEnvVars' +); + +use constant ENV_VAR_OVERRIDE_SETTING => "server.allow_environment_variables_to_override_config_settings"; + +our @configKeyOverrideDenylist = ( + ENV_VAR_OVERRIDE_SETTING +); + +our $logger; +our %prefixedEnvVars; +our %envVarAllowlist; + +our $ConfigValue; +our $SetConfigValue; +our $ShouldPrintTopic; +our $SupportedAbsoluteKeys; + +sub IsAllowEnvVarOverride { + my $allowOverride = $ConfigValue->(ENV_VAR_OVERRIDE_SETTING) // "false"; + $allowOverride eq "true"; +} + +sub OverrideConfigWithConfigEnvVars { + return unless IsAllowEnvVarOverride(); + + %prefixedEnvVars = FetchPrefixedEnvVarsFromEnvironment(); + PrepareEnvVarAllowlist(); + + for my $envVarName (sort keys %prefixedEnvVars) { + my $configKey = $envVarAllowlist{$envVarName}; + next unless defined($configKey); + + my $envVarValue = GetEnvVarValue($envVarName); + $logger->debug("Overriding $configKey with $envVarName=$envVarValue"); + $SetConfigValue->($configKey, $envVarValue); + } +} + +sub GetEnvVarValue { + my $envVarName = shift; + + $prefixedEnvVars{$envVarName}; +} + +sub PrepareEnvVarAllowlist { + %envVarAllowlist = (); + my %configKeyOverrideAllowlist = %{ ConfigKeyOverrideAllowlist() }; + + for my $configKey (keys %configKeyOverrideAllowlist) { + my $allowedEnvVarName = ConvertConfigKeyToEnvVarName($configKey); + $envVarAllowlist{$allowedEnvVarName} = $configKey; + } +} + +sub ConfigKeyOverrideAllowlist { + my %configKeyOverrideAllowlist = %{ $SupportedAbsoluteKeys->() }; + delete @configKeyOverrideAllowlist{@configKeyOverrideDenylist}; + + \%configKeyOverrideAllowlist; +} + +sub FetchPrefixedEnvVarsFromEnvironment { + my %prefixedEnvVars = map { $_ => $ENV{$_} } grep { /^KVNC_/ } keys %ENV; + PrintPrefixedEnvVars(); + + %prefixedEnvVars; +} + +sub ConvertConfigKeyToEnvVarName { + my $configKey = shift; + my $envVarName = $configKey; + + $envVarName =~ s/\./_/g; + $envVarName = "KVNC_$envVarName"; + $envVarName = uc $envVarName; + $logger->debug("$configKey -> $envVarName"); + + $envVarName; +} + +sub PrintPrefixedEnvVars { + $logger->debug("Found KVNC_ env vars:"); + for my $envVarName (sort keys %prefixedEnvVars) { + $logger->debug("$envVarName=$prefixedEnvVars{$envVarName}"); + } +} + +sub CheckForUnsupportedConfigEnvVars { + return unless IsAllowEnvVarOverride(); + + my @unsupportedEnvVars = + grep(!defined($envVarAllowlist{$_}), keys %prefixedEnvVars); + + return if (scalar @unsupportedEnvVars == 0); + + if ($ShouldPrintTopic->("validation")) { + $logger->warn("Unsupported config env vars found:"); + $logger->warn(join("\n", @unsupportedEnvVars)); + $logger->warn(); + } + + exit 1; +} + +1; diff --git a/unix/KasmVNC/ConfigKey.pm b/unix/KasmVNC/ConfigKey.pm index b8ff425..a926b6f 100644 --- a/unix/KasmVNC/ConfigKey.pm +++ b/unix/KasmVNC/ConfigKey.pm @@ -8,7 +8,7 @@ use Data::Dumper; use KasmVNC::Utils; -our $fetchValueSub; +our $ConfigValue; use constant { INT => 0, @@ -86,18 +86,12 @@ sub isValueBlank { !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)) }; + my $value = join ", ", @{ listify($ConfigValue->($name)) }; "$name '$value': $staticErrorMessage"; } @@ -117,7 +111,7 @@ sub isValidBoolean { sub value { my $self = shift; - $self->fetchValue($self->{name}); + $ConfigValue->($self->{name}); } our @EXPORT_OK = ('INT', 'STRING', 'BOOLEAN'); diff --git a/unix/KasmVNC/ConfigSetting.pm b/unix/KasmVNC/ConfigSetting.pm new file mode 100644 index 0000000..d0d1166 --- /dev/null +++ b/unix/KasmVNC/ConfigSetting.pm @@ -0,0 +1,57 @@ +package KasmVNC::ConfigSetting; + +use strict; +use warnings; +use v5.10; + +use KasmVNC::SettingValidation; + +our @ISA = ('KasmVNC::SettingValidation'); + +our $ConfigValue; + +sub new { + my ($class, $args) = @_; + my $self = bless { + configKey => $args->{configKey}, + errors => [] + }, $class; +} + +sub toValue { + my $self = shift; + + $self->deriveValue(); +} + +sub deriveValue { + my $self = shift; + + my $configKeyName = $self->{configKey}->{name}; + my $value = $ConfigValue->($configKeyName); + interpolateEnvVars($value); +} + +sub configKeyNames { + my $self = shift; + + [$self->{configKey}->{name}]; +} + +# private + +sub validate { + my $self = shift; + + $self->validateConfigValue(); + + $self->{validated} = 1; +} + +sub validateConfigValue { + my $self = shift; + + $self->{configKey}->validate($self); +} + +1; diff --git a/unix/KasmVNC/SettingValidation.pm b/unix/KasmVNC/SettingValidation.pm new file mode 100644 index 0000000..831a1c9 --- /dev/null +++ b/unix/KasmVNC/SettingValidation.pm @@ -0,0 +1,33 @@ +package KasmVNC::SettingValidation; + +use strict; +use warnings; +use v5.10; + +sub isValid { + my $self = shift; + + $self->validate() unless $self->{validated}; + + $self->isNoErrorsPresent(); +} + +sub errorMessages { + my $self = shift; + + join "\n", @{ $self->{errors} }; +} + +sub isNoErrorsPresent { + my $self = shift; + + scalar @{ $self->{errors} } == 0; +} + +sub addErrorMessage { + my ($self, $errorMessage) = @_; + + push @{ $self->{errors} }, $errorMessage; +} + +1; diff --git a/unix/KasmVNC/Utils.pm b/unix/KasmVNC/Utils.pm index a280150..5ca885b 100644 --- a/unix/KasmVNC/Utils.pm +++ b/unix/KasmVNC/Utils.pm @@ -11,7 +11,7 @@ use Exporter; @KasmVNC::Utils::ISA = qw(Exporter); our @EXPORT = ('listify', 'flatten', 'isBlank', 'isPresent', 'deriveBoolean', - 'printStackTrace'); + 'printStackTrace', 'interpolateEnvVars'); sub listify { # Implementation based on Hyper::Functions @@ -73,4 +73,17 @@ sub containsWideSymbols { $string =~ /[^\x00-\xFF]/; } +sub interpolateEnvVars { + my $value = shift; + + return $value unless defined($value); + + while ($value =~ /\$\{(\w+)\}/) { + my $envValue = $ENV{$1}; + $value =~ s/\Q$&\E/$envValue/; + } + + $value; +} + 1; diff --git a/unix/kasmvnc_defaults.yaml b/unix/kasmvnc_defaults.yaml index 0ed63ce..13aecf1 100644 --- a/unix/kasmvnc_defaults.yaml +++ b/unix/kasmvnc_defaults.yaml @@ -153,6 +153,7 @@ server: no_user_session_timeout: never active_user_session_timeout: never inactive_user_session_timeout: never + allow_environment_variables_to_override_config_settings: false command_line: prompt: true diff --git a/unix/vncserver b/unix/vncserver index 7a9588f..09543e2 100755 --- a/unix/vncserver +++ b/unix/vncserver @@ -43,6 +43,7 @@ use DateTime; use DateTime::TimeZone; use KasmVNC::CliOption; +use KasmVNC::ConfigSetting; use KasmVNC::ConfigKey; use KasmVNC::PatternValidator; use KasmVNC::EnumValidator; @@ -54,6 +55,8 @@ use KasmVNC::TextUI; use KasmVNC::Utils; use KasmVNC::Logger; +use KasmVNC::ConfigEnvVars; + use constant { NO_ARG_VALUE => 0, REQUIRED_ARG_VALUE => 1, @@ -71,9 +74,18 @@ ParseAndProcessCliOptions(); PrepareLoggingAndXvncKillingFramework(); CreateUserConfigIfNeeded(); -DefineConfigToCLIConversion(); + +DefinePossibleConfigSettings(); + LoadConfigs(); +OverrideConfigWithConfigEnvVars(); +InterpolateEnvVarsIntoConfigValues(); +ValidateConfigValues(); +CheckForUnsupportedConfigEnvVars(); +CheckForUnsupportedConfigKeys(); + ActivateConfigToCLIConversion(); + SetAppSettingsFromConfigAndCli(); DisableLegacyVncAuth(); AllowXProgramsToConnectToXvnc(); @@ -1178,6 +1190,11 @@ sub DefineFilePathsAndStuff { $KasmVNC::Users::vncPasswdBin = $exedir . "kasmvncpasswd"; $KasmVNC::Users::logger = $logger; $KasmVNC::Config::logger = $logger; + $KasmVNC::ConfigEnvVars::logger = $logger; + $KasmVNC::ConfigEnvVars::SupportedAbsoluteKeys = \&SupportedAbsoluteKeys; + $KasmVNC::ConfigEnvVars::ConfigValue = \&ConfigValue; + $KasmVNC::ConfigEnvVars::SetConfigValue = \&SetConfigValue; + $KasmVNC::ConfigEnvVars::ShouldPrintTopic = \&ShouldPrintTopic; $vncSystemConfigDir = "/etc/kasmvnc"; if ($ENV{KASMVNC_DEVELOPMENT}) { @@ -1227,8 +1244,9 @@ sub limitVncModeOptions { } sub DefineConfigToCLIConversion { - $KasmVNC::CliOption::fetchValueSub = \&ConfigValue; - $KasmVNC::ConfigKey::fetchValueSub = \&ConfigValue; + $KasmVNC::CliOption::ConfigValue = \&ConfigValue; + $KasmVNC::ConfigSetting::ConfigValue = \&ConfigValue; + $KasmVNC::ConfigKey::ConfigValue = \&ConfigValue; my $regionValidator = KasmVNC::PatternValidator->new({ pattern => qr/^(-)?\d+(%)?$/, @@ -2700,7 +2718,7 @@ sub ShouldPrintTopic { sub SupportedAbsoluteKeys { my @supportedAbsoluteKeys = - map { $_->configKeyNames() } @allCliOptions; + map { $_->configKeyNames() } (@allCliOptions, @configSettings); @supportedAbsoluteKeys = flatten(@supportedAbsoluteKeys); my %result = map { $_ => 1 } @supportedAbsoluteKeys; @@ -2724,7 +2742,6 @@ sub SupportedSectionsFromAbsoluteKey { sub StartXvncOrExit { $cmd = ConstructXvncCmd(); - CheckForUnsupportedConfigKeys(); CheckSslCertReadable(); say $cmd if ($debug || IsDryRun()) && ShouldPrintTopic("xvnc-cmd"); @@ -2877,6 +2894,13 @@ sub ConfigValue { return $configRef->get($absoluteKey); } +sub SetConfigValue { + my ($absoluteKey, $value, $configRef) = @_; + $configRef ||= $mergedConfig; + + $configRef->set($absoluteKey, $value); +} + sub DerivedValue { my $absoluteKey = shift; @@ -2951,7 +2975,6 @@ sub ConstructOptFromConfig{ } sub ConfigToCmd { - ValidateConfig(); %optFromConfig = %{ ConstructOptFromConfig() }; my @cmd = map { $cliArgMap{$_}->toString() } (keys %optFromConfig); @@ -2960,20 +2983,20 @@ sub ConfigToCmd { return $cmdStr; } -sub ValidateConfig { - foreach my $cliOption (@allCliOptions) { - ValidateCliOption($cliOption); +sub ValidateConfigValues { + foreach my $setting (@allCliOptions, @configSettings) { + ValidateSetting($setting); } } -sub ValidateCliOption { - my $cliOption = $_[0]; +sub ValidateSetting { + my $setting = $_[0]; - return if ($cliOption->isValid()); + return if ($setting->isValid()); if (ShouldPrintTopic("validation")) { $logger->warn("config errors:"); - $logger->warn($cliOption->errorMessages()); + $logger->warn($setting->errorMessages()); $logger->warn(); } @@ -3033,3 +3056,34 @@ sub InitLogger { sub UseUtfStdio { use open qw( :std :encoding(UTF-8) ); } + +sub DefinePossibleConfigSettings { + DefineConfigToCLIConversion(); + DefineConfigSettings(); +} + +sub DefineConfigSettings { + @configSettings = ( + KasmVNC::ConfigSetting->new({ + configKey => KasmVNC::ConfigKey->new({ + name => "server.allow_environment_variables_to_override_config_settings", + type => KasmVNC::ConfigKey::BOOLEAN + }) + }), + ); +} + +sub InterpolateEnvVarsIntoConfigValues { + my @interpolationDenylist = ( + "server.allow_environment_variables_to_override_config_settings" + ); + my %supportedAbsoluteKeys = %{ SupportedAbsoluteKeys() }; + + delete @supportedAbsoluteKeys{@interpolationDenylist}; + + for my $absoluteKey (keys %supportedAbsoluteKeys) { + my $value = ConfigValue($absoluteKey); + my $interpolatedValue = interpolateEnvVars($value); + SetConfigValue($absoluteKey, $interpolatedValue); + } +}