winrm 1.3.6 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +10 -10
  3. data/.rspec +3 -3
  4. data/.rubocop.yml +9 -9
  5. data/.travis.yml +4 -4
  6. data/Gemfile +9 -9
  7. data/LICENSE +202 -202
  8. data/README.md +148 -148
  9. data/Rakefile +28 -28
  10. data/Vagrantfile +9 -9
  11. data/bin/rwinrm +97 -97
  12. data/changelog.md +49 -49
  13. data/lib/winrm.rb +42 -41
  14. data/lib/winrm/exceptions/exceptions.rb +57 -57
  15. data/lib/winrm/helpers/iso8601_duration.rb +58 -58
  16. data/lib/winrm/helpers/powershell_script.rb +37 -37
  17. data/lib/winrm/http/response_handler.rb +82 -82
  18. data/lib/winrm/http/transport.rb +294 -294
  19. data/lib/winrm/output.rb +43 -43
  20. data/lib/winrm/soap_provider.rb +40 -40
  21. data/lib/winrm/version.rb +7 -0
  22. data/lib/winrm/winrm_service.rb +490 -490
  23. data/preamble +17 -17
  24. data/spec/auth_timeout_spec.rb +16 -16
  25. data/spec/cmd_spec.rb +102 -102
  26. data/spec/config-example.yml +19 -19
  27. data/spec/exception_spec.rb +50 -50
  28. data/spec/issue_59_spec.rb +15 -15
  29. data/spec/matchers.rb +74 -74
  30. data/spec/output_spec.rb +110 -110
  31. data/spec/powershell_spec.rb +103 -103
  32. data/spec/response_handler_spec.rb +59 -59
  33. data/spec/spec_helper.rb +48 -48
  34. data/spec/stubs/responses/open_shell_v1.xml +19 -19
  35. data/spec/stubs/responses/open_shell_v2.xml +20 -20
  36. data/spec/stubs/responses/soap_fault_v1.xml +36 -36
  37. data/spec/stubs/responses/soap_fault_v2.xml +42 -42
  38. data/spec/stubs/responses/wmi_error_v2.xml +41 -41
  39. data/spec/winrm_options_spec.rb +76 -76
  40. data/spec/winrm_primitives_spec.rb +51 -51
  41. data/spec/wql_spec.rb +14 -14
  42. data/winrm.gemspec +40 -41
  43. metadata +4 -4
  44. data/VERSION +0 -1
