summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKartik Null Cating-Subramanian <ksubramanian@chef.io>2015-05-12 14:31:38 -0400
committerKartik Null Cating-Subramanian <ksubramanian@chef.io>2015-06-09 14:20:37 -0400
commit65fb7ac8a962d639b587067a47e995cc8f019365 (patch)
tree6d5560c43217db2448a7a52009433f5ce6d383b5
parentd63b9d2e0cabe87cf6db6657301cf525a4fea876 (diff)
downloadchef-65fb7ac8a962d639b587067a47e995cc8f019365.tar.gz
Rewrote this after I fully understood the madness involved.
-rw-r--r--distro/powershell/chef/chef.psm198
1 files changed, 77 insertions, 21 deletions
diff --git a/distro/powershell/chef/chef.psm1 b/distro/powershell/chef/chef.psm1
index 56b42af78c..26e4fb6992 100644
--- a/distro/powershell/chef/chef.psm1
+++ b/distro/powershell/chef/chef.psm1
@@ -6,55 +6,111 @@
$PSScriptRoot
}
-function Run-Command($command, $argList) {
- # Take each input string, escape any \ ' or " character in it and then surround it with "s.
- # This is to defeat the second-level parsing performed by the MSVCRT argument parser used
- # by ruby which only understands \ ' and ".
+function Run-RubyCommand($command, $argList) {
+ # 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.
#
- # This is a fuster cluck. Don't touch this unless you are sure you understand regexes.
- # The \\ is to request a literal \ match in a regex.
- # The "" is to inject a literal " character in a PS string surrounded by "s.
- # The replacement pattern must be '\$1' and not "\$1" because $1 is not a real variable
- # that needs substituting - it's a capture group that's interpreted by the regex engine.
- # \ in the replacement pattern does not need to be escaped - it is literally substituted.
- $transformed = $argList | foreach { '"' + ( $_ -replace "([\\'""])",'\$1' ) + '"' }
+ # 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 will not
+ # evaluate them but (contrary to documentation), it will still marginally interpret them. 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.
+ #
+ # 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 and finally injecting a "" sequence at the beginning and end of the concatenated string.
+ # 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.
+ $transformedList = $argList | foreach { "'" + ( $_ -replace "'","'`"'`"'" ) + "'" }
+ $fortifiedArgString = '""' + ($transformedList -join ' ') + '""'
- #& "echoargs.exe" $transformed
# Use the correct embedded ruby path. We'll be deployed at a path that looks like
# [C:\opscode or some other prefix]\chef\modules\chef
$ruby = Join-Path (Get-ScriptDirectory) "..\..\embedded\bin\ruby.exe"
$commandPath = Join-Path (Get-ScriptDirectory) "..\..\bin\$command"
- & $ruby $commandPath $transformed
+ & $ruby $commandPath $fortifiedArgString
}
function chef-apply {
- Run-Command 'chef-apply' $args
+ Run-RubyCommand 'chef-apply' $args
}
function chef-client {
- Run-Command 'chef-client' $args
+ Run-RubyCommand 'chef-client' $args
}
function chef-service-manager {
- Run-Command 'chef-service-manager' $args
+ Run-RubyCommand 'chef-service-manager' $args
}
function chef-shell {
- Run-Command 'chef-shell' $args
+ Run-RubyCommand 'chef-shell' $args
}
function chef-solo {
- Run-Command 'chef-solo' $args
+ Run-RubyCommand 'chef-solo' $args
}
function chef-windows-service {
- Run-Command 'chef-windows-service' $args
+ Run-RubyCommand 'chef-windows-service' $args
}
function knife {
- Run-Command 'knife' $args
+ Run-RubyCommand 'knife' $args
}
-Export-ModuleMember -function chef-*
+Export-ModuleMember -function chef-apply
+Export-ModuleMember -function chef-client
+Export-ModuleMember -function chef-service-manager
+Export-ModuleMember -function chef-shell
+Export-ModuleMember -function chef-solo
+Export-ModuleMember -function chef-windows-service
Export-ModuleMember -function knife
+
+# To debug this module, uncomment the line below and then run the following.
+# Export-ModuleMember -function Run-RubyCommand
+# Remove-Module chef
+# Import-Module chef
+# "puts ARGV" | Out-File C:\opscode\chef\bin\puts_args
+# Run-RubyCommand puts_args 'Here' "are" some '"very interesting"' 'arguments[to]' "`"try out`"" \ No newline at end of file