Last active
December 22, 2017 23:03
-
-
Save vrimkus/fd743dd22e67b4977ebee0ba45dfe0c1 to your computer and use it in GitHub Desktop.
Test Chef PowerShell module which does not use Win32 API for calling ruby.exe
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
### | |
### Helper Functions | |
### | |
#region Helper Functions | |
function GetScriptDirectory { | |
if (-not $PSScriptRoot) { | |
$Invocation = (Get-Variable MyInvocation -Scope 1).Value | |
$PSScriptRoot = Split-Path $Invocation.MyCommand.Path | |
} | |
$PSScriptRoot | |
} | |
function NeedQuotes ([string] $stringToCheck) { | |
$needQuotes = $false | |
$followingBackslash = $false | |
$quoteCount = 0 | |
foreach ($i in (0..$stringToCheck.Length)) { | |
if ($stringToCheck[$i] -eq '"' -and -not $followingBackslash) { | |
$quoteCount += 1 | |
} elseif ([char]::IsWhiteSpace($stringToCheck[$i]) -and ($quoteCount % 2 -eq 0)) { | |
$needQuotes = $true | |
} | |
$followingBackslash = $stringToCheck[$i] -eq '\\' | |
} | |
$needQuotes | |
} | |
function GetValidRubyArguments ([object[]] $ArgumentList) { | |
# This method exists to take the given list of arguments and get it past ruby's command-line | |
# interpreter unscathed and untampered. See https://github.com/ruby/ruby/blob/trunk/win32/win32.c#L1582 | |
# for a list of transformations that ruby attempts to perform with your command-line arguments | |
# before passing it onto a script. The most important task is to defeat the globbing | |
# and wild-card expansion that ruby performs. Note that ruby does not use MSVCRT's argc/argv | |
# and deliberately reparses the raw command-line instead. | |
# | |
# To stop ruby from interpreting command-line arguments as globs, they need to be enclosed in ' | |
# Ruby doesn't allow any escape characters inside '. This unfortunately prevents us from sending | |
# any strings which themselves contain '. Ruby does allow multi-fragment arguments though. | |
# "foo bar"'baz qux'123"foo" is interpreted as 1 argument because there are no un-escaped | |
# whitespace there. The argument would be interpreted as the string "foo barbaz qux123foo". | |
# This lets us escape ' characters by exiting the ' quoted string, injecting a "'" fragment and | |
# then resuming the ' quoted string again. | |
# | |
# In the process of defeating ruby, one must also defeat the helpfulness of powershell. | |
# When arguments come into this method, the standard PS rules for interpreting cmdlet arguments | |
# apply. When using & (call operator) and providing an array of arguments, powershell (verified | |
# on PS 4.0 on Windows Server 2012R2) will not evaluate them but (contrary to documentation), | |
# it will still marginally interpret them. The behaviour of PS 5.0 seems to be different but | |
# ignore that for now. If any of the provided arguments has a space in it, powershell checks | |
# the first and last character to ensure that they are " characters (and that's all it checks). | |
# If they are not, it will blindly surround that argument with " characters. It won't do this | |
# operation if no space is present, even if other special characters are present. If it notices | |
# leading and trailing " characters, it won't actually check to see if there are other " | |
# characters in the string. Since PS 5.0 changes this behavior, we could consider using the --% | |
# "stop screwing up my arguments" operator, which is available since PS 3.0. When encountered | |
# --% indicates that the rest of line is to be sent literally... except if the parser encounters | |
# %FOO% cmd style environment variables. Because reasons. And there is no way to escape the | |
# % character in *any* waym shape or form. | |
# https://connect.microsoft.com/PowerShell/feedback/details/376207/executing-commands-which-require-quotes-and-variables-is-practically-impossible | |
# | |
# In case you think that you're either reading this incorrectly or that I'm full of shit, here | |
# are some examples. These use EchoArgs.exe from the PowerShell Community Extensions package. | |
# I have not included the argument parsing output from EchoArgs.exe to prevent confusing you with | |
# more details about MSVCRT's parsing algorithm. | |
# | |
# $x = "foo '' bar `"baz`"" | |
# & EchoArgs @($x, $x) | |
# Command line: | |
# "C:\Program Files (x86)\PowerShell Community Extensions\Pscx3\Pscx\Apps\EchoArgs.exe" "foo '' bar "baz"" "foo '' bar "baz"" | |
# | |
# $x = "abc'123'nospace`"lulz`"!!!" | |
# & EchoArgs @($x, $x) | |
# Command line: | |
# "C:\Program Files (x86)\PowerShell Community Extensions\Pscx3\Pscx\Apps\EchoArgs.exe" abc'123'nospace"lulz"!!! abc'123'nospace"lulz"!!! | |
# | |
# $x = "`"`"Look ma! Tonnes of spaces! 'foo' 'bar'`"`"" | |
# & EchoArgs @($x, $x) | |
# Command line: | |
# "C:\Program Files (x86)\PowerShell Community Extensions\Pscx3\Pscx\Apps\EchoArgs.exe" ""Look ma! Tonnes of spaces! 'foo' 'bar'"" ""Look ma! Tonnes of spaces! 'foo' 'bar'"" | |
# | |
# Given all this, we can now device a strategy to work around all these immensely helpful, well | |
# documented and useful tools by looking at each incoming argument, escaping any ' characters | |
# with a '"'"' sequence, surrounding each argument with ' & joining them with a space separating | |
# them. | |
# There is another bug (https://bugs.ruby-lang.org/issues/11142) that causes ruby to mangle any | |
# "" two-character double quote sequence but since we always emit our strings inside ' except for | |
# ' characters, this should be ok. Just remember that an argument '' should get translated to | |
# ''"'"''"'"'' on the command line. If those intervening empty ''s are not present, the presence | |
# of "" will cause ruby to mangle that argument. | |
foreach ($arg in $ArgumentList) { | |
if (NeedQuotes($arg)) { | |
# Wrap argument in quotes the way PowerShell would for native commands. | |
'"' + $arg + '"' | |
} else { | |
# Escape any globs and wildcards in argument value in a way that Ruby can handle. | |
"'" + ( $arg -replace "'","'`"'`"'" ) + "'" | |
} | |
} | |
} | |
#endregion Helper Functions | |
### | |
### Script Variables | |
### | |
$ruby = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath( | |
(Join-Path (GetScriptDirectory) '../../embedded/bin/ruby.exe')) | |
$chefdkBinDir = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath( | |
(Join-Path (GetScriptDirectory) '../../bin')) | |
### | |
### Public Functions | |
### | |
#region Public Functions | |
# Create PowerShell wrapper functions using proper Verb-Noun naming conventions. | |
# Then create Aliases using the corresponding ChefDK command name to remove warning when importing module. | |
function Invoke-ChefApply { | |
$commandArgumentList = @(Join-Path $chefdkBinDir chef-apply) + @(GetValidRubyArguments($args)) | |
& $ruby @commandArgumentList | |
} | |
Set-Alias -Name chef-apply -Value Invoke-ChefApply | |
function Invoke-ChefClient { | |
$commandArgumentList = @(Join-Path $chefdkBinDir chef-client) + @(GetValidRubyArguments($args)) | |
& $ruby @commandArgumentList | |
} | |
Set-Alias -Name chef-client -Value Invoke-ChefClient | |
function Invoke-ChefServiceManager { | |
$commandArgumentList = @(Join-Path $chefdkBinDir chef-service-manager) + @(GetValidRubyArguments($args)) | |
& $ruby @commandArgumentList | |
} | |
Set-Alias -Name chef-service-manager -Value Invoke-ChefServiceManager | |
function Invoke-ChefShell { | |
$commandArgumentList = @(Join-Path $chefdkBinDir chef-shell) + @(GetValidRubyArguments($args)) | |
& $ruby @commandArgumentList | |
} | |
Set-Alias -Name chef-shell -Value Invoke-ChefShell | |
function Invoke-ChefSolo { | |
$commandArgumentList = @(Join-Path $chefdkBinDir chef-solo) + @(GetValidRubyArguments($args)) | |
& $ruby @commandArgumentList | |
} | |
Set-Alias -Name chef-solo -Value Invoke-ChefSolo | |
function Invoke-ChefWindowsService { | |
$commandArgumentList = @(Join-Path $chefdkBinDir chef-windows-service) + @(GetValidRubyArguments($args)) | |
& $ruby @commandArgumentList | |
} | |
Set-Alias -Name chef-windows-service -Value Invoke-ChefWindowsService | |
function Invoke-Knife { | |
$commandArgumentList = @(Join-Path $chefdkBinDir knife) + @(GetValidRubyArguments($args)) | |
& $ruby @commandArgumentList | |
} | |
Set-Alias -Name knife -Value Invoke-knife | |
#endregion Public Functions | |
Export-ModuleMember -Function *-* -Alias * |
Testing Setup
PS C:\temp> mkdir recipei; mkdir recipep; mkdir recipes; mkdir reciped
Directory: C:\temp
Mode LastWriteTime Length Name
---- ------------- ------ ----
d---- 12/22/2017 4:35 PM recipei
d---- 12/22/2017 4:35 PM recipep
d---- 12/22/2017 4:35 PM recipes
d---- 12/22/2017 4:35 PM reciped
Without PowerShell Module
PS C:\temp> knife exec -E 'puts ARGV' recipe[iptables::default] '&s0meth1ng'
exec
-E
puts ARGV
reciped
recipei
recipep
recipes
's0meth1ng' is not recognized as an internal or external command,
operable program or batch file.
With Updated PowerShell Module
PS C:\temp> ipmo C:\opscode\chefdk\modules\chef\chef_nowin32.psm1
PS C:\temp> knife exec -E 'puts ARGV' recipe[iptables::default] '&s0meth1ng'
exec
-E
puts ARGV
recipe[iptables::default]
&s0meth1ng
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
This also renames the exported functions to comply with PowerShell function naming conventions (
Verb-Noun
), and then exports aliases matching the original name for compatibility.