@@ -1,43 +1,43 @@
1
- # encoding: UTF-8
2
- #
3
- # Copyright 2014 Max Lincoln <max@devopsy.com>
4
- #
5
- # Licensed under the Apache License, Version 2.0 (the "License");
6
- # you may not use this file except in compliance with the License.
7
- # You may obtain a copy of the License at
8
- #
9
- # http://www.apache.org/licenses/LICENSE-2.0
10
- #
11
- # Unless required by applicable law or agreed to in writing, software
12
- # distributed under the License is distributed on an "AS IS" BASIS,
13
- # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
- # See the License for the specific language governing permissions and
15
- # limitations under the License.
16
-
17
- module WinRM
18
- # This class holds raw output as a hash, and has convenience methods to parse.
19
- class Output < Hash
20
- def initialize
21
- super
22
- self[:data] = []
23
- end
24
-
25
- def output
26
- self[:data].flat_map do |line|
27
- [line[:stdout], line[:stderr]]
28
- end.compact.join
29
- end
30
-
31
- def stdout
32
- self[:data].map do |line|
33
- line[:stdout]
34
- end.compact.join
35
- end
36
-
37
- def stderr
38
- self[:data].map do |line|
39
- line[:stderr]
40
- end.compact.join
41
- end
42
- end
43
- end
1
+ # encoding: UTF-8
2
+ #
3
+ # Copyright 2014 Max Lincoln <max@devopsy.com>
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+
17
+ module WinRM
18
+ # This class holds raw output as a hash, and has convenience methods to parse.
19
+ class Output < Hash
20
+ def initialize
21
+ super
22
+ self[:data] = []
23
+ end
24
+
25
+ def output
26
+ self[:data].flat_map do |line|
27
+ [line[:stdout], line[:stderr]]
28
+ end.compact.join
29
+ end
30
+
31
+ def stdout
32
+ self[:data].map do |line|
33
+ line[:stdout]
34
+ end.compact.join
35
+ end
36
+
37
+ def stderr
38
+ self[:data].map do |line|
39
+ line[:stderr]
40
+ end.compact.join
41
+ end
42
+ end
43
+ end
@@ -1,40 +1,40 @@
1
- # encoding: UTF-8
2
- #
3
- # Copyright 2010 Dan Wanek <dan.wanek@gmail.com>
4
- #
5
- # Licensed under the Apache License, Version 2.0 (the "License");
6
- # you may not use this file except in compliance with the License.
7
- # You may obtain a copy of the License at
8
- #
9
- # http://www.apache.org/licenses/LICENSE-2.0
10
- #
11
- # Unless required by applicable law or agreed to in writing, software
12
- # distributed under the License is distributed on an "AS IS" BASIS,
13
- # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
- # See the License for the specific language governing permissions and
15
- # limitations under the License.
16
-
17
- require 'httpclient'
18
- require 'builder'
19
- require 'gyoku'
20
- require 'uuidtools'
21
- require 'base64'
22
-
23
- # SOAP constants for WinRM
24
- module WinRM
25
- NS_SOAP_ENV = 's' # http://www.w3.org/2003/05/soap-envelope
26
- NS_ADDRESSING = 'a' # http://schemas.xmlsoap.org/ws/2004/08/addressing
27
- NS_CIMBINDING = 'b' # http://schemas.dmtf.org/wbem/wsman/1/cimbinding.xsd
28
- NS_ENUM = 'n' # http://schemas.xmlsoap.org/ws/2004/09/enumeration
29
- NS_TRANSFER = 'x' # http://schemas.xmlsoap.org/ws/2004/09/transfer
30
- NS_WSMAN_DMTF = 'w' # http://schemas.dmtf.org/wbem/wsman/1/wsman.xsd
31
- NS_WSMAN_MSFT = 'p' # http://schemas.microsoft.com/wbem/wsman/1/wsman.xsd
32
- NS_SCHEMA_INST = 'xsi' # http://www.w3.org/2001/XMLSchema-instance
33
- NS_WIN_SHELL = 'rsp' # http://schemas.microsoft.com/wbem/wsman/1/windows/shell
34
- NS_WSMAN_FAULT = 'f' # http://schemas.microsoft.com/wbem/wsman/1/wsmanfault
35
- NS_WSMAN_CONF = 'cfg' # http://schemas.microsoft.com/wbem/wsman/1/config
36
- end
37
-
38
- require 'winrm/exceptions/exceptions'
39
- require 'winrm/winrm_service'
40
- require 'winrm/http/transport'
1
+ # encoding: UTF-8
2
+ #
3
+ # Copyright 2010 Dan Wanek <dan.wanek@gmail.com>
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+
17
+ require 'httpclient'
18
+ require 'builder'
19
+ require 'gyoku'
20
+ require 'uuidtools'
21
+ require 'base64'
22
+
23
+ # SOAP constants for WinRM
24
+ module WinRM
25
+ NS_SOAP_ENV = 's' # http://www.w3.org/2003/05/soap-envelope
26
+ NS_ADDRESSING = 'a' # http://schemas.xmlsoap.org/ws/2004/08/addressing
27
+ NS_CIMBINDING = 'b' # http://schemas.dmtf.org/wbem/wsman/1/cimbinding.xsd
28
+ NS_ENUM = 'n' # http://schemas.xmlsoap.org/ws/2004/09/enumeration
29
+ NS_TRANSFER = 'x' # http://schemas.xmlsoap.org/ws/2004/09/transfer
30
+ NS_WSMAN_DMTF = 'w' # http://schemas.dmtf.org/wbem/wsman/1/wsman.xsd
31
+ NS_WSMAN_MSFT = 'p' # http://schemas.microsoft.com/wbem/wsman/1/wsman.xsd
32
+ NS_SCHEMA_INST = 'xsi' # http://www.w3.org/2001/XMLSchema-instance
33
+ NS_WIN_SHELL = 'rsp' # http://schemas.microsoft.com/wbem/wsman/1/windows/shell
34
+ NS_WSMAN_FAULT = 'f' # http://schemas.microsoft.com/wbem/wsman/1/wsmanfault
35
+ NS_WSMAN_CONF = 'cfg' # http://schemas.microsoft.com/wbem/wsman/1/config
36
+ end
37
+
38
+ require 'winrm/exceptions/exceptions'
39
+ require 'winrm/winrm_service'
40
+ require 'winrm/http/transport'
@@ -0,0 +1,7 @@
1
+ # encoding: UTF-8
2
+
3
+ # WinRM module
4
+ module WinRM
5
+ # The version of the WinRM library
6
+ VERSION = '1.4.0'
7
+ end
@@ -1,490 +1,490 @@
1
- # encoding: UTF-8
2
- #
3
- # Copyright 2010 Dan Wanek <dan.wanek@gmail.com>
4
- #
5
- # Licensed under the Apache License, Version 2.0 (the "License");
6
- # you may not use this file except in compliance with the License.
7
- # You may obtain a copy of the License at
8
- #
9
- # http://www.apache.org/licenses/LICENSE-2.0
10
- #
11
- # Unless required by applicable law or agreed to in writing, software
12
- # distributed under the License is distributed on an "AS IS" BASIS,
13
- # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
- # See the License for the specific language governing permissions and
15
- # limitations under the License.
16
-
17
- require 'nori'
18
- require 'rexml/document'
19
- require_relative 'helpers/powershell_script'
20
-
21
- module WinRM
22
- # This is the main class that does the SOAP request/response logic. There are a few helper
23
- # classes, but pretty much everything comes through here first.
24
- class WinRMWebService
25
- DEFAULT_TIMEOUT = 'PT60S'
26
- DEFAULT_MAX_ENV_SIZE = 153600
27
- DEFAULT_LOCALE = 'en-US'
28
-
29
- attr_reader :endpoint, :timeout
30
-
31
- # @param [String,URI] endpoint the WinRM webservice endpoint
32
- # @param [Symbol] transport either :kerberos(default)/:ssl/:plaintext
33
- # @param [Hash] opts Misc opts for the various transports.
34
- # @see WinRM::HTTP::HttpTransport
35
- # @see WinRM::HTTP::HttpGSSAPI
36
- # @see WinRM::HTTP::HttpSSL
37
- def initialize(endpoint, transport = :kerberos, opts = {})
38
- @endpoint = endpoint
39
- @timeout = DEFAULT_TIMEOUT
40
- @max_env_sz = DEFAULT_MAX_ENV_SIZE
41
- @locale = DEFAULT_LOCALE
42
- @logger = Logging.logger[self]
43
- case transport
44
- when :kerberos
45
- require 'gssapi'
46
- require 'gssapi/extensions'
47
- @xfer = HTTP::HttpGSSAPI.new(endpoint, opts[:realm], opts[:service], opts[:keytab], opts)
48
- when :plaintext
49
- @xfer = HTTP::HttpPlaintext.new(endpoint, opts[:user], opts[:pass], opts)
50
- when :ssl
51
- @xfer = HTTP::HttpSSL.new(endpoint, opts[:user], opts[:pass], opts[:ca_trust_path], opts)
52
- else
53
- raise "Invalid transport '#{transport}' specified, expected: kerberos, plaintext, ssl."
54
- end
55
- end
56
-
57
- # Operation timeout.
58
- #
59
- # Unless specified the client receive timeout will be 10s + the operation
60
- # timeout.
61
- #
62
- # @see http://msdn.microsoft.com/en-us/library/ee916629(v=PROT.13).aspx
63
- #
64
- # @param [Fixnum] The number of seconds to set the WinRM operation timeout
65
- # @param [Fixnum] The number of seconds to set the Ruby receive timeout
66
- # @return [String] The ISO 8601 formatted operation timeout
67
- def set_timeout(op_timeout_sec, receive_timeout_sec=nil)
68
- @timeout = Iso8601Duration.sec_to_dur(op_timeout_sec)
69
- @xfer.receive_timeout = receive_timeout_sec || op_timeout_sec + 10
70
- @timeout
71
- end
72
- alias :op_timeout :set_timeout
73
-
74
- # Max envelope size
75
- # @see http://msdn.microsoft.com/en-us/library/ee916127(v=PROT.13).aspx
76
- # @param [Fixnum] byte_sz the max size in bytes to allow for the response
77
- def max_env_size(byte_sz)
78
- @max_env_sz = byte_sz
79
- end
80
-
81
- # Set the locale
82
- # @see http://msdn.microsoft.com/en-us/library/gg567404(v=PROT.13).aspx
83
- # @param [String] locale the locale to set for future messages
84
- def locale(locale)
85
- @locale = locale
86
- end
87
-
88
- # Create a Shell on the destination host
89
- # @param [Hash<optional>] shell_opts additional shell options you can pass
90
- # @option shell_opts [String] :i_stream Which input stream to open. Leave this alone unless you know what you're doing (default: stdin)
91
- # @option shell_opts [String] :o_stream Which output stream to open. Leave this alone unless you know what you're doing (default: stdout stderr)
92
- # @option shell_opts [String] :working_directory the directory to create the shell in
93
- # @option shell_opts [Hash] :env_vars environment variables to set for the shell. For instance;
94
- # :env_vars => {:myvar1 => 'val1', :myvar2 => 'var2'}
95
- # @return [String] The ShellId from the SOAP response. This is our open shell instance on the remote machine.
96
- def open_shell(shell_opts = {}, &block)
97
- i_stream = shell_opts.has_key?(:i_stream) ? shell_opts[:i_stream] : 'stdin'
98
- o_stream = shell_opts.has_key?(:o_stream) ? shell_opts[:o_stream] : 'stdout stderr'
99
- codepage = shell_opts.has_key?(:codepage) ? shell_opts[:codepage] : 65001 # utf8 as default codepage (from https://msdn.microsoft.com/en-us/library/dd317756(VS.85).aspx)
100
- noprofile = shell_opts.has_key?(:noprofile) ? shell_opts[:noprofile] : 'FALSE'
101
- h_opts = { "#{NS_WSMAN_DMTF}:OptionSet" => { "#{NS_WSMAN_DMTF}:Option" => [noprofile, codepage],
102
- :attributes! => {"#{NS_WSMAN_DMTF}:Option" => {'Name' => ['WINRS_NOPROFILE','WINRS_CODEPAGE']}}}}
103
- shell_body = {
104
- "#{NS_WIN_SHELL}:InputStreams" => i_stream,
105
- "#{NS_WIN_SHELL}:OutputStreams" => o_stream
106
- }
107
- shell_body["#{NS_WIN_SHELL}:WorkingDirectory"] = shell_opts[:working_directory] if shell_opts.has_key?(:working_directory)
108
- shell_body["#{NS_WIN_SHELL}:IdleTimeOut"] = shell_opts[:idle_timeout] if(shell_opts.has_key?(:idle_timeout) && shell_opts[:idle_timeout].is_a?(String))
109
- if(shell_opts.has_key?(:env_vars) && shell_opts[:env_vars].is_a?(Hash))
110
- keys = shell_opts[:env_vars].keys
111
- vals = shell_opts[:env_vars].values
112
- shell_body["#{NS_WIN_SHELL}:Environment"] = {
113
- "#{NS_WIN_SHELL}:Variable" => vals,
114
- :attributes! => {"#{NS_WIN_SHELL}:Variable" => {'Name' => keys}}
115
- }
116
- end
117
- builder = Builder::XmlMarkup.new
118
- builder.instruct!(:xml, :encoding => 'UTF-8')
119
- builder.tag! :env, :Envelope, namespaces do |env|
120
- env.tag!(:env, :Header) { |h| h << Gyoku.xml(merge_headers(header,resource_uri_cmd,action_create,h_opts)) }
121
- env.tag! :env, :Body do |body|
122
- body.tag!("#{NS_WIN_SHELL}:Shell") { |s| s << Gyoku.xml(shell_body)}
123
- end
124
- end
125
-
126
- resp_doc = send_message(builder.target!)
127
- shell_id = REXML::XPath.first(resp_doc, "//*[@Name='ShellId']").text
128
-
129
- if block_given?
130
- begin
131
- yield shell_id
132
- ensure
133
- close_shell(shell_id)
134
- end
135
- else
136
- shell_id
137
- end
138
- end
139
-
140
- # Run a command on a machine with an open shell
141
- # @param [String] shell_id The shell id on the remote machine. See #open_shell
142
- # @param [String] command The command to run on the remote machine
143
- # @param [Array<String>] arguments An array of arguments for this command
144
- # @return [String] The CommandId from the SOAP response. This is the ID we need to query in order to get output.
145
- def run_command(shell_id, command, arguments = [], cmd_opts = {}, &block)
146
- consolemode = cmd_opts.has_key?(:console_mode_stdin) ? cmd_opts[:console_mode_stdin] : 'TRUE'
147
- skipcmd = cmd_opts.has_key?(:skip_cmd_shell) ? cmd_opts[:skip_cmd_shell] : 'FALSE'
148
-
149
- h_opts = { "#{NS_WSMAN_DMTF}:OptionSet" => {
150
- "#{NS_WSMAN_DMTF}:Option" => [consolemode, skipcmd],
151
- :attributes! => {"#{NS_WSMAN_DMTF}:Option" => {'Name' => ['WINRS_CONSOLEMODE_STDIN','WINRS_SKIP_CMD_SHELL']}}}
152
- }
153
- body = { "#{NS_WIN_SHELL}:Command" => "\"#{command}\"", "#{NS_WIN_SHELL}:Arguments" => arguments}
154
-
155
- builder = Builder::XmlMarkup.new
156
- builder.instruct!(:xml, :encoding => 'UTF-8')
157
- builder.tag! :env, :Envelope, namespaces do |env|
158
- env.tag!(:env, :Header) { |h| h << Gyoku.xml(merge_headers(header,resource_uri_cmd,action_command,h_opts,selector_shell_id(shell_id))) }
159
- env.tag!(:env, :Body) do |env_body|
160
- env_body.tag!("#{NS_WIN_SHELL}:CommandLine") { |cl| cl << Gyoku.xml(body) }
161
- end
162
- end
163
-
164
- # Grab the command element and unescape any single quotes - issue 69
165
- xml = builder.target!
166
- escaped_cmd = /<#{NS_WIN_SHELL}:Command>(.+)<\/#{NS_WIN_SHELL}:Command>/m.match(xml)[1]
167
- xml[escaped_cmd] = escaped_cmd.gsub(/&#39;/, "'")
168
-
169
- resp_doc = send_message(xml)
170
- command_id = REXML::XPath.first(resp_doc, "//#{NS_WIN_SHELL}:CommandId").text
171
-
172
- if block_given?
173
- begin
174
- yield command_id
175
- ensure
176
- cleanup_command(shell_id, command_id)
177
- end
178
- else
179
- command_id
180
- end
181
- end
182
-
183
- def write_stdin(shell_id, command_id, stdin)
184
- # Signal the Command references to terminate (close stdout/stderr)
185
- body = {
186
- "#{NS_WIN_SHELL}:Send" => {
187
- "#{NS_WIN_SHELL}:Stream" => {
188
- "@Name" => 'stdin',
189
- "@CommandId" => command_id,
190
- :content! => Base64.encode64(stdin)
191
- }
192
- }
193
- }
194
- builder = Builder::XmlMarkup.new
195
- builder.instruct!(:xml, :encoding => 'UTF-8')
196
- builder.tag! :env, :Envelope, namespaces do |env|
197
- env.tag!(:env, :Header) { |h| h << Gyoku.xml(merge_headers(header,resource_uri_cmd,action_send,selector_shell_id(shell_id))) }
198
- env.tag!(:env, :Body) do |env_body|
199
- env_body << Gyoku.xml(body)
200
- end
201
- end
202
- resp = send_message(builder.target!)
203
- true
204
- end
205
-
206
- # Get the Output of the given shell and command
207
- # @param [String] shell_id The shell id on the remote machine. See #open_shell
208
- # @param [String] command_id The command id on the remote machine. See #run_command
209
- # @return [Hash] Returns a Hash with a key :exitcode and :data. Data is an Array of Hashes where the cooresponding key
210
- # is either :stdout or :stderr. The reason it is in an Array so so we can get the output in the order it ocurrs on
211
- # the console.
212
- def get_command_output(shell_id, command_id, &block)
213
- body = { "#{NS_WIN_SHELL}:DesiredStream" => 'stdout stderr',
214
- :attributes! => {"#{NS_WIN_SHELL}:DesiredStream" => {'CommandId' => command_id}}}
215
-
216
- builder = Builder::XmlMarkup.new
217
- builder.instruct!(:xml, :encoding => 'UTF-8')
218
- builder.tag! :env, :Envelope, namespaces do |env|
219
- env.tag!(:env, :Header) { |h| h << Gyoku.xml(merge_headers(header,resource_uri_cmd,action_receive,selector_shell_id(shell_id))) }
220
- env.tag!(:env, :Body) do |env_body|
221
- env_body.tag!("#{NS_WIN_SHELL}:Receive") { |cl| cl << Gyoku.xml(body) }
222
- end
223
- end
224
-
225
- resp_doc = nil
226
- request_msg = builder.target!
227
- done_elems = []
228
- output = Output.new
229
-
230
- while done_elems.empty?
231
- resp_doc = send_get_output_message(request_msg)
232
-
233
- REXML::XPath.match(resp_doc, "//#{NS_WIN_SHELL}:Stream").each do |n|
234
- next if n.text.nil? || n.text.empty?
235
-
236
- # decode and strip off BOM which win 2008R2 applies
237
- stream = { n.attributes['Name'].to_sym => Base64.decode64(n.text).force_encoding('utf-8').sub("\xEF\xBB\xBF", "") }
238
- output[:data] << stream
239
- yield stream[:stdout], stream[:stderr] if block_given?
240
- end
241
-
242
- # We may need to get additional output if the stream has not finished.
243
- # The CommandState will change from Running to Done like so:
244
- # @example
245
- # from...
246
- # <rsp:CommandState CommandId="..." State="http://schemas.microsoft.com/wbem/wsman/1/windows/shell/CommandState/Running"/>
247
- # to...
248
- # <rsp:CommandState CommandId="..." State="http://schemas.microsoft.com/wbem/wsman/1/windows/shell/CommandState/Done">
249
- # <rsp:ExitCode>0</rsp:ExitCode>
250
- # </rsp:CommandState>
251
- done_elems = REXML::XPath.match(resp_doc, "//*[@State='http://schemas.microsoft.com/wbem/wsman/1/windows/shell/CommandState/Done']")
252
- end
253
-
254
- output[:exitcode] = REXML::XPath.first(resp_doc, "//#{NS_WIN_SHELL}:ExitCode").text.to_i
255
- output
256
- end
257
-
258
- # Clean-up after a command.
259
- # @see #run_command
260
- # @param [String] shell_id The shell id on the remote machine. See #open_shell
261
- # @param [String] command_id The command id on the remote machine. See #run_command
262
- # @return [true] This should have more error checking but it just returns true for now.
263
- def cleanup_command(shell_id, command_id)
264
- # Signal the Command references to terminate (close stdout/stderr)
265
- body = { "#{NS_WIN_SHELL}:Code" => 'http://schemas.microsoft.com/wbem/wsman/1/windows/shell/signal/terminate' }
266
- builder = Builder::XmlMarkup.new
267
- builder.instruct!(:xml, :encoding => 'UTF-8')
268
- builder.tag! :env, :Envelope, namespaces do |env|
269
- env.tag!(:env, :Header) { |h| h << Gyoku.xml(merge_headers(header,resource_uri_cmd,action_signal,selector_shell_id(shell_id))) }
270
- env.tag!(:env, :Body) do |env_body|
271
- env_body.tag!("#{NS_WIN_SHELL}:Signal", {'CommandId' => command_id}) { |cl| cl << Gyoku.xml(body) }
272
- end
273
- end
274
- resp = send_message(builder.target!)
275
- true
276
- end
277
-
278
- # Close the shell
279
- # @param [String] shell_id The shell id on the remote machine. See #open_shell
280
- # @return [true] This should have more error checking but it just returns true for now.
281
- def close_shell(shell_id)
282
- builder = Builder::XmlMarkup.new
283
- builder.instruct!(:xml, :encoding => 'UTF-8')
284
-
285
- builder.tag!('env:Envelope', namespaces) do |env|
286
- env.tag!('env:Header') { |h| h << Gyoku.xml(merge_headers(header,resource_uri_cmd,action_delete,selector_shell_id(shell_id))) }
287
- env.tag!('env:Body')
288
- end
289
-
290
- resp = send_message(builder.target!)
291
- true
292
- end
293
-
294
- # Run a CMD command
295
- # @param [String] command The command to run on the remote system
296
- # @param [Array <String>] arguments arguments to the command
297
- # @param [String] an existing and open shell id to reuse
298
- # @return [Hash] :stdout and :stderr
299
- def run_cmd(command, arguments = [], &block)
300
- command_output = nil
301
- open_shell do |shell_id|
302
- run_command(shell_id, command, arguments) do |command_id|
303
- command_output = get_command_output(shell_id, command_id, &block)
304
- end
305
- end
306
- command_output
307
- end
308
- alias :cmd :run_cmd
309
-
310
-
311
- # Run a Powershell script that resides on the local box.
312
- # @param [IO,String] script_file an IO reference for reading the Powershell script or the actual file contents
313
- # @param [String] an existing and open shell id to reuse
314
- # @return [Hash] :stdout and :stderr
315
- def run_powershell_script(script_file, &block)
316
- # if an IO object is passed read it..otherwise assume the contents of the file were passed
317
- script_text = script_file.respond_to?(:read) ? script_file.read : script_file
318
- script = WinRM::PowershellScript.new(script_text)
319
- run_cmd("powershell -encodedCommand #{script.encoded()}", &block)
320
- end
321
- alias :powershell :run_powershell_script
322
-
323
-
324
- # Run a WQL Query
325
- # @see http://msdn.microsoft.com/en-us/library/aa394606(VS.85).aspx
326
- # @param [String] wql The WQL query
327
- # @return [Hash] Returns a Hash that contain the key/value pairs returned from the query.
328
- def run_wql(wql)
329
-
330
- body = { "#{NS_WSMAN_DMTF}:OptimizeEnumeration" => nil,
331
- "#{NS_WSMAN_DMTF}:MaxElements" => '32000',
332
- "#{NS_WSMAN_DMTF}:Filter" => wql,
333
- :attributes! => { "#{NS_WSMAN_DMTF}:Filter" => {'Dialect' => 'http://schemas.microsoft.com/wbem/wsman/1/WQL'}}
334
- }
335
-
336
- builder = Builder::XmlMarkup.new
337
- builder.instruct!(:xml, :encoding => 'UTF-8')
338
- builder.tag! :env, :Envelope, namespaces do |env|
339
- env.tag!(:env, :Header) { |h| h << Gyoku.xml(merge_headers(header,resource_uri_wmi,action_enumerate)) }
340
- env.tag!(:env, :Body) do |env_body|
341
- env_body.tag!("#{NS_ENUM}:Enumerate") { |en| en << Gyoku.xml(body) }
342
- end
343
- end
344
-
345
- resp = send_message(builder.target!)
346
- parser = Nori.new(:parser => :rexml, :advanced_typecasting => false, :convert_tags_to => lambda { |tag| tag.snakecase.to_sym }, :strip_namespaces => true)
347
- hresp = parser.parse(resp.to_s)[:envelope][:body]
348
-
349
- # Normalize items so the type always has an array even if it's just a single item.
350
- items = {}
351
- if hresp[:enumerate_response][:items]
352
- hresp[:enumerate_response][:items].each_pair do |k,v|
353
- if v.is_a?(Array)
354
- items[k] = v
355
- else
356
- items[k] = [v]
357
- end
358
- end
359
- end
360
- items
361
- end
362
- alias :wql :run_wql
363
-
364
- def toggle_nori_type_casting(to)
365
- @logger.warn('toggle_nori_type_casting is deprecated and has no effect, ' +
366
- 'please remove calls to it')
367
- end
368
-
369
- private
370
-
371
- def namespaces
372
- {
373
- 'xmlns:xsd' => 'http://www.w3.org/2001/XMLSchema',
374
- 'xmlns:xsi' => 'http://www.w3.org/2001/XMLSchema-instance',
375
- 'xmlns:env' => 'http://www.w3.org/2003/05/soap-envelope',
376
- 'xmlns:a' => 'http://schemas.xmlsoap.org/ws/2004/08/addressing',
377
- 'xmlns:b' => 'http://schemas.dmtf.org/wbem/wsman/1/cimbinding.xsd',
378
- 'xmlns:n' => 'http://schemas.xmlsoap.org/ws/2004/09/enumeration',
379
- 'xmlns:x' => 'http://schemas.xmlsoap.org/ws/2004/09/transfer',
380
- 'xmlns:w' => 'http://schemas.dmtf.org/wbem/wsman/1/wsman.xsd',
381
- 'xmlns:p' => 'http://schemas.microsoft.com/wbem/wsman/1/wsman.xsd',
382
- 'xmlns:rsp' => 'http://schemas.microsoft.com/wbem/wsman/1/windows/shell',
383
- 'xmlns:cfg' => 'http://schemas.microsoft.com/wbem/wsman/1/config',
384
- }
385
- end
386
-
387
- def header
388
- { "#{NS_ADDRESSING}:To" => "#{@xfer.endpoint.to_s}",
389
- "#{NS_ADDRESSING}:ReplyTo" => {
390
- "#{NS_ADDRESSING}:Address" => 'http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous',
391
- :attributes! => {"#{NS_ADDRESSING}:Address" => {'mustUnderstand' => true}}},
392
- "#{NS_WSMAN_DMTF}:MaxEnvelopeSize" => @max_env_sz,
393
- "#{NS_ADDRESSING}:MessageID" => "uuid:#{UUIDTools::UUID.random_create.to_s.upcase}",
394
- "#{NS_WSMAN_DMTF}:Locale/" => '',
395
- "#{NS_WSMAN_MSFT}:DataLocale/" => '',
396
- "#{NS_WSMAN_DMTF}:OperationTimeout" => @timeout,
397
- :attributes! => {
398
- "#{NS_WSMAN_DMTF}:MaxEnvelopeSize" => {'mustUnderstand' => true},
399
- "#{NS_WSMAN_DMTF}:Locale/" => {'xml:lang' => @locale, 'mustUnderstand' => false},
400
- "#{NS_WSMAN_MSFT}:DataLocale/" => {'xml:lang' => @locale, 'mustUnderstand' => false}
401
- }}
402
- end
403
-
404
- # merge the various header hashes and make sure we carry all of the attributes
405
- # through instead of overwriting them.
406
- def merge_headers(*headers)
407
- hdr = {}
408
- headers.each do |h|
409
- hdr.merge!(h) do |k,v1,v2|
410
- v1.merge!(v2) if k == :attributes!
411
- end
412
- end
413
- hdr
414
- end
415
-
416
- def send_get_output_message(message)
417
- send_message(message)
418
- rescue WinRMWSManFault => e
419
- # If no output is available before the wsman:OperationTimeout expires,
420
- # the server MUST return a WSManFault with the Code attribute equal to
421
- # 2150858793. When the client receives this fault, it SHOULD issue
422
- # another Receive request.
423
- # http://msdn.microsoft.com/en-us/library/cc251676.aspx
424
- if e.fault_code == '2150858793'
425
- retry
426
- else
427
- raise
428
- end
429
- end
430
-
431
- def send_message(message)
432
- @xfer.send_request(message)
433
- end
434
-
435
-
436
- # Helper methods for SOAP Headers
437
-
438
- def resource_uri_cmd
439
- {"#{NS_WSMAN_DMTF}:ResourceURI" => 'http://schemas.microsoft.com/wbem/wsman/1/windows/shell/cmd',
440
- :attributes! => {"#{NS_WSMAN_DMTF}:ResourceURI" => {'mustUnderstand' => true}}}
441
- end
442
-
443
- def resource_uri_wmi(namespace = 'root/cimv2/*')
444
- {"#{NS_WSMAN_DMTF}:ResourceURI" => "http://schemas.microsoft.com/wbem/wsman/1/wmi/#{namespace}",
445
- :attributes! => {"#{NS_WSMAN_DMTF}:ResourceURI" => {'mustUnderstand' => true}}}
446
- end
447
-
448
- def action_create
449
- {"#{NS_ADDRESSING}:Action" => 'http://schemas.xmlsoap.org/ws/2004/09/transfer/Create',
450
- :attributes! => {"#{NS_ADDRESSING}:Action" => {'mustUnderstand' => true}}}
451
- end
452
-
453
- def action_delete
454
- {"#{NS_ADDRESSING}:Action" => 'http://schemas.xmlsoap.org/ws/2004/09/transfer/Delete',
455
- :attributes! => {"#{NS_ADDRESSING}:Action" => {'mustUnderstand' => true}}}
456
- end
457
-
458
- def action_command
459
- {"#{NS_ADDRESSING}:Action" => 'http://schemas.microsoft.com/wbem/wsman/1/windows/shell/Command',
460
- :attributes! => {"#{NS_ADDRESSING}:Action" => {'mustUnderstand' => true}}}
461
- end
462
-
463
- def action_receive
464
- {"#{NS_ADDRESSING}:Action" => 'http://schemas.microsoft.com/wbem/wsman/1/windows/shell/Receive',
465
- :attributes! => {"#{NS_ADDRESSING}:Action" => {'mustUnderstand' => true}}}
466
- end
467
-
468
- def action_signal
469
- {"#{NS_ADDRESSING}:Action" => 'http://schemas.microsoft.com/wbem/wsman/1/windows/shell/Signal',
470
- :attributes! => {"#{NS_ADDRESSING}:Action" => {'mustUnderstand' => true}}}
471
- end
472
-
473
- def action_send
474
- {"#{NS_ADDRESSING}:Action" => 'http://schemas.microsoft.com/wbem/wsman/1/windows/shell/Send',
475
- :attributes! => {"#{NS_ADDRESSING}:Action" => {'mustUnderstand' => true}}}
476
- end
477
-
478
- def action_enumerate
479
- {"#{NS_ADDRESSING}:Action" => 'http://schemas.xmlsoap.org/ws/2004/09/enumeration/Enumerate',
480
- :attributes! => {"#{NS_ADDRESSING}:Action" => {'mustUnderstand' => true}}}
481
- end
482
-
483
- def selector_shell_id(shell_id)
484
- {"#{NS_WSMAN_DMTF}:SelectorSet" =>
485
- {"#{NS_WSMAN_DMTF}:Selector" => shell_id, :attributes! => {"#{NS_WSMAN_DMTF}:Selector" => {'Name' => 'ShellId'}}}
486
- }
487
- end
488
-
489
- end # WinRMWebService
490
- end # WinRM
1
+ # encoding: UTF-8
2
+ #
3
+ # Copyright 2010 Dan Wanek <dan.wanek@gmail.com>
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+
17
+ require 'nori'
18
+ require 'rexml/document'
19
+ require_relative 'helpers/powershell_script'
20
+
21
+ module WinRM
22
+ # This is the main class that does the SOAP request/response logic. There are a few helper
23
+ # classes, but pretty much everything comes through here first.
24
+ class WinRMWebService
25
+ DEFAULT_TIMEOUT = 'PT60S'
26
+ DEFAULT_MAX_ENV_SIZE = 153600
27
+ DEFAULT_LOCALE = 'en-US'
28
+
29
+ attr_reader :endpoint, :timeout
30
+
31
+ # @param [String,URI] endpoint the WinRM webservice endpoint
32
+ # @param [Symbol] transport either :kerberos(default)/:ssl/:plaintext
33
+ # @param [Hash] opts Misc opts for the various transports.
34
+ # @see WinRM::HTTP::HttpTransport
35
+ # @see WinRM::HTTP::HttpGSSAPI
36
+ # @see WinRM::HTTP::HttpSSL
37
+ def initialize(endpoint, transport = :kerberos, opts = {})
38
+ @endpoint = endpoint
39
+ @timeout = DEFAULT_TIMEOUT
40
+ @max_env_sz = DEFAULT_MAX_ENV_SIZE
41
+ @locale = DEFAULT_LOCALE
42
+ @logger = Logging.logger[self]
43
+ case transport
44
+ when :kerberos
45
+ require 'gssapi'
46
+ require 'gssapi/extensions'
47
+ @xfer = HTTP::HttpGSSAPI.new(endpoint, opts[:realm], opts[:service], opts[:keytab], opts)
48
+ when :plaintext
49
+ @xfer = HTTP::HttpPlaintext.new(endpoint, opts[:user], opts[:pass], opts)
50
+ when :ssl
51
+ @xfer = HTTP::HttpSSL.new(endpoint, opts[:user], opts[:pass], opts[:ca_trust_path], opts)
52
+ else
53
+ raise "Invalid transport '#{transport}' specified, expected: kerberos, plaintext, ssl."
54
+ end
55
+ end
56
+
57
+ # Operation timeout.
58
+ #
59
+ # Unless specified the client receive timeout will be 10s + the operation
60
+ # timeout.
61
+ #
62
+ # @see http://msdn.microsoft.com/en-us/library/ee916629(v=PROT.13).aspx
63
+ #
64
+ # @param [Fixnum] The number of seconds to set the WinRM operation timeout
65
+ # @param [Fixnum] The number of seconds to set the Ruby receive timeout
66
+ # @return [String] The ISO 8601 formatted operation timeout
67
+ def set_timeout(op_timeout_sec, receive_timeout_sec=nil)
68
+ @timeout = Iso8601Duration.sec_to_dur(op_timeout_sec)
69
+ @xfer.receive_timeout = receive_timeout_sec || op_timeout_sec + 10
70
+ @timeout
71
+ end
72
+ alias :op_timeout :set_timeout
73
+
74
+ # Max envelope size
75
+ # @see http://msdn.microsoft.com/en-us/library/ee916127(v=PROT.13).aspx
76
+ # @param [Fixnum] byte_sz the max size in bytes to allow for the response
77
+ def max_env_size(byte_sz)
78
+ @max_env_sz = byte_sz
79
+ end
80
+
81
+ # Set the locale
82
+ # @see http://msdn.microsoft.com/en-us/library/gg567404(v=PROT.13).aspx
83
+ # @param [String] locale the locale to set for future messages
84
+ def locale(locale)
85
+ @locale = locale
86
+ end
87
+
88
+ # Create a Shell on the destination host
89
+ # @param [Hash<optional>] shell_opts additional shell options you can pass
90
+ # @option shell_opts [String] :i_stream Which input stream to open. Leave this alone unless you know what you're doing (default: stdin)
91
+ # @option shell_opts [String] :o_stream Which output stream to open. Leave this alone unless you know what you're doing (default: stdout stderr)
92
+ # @option shell_opts [String] :working_directory the directory to create the shell in
93
+ # @option shell_opts [Hash] :env_vars environment variables to set for the shell. For instance;
94
+ # :env_vars => {:myvar1 => 'val1', :myvar2 => 'var2'}
95
+ # @return [String] The ShellId from the SOAP response. This is our open shell instance on the remote machine.
96
+ def open_shell(shell_opts = {}, &block)
97
+ i_stream = shell_opts.has_key?(:i_stream) ? shell_opts[:i_stream] : 'stdin'
98
+ o_stream = shell_opts.has_key?(:o_stream) ? shell_opts[:o_stream] : 'stdout stderr'
99
+ codepage = shell_opts.has_key?(:codepage) ? shell_opts[:codepage] : 65001 # utf8 as default codepage (from https://msdn.microsoft.com/en-us/library/dd317756(VS.85).aspx)
100
+ noprofile = shell_opts.has_key?(:noprofile) ? shell_opts[:noprofile] : 'FALSE'
101
+ h_opts = { "#{NS_WSMAN_DMTF}:OptionSet" => { "#{NS_WSMAN_DMTF}:Option" => [noprofile, codepage],
102
+ :attributes! => {"#{NS_WSMAN_DMTF}:Option" => {'Name' => ['WINRS_NOPROFILE','WINRS_CODEPAGE']}}}}
103
+ shell_body = {
104
+ "#{NS_WIN_SHELL}:InputStreams" => i_stream,
105
+ "#{NS_WIN_SHELL}:OutputStreams" => o_stream
106
+ }
107
+ shell_body["#{NS_WIN_SHELL}:WorkingDirectory"] = shell_opts[:working_directory] if shell_opts.has_key?(:working_directory)
108
+ shell_body["#{NS_WIN_SHELL}:IdleTimeOut"] = shell_opts[:idle_timeout] if(shell_opts.has_key?(:idle_timeout) && shell_opts[:idle_timeout].is_a?(String))
109
+ if(shell_opts.has_key?(:env_vars) && shell_opts[:env_vars].is_a?(Hash))
110
+ keys = shell_opts[:env_vars].keys
111
+ vals = shell_opts[:env_vars].values
112
+ shell_body["#{NS_WIN_SHELL}:Environment"] = {
113
+ "#{NS_WIN_SHELL}:Variable" => vals,
114
+ :attributes! => {"#{NS_WIN_SHELL}:Variable" => {'Name' => keys}}
115
+ }
116
+ end
117
+ builder = Builder::XmlMarkup.new
118
+ builder.instruct!(:xml, :encoding => 'UTF-8')
119
+ builder.tag! :env, :Envelope, namespaces do |env|
120
+ env.tag!(:env, :Header) { |h| h << Gyoku.xml(merge_headers(header,resource_uri_cmd,action_create,h_opts)) }
121
+ env.tag! :env, :Body do |body|
122
+ body.tag!("#{NS_WIN_SHELL}:Shell") { |s| s << Gyoku.xml(shell_body)}
123
+ end
124
+ end
125
+
126
+ resp_doc = send_message(builder.target!)
127
+ shell_id = REXML::XPath.first(resp_doc, "//*[@Name='ShellId']").text
128
+
129
+ if block_given?
130
+ begin
131
+ yield shell_id
132
+ ensure
133
+ close_shell(shell_id)
134
+ end
135
+ else
136
+ shell_id
137
+ end
138
+ end
139
+
140
+ # Run a command on a machine with an open shell
141
+ # @param [String] shell_id The shell id on the remote machine. See #open_shell
142
+ # @param [String] command The command to run on the remote machine
143
+ # @param [Array<String>] arguments An array of arguments for this command
144
+ # @return [String] The CommandId from the SOAP response. This is the ID we need to query in order to get output.
145
+ def run_command(shell_id, command, arguments = [], cmd_opts = {}, &block)
146
+ consolemode = cmd_opts.has_key?(:console_mode_stdin) ? cmd_opts[:console_mode_stdin] : 'TRUE'
147
+ skipcmd = cmd_opts.has_key?(:skip_cmd_shell) ? cmd_opts[:skip_cmd_shell] : 'FALSE'
148
+
149
+ h_opts = { "#{NS_WSMAN_DMTF}:OptionSet" => {
150
+ "#{NS_WSMAN_DMTF}:Option" => [consolemode, skipcmd],
151
+ :attributes! => {"#{NS_WSMAN_DMTF}:Option" => {'Name' => ['WINRS_CONSOLEMODE_STDIN','WINRS_SKIP_CMD_SHELL']}}}
152
+ }
153
+ body = { "#{NS_WIN_SHELL}:Command" => "\"#{command}\"", "#{NS_WIN_SHELL}:Arguments" => arguments}
154
+
155
+ builder = Builder::XmlMarkup.new
156
+ builder.instruct!(:xml, :encoding => 'UTF-8')
157
+ builder.tag! :env, :Envelope, namespaces do |env|
158
+ env.tag!(:env, :Header) { |h| h << Gyoku.xml(merge_headers(header,resource_uri_cmd,action_command,h_opts,selector_shell_id(shell_id))) }
159
+ env.tag!(:env, :Body) do |env_body|
160
+ env_body.tag!("#{NS_WIN_SHELL}:CommandLine") { |cl| cl << Gyoku.xml(body) }
161
+ end
162
+ end
163
+
164
+ # Grab the command element and unescape any single quotes - issue 69
165
+ xml = builder.target!
166
+ escaped_cmd = /<#{NS_WIN_SHELL}:Command>(.+)<\/#{NS_WIN_SHELL}:Command>/m.match(xml)[1]
167
+ xml[escaped_cmd] = escaped_cmd.gsub(/&#39;/, "'")
168
+
169
+ resp_doc = send_message(xml)
170
+ command_id = REXML::XPath.first(resp_doc, "//#{NS_WIN_SHELL}:CommandId").text
171
+
172
+ if block_given?
173
+ begin
174
+ yield command_id
175
+ ensure
176
+ cleanup_command(shell_id, command_id)
177
+ end
178
+ else
179
+ command_id
180
+ end
181
+ end
182
+
183
+ def write_stdin(shell_id, command_id, stdin)
184
+ # Signal the Command references to terminate (close stdout/stderr)
185
+ body = {
186
+ "#{NS_WIN_SHELL}:Send" => {
187
+ "#{NS_WIN_SHELL}:Stream" => {
188
+ "@Name" => 'stdin',
189
+ "@CommandId" => command_id,
190
+ :content! => Base64.encode64(stdin)
191
+ }
192
+ }
193
+ }
194
+ builder = Builder::XmlMarkup.new
195
+ builder.instruct!(:xml, :encoding => 'UTF-8')
196
+ builder.tag! :env, :Envelope, namespaces do |env|
197
+ env.tag!(:env, :Header) { |h| h << Gyoku.xml(merge_headers(header,resource_uri_cmd,action_send,selector_shell_id(shell_id))) }
198
+ env.tag!(:env, :Body) do |env_body|
199
+ env_body << Gyoku.xml(body)
200
+ end
201
+ end
202
+ resp = send_message(builder.target!)
203
+ true
204
+ end
205
+
206
+ # Get the Output of the given shell and command
207
+ # @param [String] shell_id The shell id on the remote machine. See #open_shell
208
+ # @param [String] command_id The command id on the remote machine. See #run_command
209
+ # @return [Hash] Returns a Hash with a key :exitcode and :data. Data is an Array of Hashes where the cooresponding key
210
+ # is either :stdout or :stderr. The reason it is in an Array so so we can get the output in the order it ocurrs on
211
+ # the console.
212
+ def get_command_output(shell_id, command_id, &block)
213
+ body = { "#{NS_WIN_SHELL}:DesiredStream" => 'stdout stderr',
214
+ :attributes! => {"#{NS_WIN_SHELL}:DesiredStream" => {'CommandId' => command_id}}}
215
+
216
+ builder = Builder::XmlMarkup.new
217
+ builder.instruct!(:xml, :encoding => 'UTF-8')
218
+ builder.tag! :env, :Envelope, namespaces do |env|
219
+ env.tag!(:env, :Header) { |h| h << Gyoku.xml(merge_headers(header,resource_uri_cmd,action_receive,selector_shell_id(shell_id))) }
220
+ env.tag!(:env, :Body) do |env_body|
221
+ env_body.tag!("#{NS_WIN_SHELL}:Receive") { |cl| cl << Gyoku.xml(body) }
222
+ end
223
+ end
224
+
225
+ resp_doc = nil
226
+ request_msg = builder.target!
227
+ done_elems = []
228
+ output = Output.new
229
+
230
+ while done_elems.empty?
231
+ resp_doc = send_get_output_message(request_msg)
232
+
233
+ REXML::XPath.match(resp_doc, "//#{NS_WIN_SHELL}:Stream").each do |n|
234
+ next if n.text.nil? || n.text.empty?
235
+
236
+ # decode and strip off BOM which win 2008R2 applies
237
+ stream = { n.attributes['Name'].to_sym => Base64.decode64(n.text).force_encoding('utf-8').sub("\xEF\xBB\xBF", "") }
238
+ output[:data] << stream
239
+ yield stream[:stdout], stream[:stderr] if block_given?
240
+ end
241
+
242
+ # We may need to get additional output if the stream has not finished.
243
+ # The CommandState will change from Running to Done like so:
244
+ # @example
245
+ # from...
246
+ # <rsp:CommandState CommandId="..." State="http://schemas.microsoft.com/wbem/wsman/1/windows/shell/CommandState/Running"/>
247
+ # to...
248
+ # <rsp:CommandState CommandId="..." State="http://schemas.microsoft.com/wbem/wsman/1/windows/shell/CommandState/Done">
249
+ # <rsp:ExitCode>0</rsp:ExitCode>
250
+ # </rsp:CommandState>
251
+ done_elems = REXML::XPath.match(resp_doc, "//*[@State='http://schemas.microsoft.com/wbem/wsman/1/windows/shell/CommandState/Done']")
252
+ end
253
+
254
+ output[:exitcode] = REXML::XPath.first(resp_doc, "//#{NS_WIN_SHELL}:ExitCode").text.to_i
255
+ output
256
+ end
257
+
258
+ # Clean-up after a command.
259
+ # @see #run_command
260
+ # @param [String] shell_id The shell id on the remote machine. See #open_shell
261
+ # @param [String] command_id The command id on the remote machine. See #run_command
262
+ # @return [true] This should have more error checking but it just returns true for now.
263
+ def cleanup_command(shell_id, command_id)
264
+ # Signal the Command references to terminate (close stdout/stderr)
265
+ body = { "#{NS_WIN_SHELL}:Code" => 'http://schemas.microsoft.com/wbem/wsman/1/windows/shell/signal/terminate' }
266
+ builder = Builder::XmlMarkup.new
267
+ builder.instruct!(:xml, :encoding => 'UTF-8')
268
+ builder.tag! :env, :Envelope, namespaces do |env|
269
+ env.tag!(:env, :Header) { |h| h << Gyoku.xml(merge_headers(header,resource_uri_cmd,action_signal,selector_shell_id(shell_id))) }
270
+ env.tag!(:env, :Body) do |env_body|
271
+ env_body.tag!("#{NS_WIN_SHELL}:Signal", {'CommandId' => command_id}) { |cl| cl << Gyoku.xml(body) }
272
+ end
273
+ end
274
+ resp = send_message(builder.target!)
275
+ true
276
+ end
277
+
278
+ # Close the shell
279
+ # @param [String] shell_id The shell id on the remote machine. See #open_shell
280
+ # @return [true] This should have more error checking but it just returns true for now.
281
+ def close_shell(shell_id)
282
+ builder = Builder::XmlMarkup.new
283
+ builder.instruct!(:xml, :encoding => 'UTF-8')
284
+
285
+ builder.tag!('env:Envelope', namespaces) do |env|
286
+ env.tag!('env:Header') { |h| h << Gyoku.xml(merge_headers(header,resource_uri_cmd,action_delete,selector_shell_id(shell_id))) }
287
+ env.tag!('env:Body')
288
+ end
289
+
290
+ resp = send_message(builder.target!)
291
+ true
292
+ end
293
+
294
+ # Run a CMD command
295
+ # @param [String] command The command to run on the remote system
296
+ # @param [Array <String>] arguments arguments to the command
297
+ # @param [String] an existing and open shell id to reuse
298
+ # @return [Hash] :stdout and :stderr
299
+ def run_cmd(command, arguments = [], &block)
300
+ command_output = nil
301
+ open_shell do |shell_id|
302
+ run_command(shell_id, command, arguments) do |command_id|
303
+ command_output = get_command_output(shell_id, command_id, &block)
304
+ end
305
+ end
306
+ command_output
307
+ end
308
+ alias :cmd :run_cmd
309
+
310
+
311
+ # Run a Powershell script that resides on the local box.
312
+ # @param [IO,String] script_file an IO reference for reading the Powershell script or the actual file contents
313
+ # @param [String] an existing and open shell id to reuse
314
+ # @return [Hash] :stdout and :stderr
315
+ def run_powershell_script(script_file, &block)
316
+ # if an IO object is passed read it..otherwise assume the contents of the file were passed
317
+ script_text = script_file.respond_to?(:read) ? script_file.read : script_file
318
+ script = WinRM::PowershellScript.new(script_text)
319
+ run_cmd("powershell -encodedCommand #{script.encoded()}", &block)
320
+ end
321
+ alias :powershell :run_powershell_script
322
+
323
+
324
+ # Run a WQL Query
325
+ # @see http://msdn.microsoft.com/en-us/library/aa394606(VS.85).aspx
326
+ # @param [String] wql The WQL query
327
+ # @return [Hash] Returns a Hash that contain the key/value pairs returned from the query.
328
+ def run_wql(wql)
329
+
330
+ body = { "#{NS_WSMAN_DMTF}:OptimizeEnumeration" => nil,
331
+ "#{NS_WSMAN_DMTF}:MaxElements" => '32000',
332
+ "#{NS_WSMAN_DMTF}:Filter" => wql,
333
+ :attributes! => { "#{NS_WSMAN_DMTF}:Filter" => {'Dialect' => 'http://schemas.microsoft.com/wbem/wsman/1/WQL'}}
334
+ }
335
+
336
+ builder = Builder::XmlMarkup.new
337
+ builder.instruct!(:xml, :encoding => 'UTF-8')
338
+ builder.tag! :env, :Envelope, namespaces do |env|
339
+ env.tag!(:env, :Header) { |h| h << Gyoku.xml(merge_headers(header,resource_uri_wmi,action_enumerate)) }
340
+ env.tag!(:env, :Body) do |env_body|
341
+ env_body.tag!("#{NS_ENUM}:Enumerate") { |en| en << Gyoku.xml(body) }
342
+ end
343
+ end
344
+
345
+ resp = send_message(builder.target!)
346
+ parser = Nori.new(:parser => :rexml, :advanced_typecasting => false, :convert_tags_to => lambda { |tag| tag.snakecase.to_sym }, :strip_namespaces => true)
347
+ hresp = parser.parse(resp.to_s)[:envelope][:body]
348
+
349
+ # Normalize items so the type always has an array even if it's just a single item.
350
+ items = {}
351
+ if hresp[:enumerate_response][:items]
352
+ hresp[:enumerate_response][:items].each_pair do |k,v|
353
+ if v.is_a?(Array)
354
+ items[k] = v
355
+ else
356
+ items[k] = [v]
357
+ end
358
+ end
359
+ end
360
+ items
361
+ end
362
+ alias :wql :run_wql
363
+
364
+ def toggle_nori_type_casting(to)
365
+ @logger.warn('toggle_nori_type_casting is deprecated and has no effect, ' +
366
+ 'please remove calls to it')
367
+ end
368
+
369
+ private
370
+
371
+ def namespaces
372
+ {
373
+ 'xmlns:xsd' => 'http://www.w3.org/2001/XMLSchema',
374
+ 'xmlns:xsi' => 'http://www.w3.org/2001/XMLSchema-instance',
375
+ 'xmlns:env' => 'http://www.w3.org/2003/05/soap-envelope',
376
+ 'xmlns:a' => 'http://schemas.xmlsoap.org/ws/2004/08/addressing',
377
+ 'xmlns:b' => 'http://schemas.dmtf.org/wbem/wsman/1/cimbinding.xsd',
378
+ 'xmlns:n' => 'http://schemas.xmlsoap.org/ws/2004/09/enumeration',
379
+ 'xmlns:x' => 'http://schemas.xmlsoap.org/ws/2004/09/transfer',
380
+ 'xmlns:w' => 'http://schemas.dmtf.org/wbem/wsman/1/wsman.xsd',
381
+ 'xmlns:p' => 'http://schemas.microsoft.com/wbem/wsman/1/wsman.xsd',
382
+ 'xmlns:rsp' => 'http://schemas.microsoft.com/wbem/wsman/1/windows/shell',
383
+ 'xmlns:cfg' => 'http://schemas.microsoft.com/wbem/wsman/1/config',
384
+ }
385
+ end
386
+
387
+ def header
388
+ { "#{NS_ADDRESSING}:To" => "#{@xfer.endpoint.to_s}",
389
+ "#{NS_ADDRESSING}:ReplyTo" => {
390
+ "#{NS_ADDRESSING}:Address" => 'http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous',
391
+ :attributes! => {"#{NS_ADDRESSING}:Address" => {'mustUnderstand' => true}}},
392
+ "#{NS_WSMAN_DMTF}:MaxEnvelopeSize" => @max_env_sz,
393
+ "#{NS_ADDRESSING}:MessageID" => "uuid:#{UUIDTools::UUID.random_create.to_s.upcase}",
394
+ "#{NS_WSMAN_DMTF}:Locale/" => '',
395
+ "#{NS_WSMAN_MSFT}:DataLocale/" => '',
396
+ "#{NS_WSMAN_DMTF}:OperationTimeout" => @timeout,
397
+ :attributes! => {
398
+ "#{NS_WSMAN_DMTF}:MaxEnvelopeSize" => {'mustUnderstand' => true},
399
+ "#{NS_WSMAN_DMTF}:Locale/" => {'xml:lang' => @locale, 'mustUnderstand' => false},
400
+ "#{NS_WSMAN_MSFT}:DataLocale/" => {'xml:lang' => @locale, 'mustUnderstand' => false}
401
+ }}
402
+ end
403
+
404
+ # merge the various header hashes and make sure we carry all of the attributes
405
+ # through instead of overwriting them.
406
+ def merge_headers(*headers)
407
+ hdr = {}
408
+ headers.each do |h|
409
+ hdr.merge!(h) do |k,v1,v2|
410
+ v1.merge!(v2) if k == :attributes!
411
+ end
412
+ end
413
+ hdr
414
+ end
415
+
416
+ def send_get_output_message(message)
417
+ send_message(message)
418
+ rescue WinRMWSManFault => e
419
+ # If no output is available before the wsman:OperationTimeout expires,
420
+ # the server MUST return a WSManFault with the Code attribute equal to
421
+ # 2150858793. When the client receives this fault, it SHOULD issue
422
+ # another Receive request.
423
+ # http://msdn.microsoft.com/en-us/library/cc251676.aspx
424
+ if e.fault_code == '2150858793'
425
+ retry
426
+ else
427
+ raise
428
+ end
429
+ end
430
+
431
+ def send_message(message)
432
+ @xfer.send_request(message)
433
+ end
434
+
435
+
436
+ # Helper methods for SOAP Headers
437
+
438
+ def resource_uri_cmd
439
+ {"#{NS_WSMAN_DMTF}:ResourceURI" => 'http://schemas.microsoft.com/wbem/wsman/1/windows/shell/cmd',
440
+ :attributes! => {"#{NS_WSMAN_DMTF}:ResourceURI" => {'mustUnderstand' => true}}}
441
+ end
442
+
443
+ def resource_uri_wmi(namespace = 'root/cimv2/*')
444
+ {"#{NS_WSMAN_DMTF}:ResourceURI" => "http://schemas.microsoft.com/wbem/wsman/1/wmi/#{namespace}",
445
+ :attributes! => {"#{NS_WSMAN_DMTF}:ResourceURI" => {'mustUnderstand' => true}}}
446
+ end
447
+
448
+ def action_create
449
+ {"#{NS_ADDRESSING}:Action" => 'http://schemas.xmlsoap.org/ws/2004/09/transfer/Create',
450
+ :attributes! => {"#{NS_ADDRESSING}:Action" => {'mustUnderstand' => true}}}
451
+ end
452
+
453
+ def action_delete
454
+ {"#{NS_ADDRESSING}:Action" => 'http://schemas.xmlsoap.org/ws/2004/09/transfer/Delete',
455
+ :attributes! => {"#{NS_ADDRESSING}:Action" => {'mustUnderstand' => true}}}
456
+ end
457
+
458
+ def action_command
459
+ {"#{NS_ADDRESSING}:Action" => 'http://schemas.microsoft.com/wbem/wsman/1/windows/shell/Command',
460
+ :attributes! => {"#{NS_ADDRESSING}:Action" => {'mustUnderstand' => true}}}
461
+ end
462
+
463
+ def action_receive
464
+ {"#{NS_ADDRESSING}:Action" => 'http://schemas.microsoft.com/wbem/wsman/1/windows/shell/Receive',
465
+ :attributes! => {"#{NS_ADDRESSING}:Action" => {'mustUnderstand' => true}}}
466
+ end
467
+
468
+ def action_signal
469
+ {"#{NS_ADDRESSING}:Action" => 'http://schemas.microsoft.com/wbem/wsman/1/windows/shell/Signal',
470
+ :attributes! => {"#{NS_ADDRESSING}:Action" => {'mustUnderstand' => true}}}
471
+ end
472
+
473
+ def action_send
474
+ {"#{NS_ADDRESSING}:Action" => 'http://schemas.microsoft.com/wbem/wsman/1/windows/shell/Send',
475
+ :attributes! => {"#{NS_ADDRESSING}:Action" => {'mustUnderstand' => true}}}
476
+ end
477
+
478
+ def action_enumerate
479
+ {"#{NS_ADDRESSING}:Action" => 'http://schemas.xmlsoap.org/ws/2004/09/enumeration/Enumerate',
480
+ :attributes! => {"#{NS_ADDRESSING}:Action" => {'mustUnderstand' => true}}}
481
+ end
482
+
483
+ def selector_shell_id(shell_id)
484
+ {"#{NS_WSMAN_DMTF}:SelectorSet" =>
485
+ {"#{NS_WSMAN_DMTF}:Selector" => shell_id, :attributes! => {"#{NS_WSMAN_DMTF}:Selector" => {'Name' => 'ShellId'}}}
486
+ }
487
+ end
488
+
489
+ end # WinRMWebService
490
+ end # WinRM