Skip to content

Instantly share code, notes, and snippets.

@vrimkus
Last active December 22, 2017 23:03
Show Gist options
  • Save vrimkus/fd743dd22e67b4977ebee0ba45dfe0c1 to your computer and use it in GitHub Desktop.
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
###
### 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 *
@vrimkus
Copy link
Author

vrimkus commented Dec 14, 2017

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.

@vrimkus
Copy link
Author

vrimkus commented Dec 22, 2017

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