From 813a3628e9a60902ce8ec8364ce0343e8735f3a0 Mon Sep 17 00:00:00 2001 From: Lord Hepipud Date: Wed, 16 Feb 2022 16:36:07 +0100 Subject: [PATCH] Fixes UTF8 encoding for plugin execution --- doc/100-General/10-Changelog.md | 1 + icinga-powershell-framework.psm1 | 4 +- .../Invoke-IcingaInternalServiceCall.psm1 | 50 +------ .../readers/Read-IcingaAgentDebugLogFile.psm1 | 2 +- .../readers/Read-IcingaAgentLogFile.psm1 | 2 +- .../ConvertTo-IcingaPowerShellArguments.psm1 | 133 ++++++++++++++++++ lib/core/tools/ConvertTo-IcingaUTF8Value.psm1 | 50 +++++++ .../plugin/Exit-IcingaExecutePlugin.psm1 | 20 +-- 8 files changed, 206 insertions(+), 56 deletions(-) create mode 100644 lib/core/tools/ConvertTo-IcingaPowerShellArguments.psm1 create mode 100644 lib/core/tools/ConvertTo-IcingaUTF8Value.psm1 diff --git a/doc/100-General/10-Changelog.md b/doc/100-General/10-Changelog.md index 3b1dc7bc..10a55e3c 100644 --- a/doc/100-General/10-Changelog.md +++ b/doc/100-General/10-Changelog.md @@ -54,6 +54,7 @@ Released closed milestones can be found on [GitHub](https://github.com/Icinga/ic * [#478](https://github.com/Icinga/icinga-powershell-framework/pull/478) Fixes connection option "Connecting from parent system" which is not asking for ca.crt path * [#479](https://github.com/Icinga/icinga-powershell-framework/pull/479) Fixes possible exceptions while trying to remove downloaded repository temp files which might still contain a file lock from virusscanners or other tasks * [#480](https://github.com/Icinga/icinga-powershell-framework/pull/480) Fixes service locking during Icinga Agent upgrade and ensures errors on service management are caught and printed with internal error handling +* [#482](https://github.com/Icinga/icinga-powershell-framework/pull/482) Fixes encoding problems with special chars or German umlauts during plugin execution and unescaped whitespace in plugin argument strings like `Icinga for Windows`, which was previously wrongly rended as `Icinga` for example * [#483](https://github.com/Icinga/icinga-powershell-framework/issues/483) Fixes REST-Api SSL certificate lookup from the Icinga Agent, in case a custom hostname was used or in certain domain environments were domain is not matching DNS domain * [#490](https://github.com/Icinga/icinga-powershell-framework/pull/490) Fixes the command `Uninstall-IcingaComponent` for the `service` component which is not doing anything * [#491](https://github.com/Icinga/icinga-powershell-framework/issues/491) Fixes GC collection with `Optimize-IcingaForWindowsMemory` for every incoming REST connection call diff --git a/icinga-powershell-framework.psm1 b/icinga-powershell-framework.psm1 index 8a11df04..bb593150 100644 --- a/icinga-powershell-framework.psm1 +++ b/icinga-powershell-framework.psm1 @@ -108,7 +108,7 @@ function Write-IcingaFrameworkCodeCache() # Load modules from directory Get-ChildItem -Path $directory -Recurse -Filter '*.psm1' | ForEach-Object { - $CacheContent += (Get-Content -Path $_.FullName -Raw); + $CacheContent += (Get-Content -Path $_.FullName -Raw -Encoding 'UTF8'); $CacheContent += "`r`n"; } @@ -122,7 +122,7 @@ function Write-IcingaFrameworkCodeCache() return; } - Set-Content -Path $CacheFile -Value $CacheContent; + Set-Content -Path $CacheFile -Value $CacheContent -Encoding 'UTF8'; Remove-IcingaFrameworkDependencyFile; diff --git a/lib/core/framework/Invoke-IcingaInternalServiceCall.psm1 b/lib/core/framework/Invoke-IcingaInternalServiceCall.psm1 index 684844ad..f83edbc0 100644 --- a/lib/core/framework/Invoke-IcingaInternalServiceCall.psm1 +++ b/lib/core/framework/Invoke-IcingaInternalServiceCall.psm1 @@ -1,9 +1,9 @@ function Invoke-IcingaInternalServiceCall() { param ( - [string]$Command = '', - [array]$Arguments = @(), - [switch]$NoExit = $FALSE + [string]$Command = '', + [hashtable]$Arguments = @{ }, + [switch]$NoExit = $FALSE ); # If our Framework is running as daemon, never call our api @@ -49,48 +49,12 @@ function Invoke-IcingaInternalServiceCall() Set-IcingaTLSVersion; Enable-IcingaUntrustedCertificateValidation -SuppressMessages; - [hashtable]$CommandArguments = @{ }; - [int]$ArgumentIndex = 0; - - # Resolve our array arguments provided by $args and build proper check arguments - while ($ArgumentIndex -lt $Arguments.Count) { - $Value = $Arguments[$ArgumentIndex]; - [string]$Argument = [string]$Value; - $ArgumentValue = $null; - - if ($Argument -eq '-IcingaForWindowsRemoteExecution' -Or $Argument -eq '-IcingaForWindowsJEARemoteExecution') { - $ArgumentIndex += 1; - continue; - } - - if ($Value[0] -eq '-') { - if (($ArgumentIndex + 1) -lt $Arguments.Count) { - [string]$NextValue = $Arguments[$ArgumentIndex + 1]; - if ($NextValue[0] -eq '-') { - $ArgumentValue = $TRUE; - } else { - $ArgumentValue = $Arguments[$ArgumentIndex + 1]; - } - } else { - $ArgumentValue = $TRUE; - } - } else { - $ArgumentIndex += 1; - continue; - } - - $Argument = $Argument.Replace('-', ''); - - $CommandArguments.Add($Argument, $ArgumentValue); - $ArgumentIndex += 1; - } - # Now queue the check inside our REST-Api try { - $ApiResult = Invoke-WebRequest -Method POST -UseBasicParsing -Uri ([string]::Format('https://localhost:{0}/v1/checker?command={1}', $RestApiPort, $Command)) -Body (ConvertTo-JsonUTF8Bytes -InputObject $CommandArguments -Depth 100 -Compress) -ContentType 'application/json' -TimeoutSec $Timeout; + $ApiResult = Invoke-WebRequest -Method POST -UseBasicParsing -Uri ([string]::Format('https://localhost:{0}/v1/checker?command={1}', $RestApiPort, $Command)) -Body (ConvertTo-JsonUTF8Bytes -InputObject $Arguments -Depth 100 -Compress) -ContentType 'application/json' -TimeoutSec $Timeout; } catch { # Fallback to execute plugin locally - Write-IcingaEventMessage -Namespace 'Framework' -EventId 1553 -ExceptionObject $_ -Objects $Command, $CommandArguments; + Write-IcingaEventMessage -Namespace 'Framework' -EventId 1553 -ExceptionObject $_ -Objects $Command, $Arguments; return $NULL; } @@ -100,12 +64,12 @@ function Invoke-IcingaInternalServiceCall() # In case we didn't receive a check result, fallback to local execution if ([string]::IsNullOrEmpty($IcingaResult.$Command.checkresult)) { - Write-IcingaEventMessage -Namespace 'Framework' -EventId 1553 -Objects 'The check result for the executed command was empty', $Command, $CommandArguments; + Write-IcingaEventMessage -Namespace 'Framework' -EventId 1553 -Objects 'The check result for the executed command was empty', $Command, $Arguments; return $NULL; } if ([string]::IsNullOrEmpty($IcingaResult.$Command.exitcode)) { - Write-IcingaEventMessage -Namespace 'Framework' -EventId 1553 -Objects 'The check result for the executed command was empty', $Command, $CommandArguments; + Write-IcingaEventMessage -Namespace 'Framework' -EventId 1553 -Objects 'The check result for the executed command was empty', $Command, $Arguments; return $NULL; } diff --git a/lib/core/icingaagent/readers/Read-IcingaAgentDebugLogFile.psm1 b/lib/core/icingaagent/readers/Read-IcingaAgentDebugLogFile.psm1 index 139dfe6a..cf163ede 100644 --- a/lib/core/icingaagent/readers/Read-IcingaAgentDebugLogFile.psm1 +++ b/lib/core/icingaagent/readers/Read-IcingaAgentDebugLogFile.psm1 @@ -6,5 +6,5 @@ function Read-IcingaAgentDebugLogFile() return; } - Get-Content -Path $Logfile -Tail 20 -Wait; + Get-Content -Path $Logfile -Tail 20 -Wait -Encoding 'UTF8'; } diff --git a/lib/core/icingaagent/readers/Read-IcingaAgentLogFile.psm1 b/lib/core/icingaagent/readers/Read-IcingaAgentLogFile.psm1 index b0fc1adb..0e376a61 100644 --- a/lib/core/icingaagent/readers/Read-IcingaAgentLogFile.psm1 +++ b/lib/core/icingaagent/readers/Read-IcingaAgentLogFile.psm1 @@ -11,6 +11,6 @@ function Read-IcingaAgentLogFile() return; } - Get-Content -Path $Logfile -Tail 20 -Wait; + Get-Content -Path $Logfile -Tail 20 -Wait -Encoding 'UTF8'; } } diff --git a/lib/core/tools/ConvertTo-IcingaPowerShellArguments.psm1 b/lib/core/tools/ConvertTo-IcingaPowerShellArguments.psm1 new file mode 100644 index 00000000..79ccd2a1 --- /dev/null +++ b/lib/core/tools/ConvertTo-IcingaPowerShellArguments.psm1 @@ -0,0 +1,133 @@ +<# +.SYNOPSIS + Fixes the current encoding hell for arguments by taking every argument + parsed from Icinga and converting it from PowerShell native encoding + to UTF8 +.DESCRIPTION + Fixes the current encoding hell for arguments by taking every argument + parsed from Icinga and converting it from PowerShell native encoding + to UTF8 +.PARAMETER Arguments + The array of arguments for re-encoding. By default, this could be $args + for calls from Exit-IcingaExecutePlugin +.EXAMPLE + PS> [hashtable]$ConvertedArgs = ConvertTo-IcingaPowerShellArguments -Arguments $args; +#> + +function ConvertTo-IcingaPowerShellArguments() +{ + param ( + [array]$Arguments = @() + ); + + [hashtable]$IcingaArguments = @{ }; + [int]$ArgumentIndex = 0; + + while ($ArgumentIndex -lt $Arguments.Count) { + # Check if the current position is a string + if ($Arguments[$ArgumentIndex] -IsNot [string]) { + # Continue if we are not a string (argument) + $ArgumentIndex += 1; + continue; + } + + # check_by_icingaforwindows arguments -> not required for any plugin execution + if ($Arguments[$ArgumentIndex] -eq '-IcingaForWindowsRemoteExecution' -Or $Arguments[$ArgumentIndex] -eq '-IcingaForWindowsJEARemoteExecution') { + $ArgumentIndex += 1; + continue; + } + + # Check if it starts with '-', which should indicate it being an argument + if ($Arguments[$ArgumentIndex][0] -ne '-') { + # Continue if we are not an argument + $ArgumentIndex += 1; + continue; + } + + # First convert our argument + [string]$Argument = ConvertTo-IcingaUTF8Value -InputObject $Arguments[$ArgumentIndex]; + # Cut the first '-' + $Argument = $Argument.Substring(1, $Argument.Length - 1); + + # Check if there is anything beyond this argument, if not + # -> We are a switch argument, adding TRUE; + if (($ArgumentIndex + 1) -ge $Arguments.Count) { + $IcingaArguments.Add($Argument, $TRUE); + $ArgumentIndex += 1; + continue; + } + + # Check if our next value in the array is a string + if ($Arguments[$ArgumentIndex + 1] -Is [string]) { + [string]$NextValue = $Arguments[$ArgumentIndex + 1]; + + # If our next value on the index starts with '-', we found another argument + # -> The current argument seems to be a switch argument + if ($NextValue[0] -eq '-') { + $IcingaArguments.Add($Argument, $TRUE); + $ArgumentIndex += 1; + continue; + } + + # It could be that we parse strings without quotation which is broken because on how + # Icinga is actually writing the arguments, let's fix this by building the string ourselves + [int]$ReadStringIndex = $ArgumentIndex; + $StringValue = New-Object -TypeName 'System.Text.StringBuilder'; + while ($TRUE) { + # Check if we read beyond our array + if (($ReadStringIndex + 1) -ge $Arguments.Count) { + break; + } + + # Check if the next element is no longer a string element + if ($Arguments[$ReadStringIndex + 1] -IsNot [string]) { + break; + } + + [string]$NextValue = $Arguments[$ReadStringIndex + 1]; + + # In case the next string element starts with '-', this could be an argument + if ($NextValue[0] -eq '-') { + break; + } + + # If we already added elements to our string builder before, add a whitespace + if ($StringValue.Length -ne 0) { + $StringValue.Append(' ') | Out-Null; + } + + # Append our string value to the string builder + $StringValue.Append($NextValue) | Out-Null; + $ReadStringIndex += 1; + } + + # Add our argument with the string builder value, in case we had something to add there + if ($StringValue.Length -ne 0) { + $IcingaArguments.Add($Argument, (ConvertTo-IcingaUTF8Value -InputObject $StringValue.ToString())); + $ArgumentIndex += 1; + continue; + } + } + + # All Remaining values + + # If we are an array object, handle empty arrays + if ($Arguments[$ArgumentIndex + 1] -Is [array]) { + if ($null -eq $Arguments[$ArgumentIndex + 1] -Or ($Arguments[$ArgumentIndex + 1]).Count -eq 0) { + $IcingaArguments.Add($Argument, @()); + $ArgumentIndex += 1; + continue; + } + } + + # Add everything else + $IcingaArguments.Add( + $Argument, + (ConvertTo-IcingaUTF8Value -InputObject $Arguments[$ArgumentIndex + 1]) + ); + + $ArgumentIndex += 1; + } + + return $IcingaArguments; +} diff --git a/lib/core/tools/ConvertTo-IcingaUTF8Value.psm1 b/lib/core/tools/ConvertTo-IcingaUTF8Value.psm1 new file mode 100644 index 00000000..0a5f73ba --- /dev/null +++ b/lib/core/tools/ConvertTo-IcingaUTF8Value.psm1 @@ -0,0 +1,50 @@ +<# +.SYNOPSIS + Converts strings and all objects within an array from Default PowerShell encoding + to UTF8 +.DESCRIPTION + Converts strings and all objects within an array from Default PowerShell encoding + to UTF8 +.PARAMETER InputObject + A string or array object to convert +.EXAMPLE + PS> [array]$ConvertedArgs = ConvertTo-IcingaUTF8Arguments -Arguments $args; +#> + +function ConvertTo-IcingaUTF8Value() +{ + param ( + $InputObject = $null + ); + + if ($null -eq $InputObject) { + return $InputObject; + } + + if ($InputObject -Is [string]) { + # If german umlauts are contained, do not convert the value + # Fixing issues for running checks locally on CLI vs. Icinga Agent + if ($InputObject -Match "[äöüÄÖÜß]") { + return $InputObject; + } + + $InputInBytes = [System.Text.Encoding]::Default.GetBytes($InputObject); + + return ([string]([System.Text.Encoding]::UTF8.GetString($InputInBytes))); + } elseif ($InputObject -Is [array]) { + [array]$ArrayObject = @(); + + foreach ($entry in $InputObject) { + if ($entry -Is [array]) { + $ArrayObject += , (ConvertTo-IcingaUTF8Value -InputObject $entry); + } else { + $ArrayObject += ConvertTo-IcingaUTF8Value -InputObject $entry; + } + } + + return $ArrayObject; + } + + # If we are not a string or a array, just return the object + return $InputObject; +} diff --git a/lib/icinga/plugin/Exit-IcingaExecutePlugin.psm1 b/lib/icinga/plugin/Exit-IcingaExecutePlugin.psm1 index e7c93eb8..edfe7dbb 100644 --- a/lib/icinga/plugin/Exit-IcingaExecutePlugin.psm1 +++ b/lib/icinga/plugin/Exit-IcingaExecutePlugin.psm1 @@ -4,6 +4,8 @@ function Exit-IcingaExecutePlugin() [string]$Command = '' ); + # We need to fix the argument encoding hell + [hashtable]$ConvertedArgs = ConvertTo-IcingaPowerShellArguments -Arguments $args; [string]$JEAProfile = Get-IcingaJEAContext; [bool]$CheckByIcingaForWindows = $FALSE; [bool]$CheckByJEAShell = $FALSE; @@ -19,7 +21,7 @@ function Exit-IcingaExecutePlugin() # checks from a Linux/Windows remote source if ($CheckByIcingaForWindows) { # First try to queue the check over the REST-Api - $CheckResult = Invoke-IcingaInternalServiceCall -Command $Command -Arguments $args -NoExit; + $CheckResult = Invoke-IcingaInternalServiceCall -Command $Command -Arguments $ConvertedArgs -NoExit; if ($null -ne $CheckResult) { # Seems we got a result @@ -36,14 +38,14 @@ function Exit-IcingaExecutePlugin() try { # Execute our plugin - (& $Command @args) | Out-Null; + (& $Command @ConvertedArgs) | Out-Null; } catch { # Handle errors within our plugins # If anything goes wrong handle the error very detailed $Global:Icinga.Protected.RunAsDaemon = $FALSE; - Write-IcingaExecutePluginException -Command $Command -ErrorObject $_ -Arguments $args; - $args.Clear(); + Write-IcingaExecutePluginException -Command $Command -ErrorObject $_ -Arguments $ConvertedArgs; + $ConvertedArgs.Clear(); # Do not close the session, we need to read the ExitCode from Get-IcingaInternalPluginExitCode # The plugin itself will terminate the session @@ -64,7 +66,7 @@ function Exit-IcingaExecutePlugin() # Regardless of JEA enabled or disabled, forward all checks to the internal API # and check if we get a result from there - Invoke-IcingaInternalServiceCall -Command $Command -Arguments $args; + Invoke-IcingaInternalServiceCall -Command $Command -Arguments $ConvertedArgs; try { # If the plugin is not installed, throw a good exception @@ -106,7 +108,7 @@ function Exit-IcingaExecutePlugin() 'PerfData' = (Get-IcingaCheckSchedulerPerfData) 'ExitCode' = $ExitCode; } - } -args $Command, $args + } -args $Command, $ConvertedArgs ) 2>$ErrorHandler; # If we have an exit code larger or equal 0, the execution inside the JEA shell was successfully and we can share the result @@ -123,12 +125,12 @@ function Exit-IcingaExecutePlugin() } else { # If we simply run the check without JEA context or from remote, we can just execute the plugin and # exit with the exit code received from the result - exit (& $Command @args); + exit (& $Command @ConvertedArgs); } } catch { # If anything goes wrong handle the error - Write-IcingaExecutePluginException -Command $Command -ErrorObject $_ -Arguments $args; - $args.Clear(); + Write-IcingaExecutePluginException -Command $Command -ErrorObject $_ -Arguments $ConvertedArgs; + $ConvertedArgs.Clear(); exit 3; }