winrm 1.3.6 → 1.4.0

Sign up to get free protection for your applications and to get access to all the features.
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