Skip to content

Commit

Permalink
Fixes UTF8 encoding for plugin execution
Browse files Browse the repository at this point in the history
  • Loading branch information
LordHepipud committed Aug 17, 2022
1 parent fd3ddd1 commit 787500c
Show file tree
Hide file tree
Showing 8 changed files with 206 additions and 56 deletions.
1 change: 1 addition & 0 deletions doc/100-General/10-Changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ Released closed milestones can be found on [GitHub](https://github.com/Icinga/ic

* [#473](https://github.com/Icinga/icinga-powershell-framework/pull/473) Fixes an issue with current string rendering config implementation, as string values containing whitespaces or `$` are rendered wrong by default, if not set in single quotes `''`
* [#476](https://github.com/Icinga/icinga-powershell-framework/pull/476) Fixes exception `You cannot call a method on va null-valued expression` during installation in case no background daemon is configured
* [#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
* [#529](https://github.com/Icinga/icinga-powershell-framework/pull/529) Fixes package manifest reader for Icinga for Windows components on Windows 2012 R2 and older
* [#523](https://github.com/Icinga/icinga-powershell-framework/pull/523) Fixes errors on encapsulated PowerShell calls for missing Cmdlets `Write-IcingaConsoleError` and `Optimize-IcingaForWindowsMemory`
* [#524](https://github.com/Icinga/icinga-powershell-framework/issues/524) Fixes uninstallation process by improving the location handling of PowerShell instances with Icinga IMC or Shell
Expand Down
4 changes: 2 additions & 2 deletions icinga-powershell-framework.psm1
Original file line number Diff line number Diff line change
Expand Up @@ -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";
}

Expand All @@ -122,7 +122,7 @@ function Write-IcingaFrameworkCodeCache()
return;
}

Set-Content -Path $CacheFile -Value $CacheContent;
Set-Content -Path $CacheFile -Value $CacheContent -Encoding 'UTF8';

Remove-IcingaFrameworkDependencyFile;

Expand Down
50 changes: 7 additions & 43 deletions lib/core/framework/Invoke-IcingaInternalServiceCall.psm1
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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;
}

Expand All @@ -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;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@ function Read-IcingaAgentDebugLogFile()
return;
}

Get-Content -Path $Logfile -Tail 20 -Wait;
Get-Content -Path $Logfile -Tail 20 -Wait -Encoding 'UTF8';
}
2 changes: 1 addition & 1 deletion lib/core/icingaagent/readers/Read-IcingaAgentLogFile.psm1
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,6 @@ function Read-IcingaAgentLogFile()
return;
}

Get-Content -Path $Logfile -Tail 20 -Wait;
Get-Content -Path $Logfile -Tail 20 -Wait -Encoding 'UTF8';
}
}
133 changes: 133 additions & 0 deletions lib/core/tools/ConvertTo-IcingaPowerShellArguments.psm1
Original file line number Diff line number Diff line change
@@ -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;
}
50 changes: 50 additions & 0 deletions lib/core/tools/ConvertTo-IcingaUTF8Value.psm1
Original file line number Diff line number Diff line change
@@ -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;
}
20 changes: 11 additions & 9 deletions lib/icinga/plugin/Exit-IcingaExecutePlugin.psm1
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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;
}
Expand Down

0 comments on commit 787500c

Please sign in to comment.