winrm 1.7.3 → 1.8.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +10 -10
  3. data/.rspec +3 -3
  4. data/.travis.yml +12 -12
  5. data/Gemfile +9 -9
  6. data/LICENSE +202 -202
  7. data/README.md +213 -194
  8. data/Rakefile +36 -36
  9. data/Vagrantfile +9 -9
  10. data/WinrmAppveyor.psm1 +32 -0
  11. data/appveyor.yml +51 -42
  12. data/bin/rwinrm +97 -97
  13. data/changelog.md +3 -0
  14. data/lib/winrm.rb +42 -42
  15. data/lib/winrm/command_executor.rb +6 -2
  16. data/lib/winrm/command_output_decoder.rb +53 -53
  17. data/lib/winrm/exceptions/exceptions.rb +57 -57
  18. data/lib/winrm/helpers/iso8601_duration.rb +58 -58
  19. data/lib/winrm/helpers/powershell_script.rb +42 -42
  20. data/lib/winrm/http/response_handler.rb +82 -82
  21. data/lib/winrm/http/transport.rb +17 -0
  22. data/lib/winrm/output.rb +43 -43
  23. data/lib/winrm/soap_provider.rb +39 -39
  24. data/lib/winrm/version.rb +1 -1
  25. data/lib/winrm/winrm_service.rb +550 -547
  26. data/preamble +17 -17
  27. data/spec/auth_timeout_spec.rb +16 -16
  28. data/spec/cmd_spec.rb +102 -102
  29. data/spec/command_executor_spec.rb +27 -10
  30. data/spec/command_output_decoder_spec.rb +37 -37
  31. data/spec/config-example.yml +19 -19
  32. data/spec/exception_spec.rb +50 -50
  33. data/spec/issue_184_spec.rb +67 -67
  34. data/spec/issue_59_spec.rb +23 -23
  35. data/spec/matchers.rb +74 -74
  36. data/spec/output_spec.rb +110 -110
  37. data/spec/powershell_spec.rb +97 -97
  38. data/spec/response_handler_spec.rb +59 -59
  39. data/spec/spec_helper.rb +73 -73
  40. data/spec/stubs/responses/get_command_output_response.xml.erb +13 -13
  41. data/spec/stubs/responses/open_shell_v1.xml +19 -19
  42. data/spec/stubs/responses/open_shell_v2.xml +20 -20
  43. data/spec/stubs/responses/soap_fault_v1.xml +36 -36
  44. data/spec/stubs/responses/soap_fault_v2.xml +42 -42
  45. data/spec/stubs/responses/wmi_error_v2.xml +41 -41
  46. data/spec/transport_spec.rb +139 -124
  47. data/spec/winrm_options_spec.rb +76 -76
  48. data/spec/winrm_primitives_spec.rb +51 -51
  49. data/spec/wql_spec.rb +14 -14
  50. data/winrm.gemspec +40 -40
  51. metadata +4 -3
@@ -44,6 +44,11 @@ module WinRM
44
44
  log_soap_message(message)
45
45
  hdr = { 'Content-Type' => 'application/soap+xml;charset=UTF-8',
46
46
  'Content-Length' => message.bytesize }
47
+ # We need to add this header if using Client Certificate authentication
48
+ unless @httpcli.ssl_config.client_cert.nil?
49
+ hdr['Authorization'] = 'http://schemas.dmtf.org/wbem/wsman/1/wsman/secprofile/https/mutual'
50
+ end
51
+
47
52
  resp = @httpcli.post(@endpoint, message, hdr)
48
53
  log_soap_message(resp.http_body.content)
49
54
  verify_ssl_fingerprint(resp.peer_cert)
@@ -242,6 +247,18 @@ module WinRM
242
247
  end
243
248
  end
244
249
 
250
+ # Uses Client Certificate to authenticate and SSL to secure the transport
251
+ class ClientCertAuthSSL < HttpTransport
252
+ def initialize(endpoint, client_cert, client_key, key_pass, opts)
253
+ super(endpoint)
254
+ @httpcli.ssl_config.set_client_cert_file(client_cert, client_key, key_pass)
255
+ @httpcli.www_auth.instance_variable_set('@authenticator', [])
256
+ no_ssl_peer_verification! if opts[:no_ssl_peer_verification]
257
+ @ssl_peer_fingerprint = opts[:ssl_peer_fingerprint]
258
+ @httpcli.ssl_config.set_trust_ca(opts[:ca_trust_path]) if opts[:ca_trust_path]
259
+ end
260
+ end
261
+
245
262
  # Uses Kerberos/GSSAPI to authenticate and encrypt messages
246
263
  # rubocop:disable Metrics/ClassLength
247
264
  class HttpGSSAPI < HttpTransport
@@ -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,39 +1,39 @@
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 'base64'
21
-
22
- # SOAP constants for WinRM
23
- module WinRM
24
- NS_SOAP_ENV = 's' # http://www.w3.org/2003/05/soap-envelope
25
- NS_ADDRESSING = 'a' # http://schemas.xmlsoap.org/ws/2004/08/addressing
26
- NS_CIMBINDING = 'b' # http://schemas.dmtf.org/wbem/wsman/1/cimbinding.xsd
27
- NS_ENUM = 'n' # http://schemas.xmlsoap.org/ws/2004/09/enumeration
28
- NS_TRANSFER = 'x' # http://schemas.xmlsoap.org/ws/2004/09/transfer
29
- NS_WSMAN_DMTF = 'w' # http://schemas.dmtf.org/wbem/wsman/1/wsman.xsd
30
- NS_WSMAN_MSFT = 'p' # http://schemas.microsoft.com/wbem/wsman/1/wsman.xsd
31
- NS_SCHEMA_INST = 'xsi' # http://www.w3.org/2001/XMLSchema-instance
32
- NS_WIN_SHELL = 'rsp' # http://schemas.microsoft.com/wbem/wsman/1/windows/shell
33
- NS_WSMAN_FAULT = 'f' # http://schemas.microsoft.com/wbem/wsman/1/wsmanfault
34
- NS_WSMAN_CONF = 'cfg' # http://schemas.microsoft.com/wbem/wsman/1/config
35
- end
36
-
37
- require 'winrm/exceptions/exceptions'
38
- require 'winrm/winrm_service'
39
- 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 'base64'
21
+
22
+ # SOAP constants for WinRM
23
+ module WinRM
24
+ NS_SOAP_ENV = 's' # http://www.w3.org/2003/05/soap-envelope
25
+ NS_ADDRESSING = 'a' # http://schemas.xmlsoap.org/ws/2004/08/addressing
26
+ NS_CIMBINDING = 'b' # http://schemas.dmtf.org/wbem/wsman/1/cimbinding.xsd
27
+ NS_ENUM = 'n' # http://schemas.xmlsoap.org/ws/2004/09/enumeration
28
+ NS_TRANSFER = 'x' # http://schemas.xmlsoap.org/ws/2004/09/transfer
29
+ NS_WSMAN_DMTF = 'w' # http://schemas.dmtf.org/wbem/wsman/1/wsman.xsd
30
+ NS_WSMAN_MSFT = 'p' # http://schemas.microsoft.com/wbem/wsman/1/wsman.xsd
31
+ NS_SCHEMA_INST = 'xsi' # http://www.w3.org/2001/XMLSchema-instance
32
+ NS_WIN_SHELL = 'rsp' # http://schemas.microsoft.com/wbem/wsman/1/windows/shell
33
+ NS_WSMAN_FAULT = 'f' # http://schemas.microsoft.com/wbem/wsman/1/wsmanfault
34
+ NS_WSMAN_CONF = 'cfg' # http://schemas.microsoft.com/wbem/wsman/1/config
35
+ end
36
+
37
+ require 'winrm/exceptions/exceptions'
38
+ require 'winrm/winrm_service'
39
+ require 'winrm/http/transport'
@@ -3,5 +3,5 @@
3
3
  # WinRM module
4
4
  module WinRM
5
5
  # The version of the WinRM library
6
- VERSION = '1.7.3'
6
+ VERSION = '1.8.0'
7
7
  end
@@ -1,547 +1,550 @@
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 'securerandom'
20
- require_relative 'command_executor'
21
- require_relative 'command_output_decoder'
22
- require_relative 'helpers/powershell_script'
23
-
24
- module WinRM
25
- # This is the main class that does the SOAP request/response logic. There are a few helper
26
- # classes, but pretty much everything comes through here first.
27
- class WinRMWebService
28
- DEFAULT_TIMEOUT = 'PT60S'
29
- DEFAULT_MAX_ENV_SIZE = 153600
30
- DEFAULT_LOCALE = 'en-US'
31
-
32
- attr_reader :endpoint, :timeout, :retry_limit, :retry_delay, :output_decoder
33
-
34
- attr_accessor :logger
35
-
36
- # @param [String,URI] endpoint the WinRM webservice endpoint
37
- # @param [Symbol] transport either :kerberos(default)/:ssl/:plaintext
38
- # @param [Hash] opts Misc opts for the various transports.
39
- # @see WinRM::HTTP::HttpTransport
40
- # @see WinRM::HTTP::HttpGSSAPI
41
- # @see WinRM::HTTP::HttpNegotiate
42
- # @see WinRM::HTTP::HttpSSL
43
- def initialize(endpoint, transport = :kerberos, opts = {})
44
- @endpoint = endpoint
45
- @timeout = DEFAULT_TIMEOUT
46
- @max_env_sz = DEFAULT_MAX_ENV_SIZE
47
- @locale = DEFAULT_LOCALE
48
- @output_decoder = CommandOutputDecoder.new
49
- setup_logger
50
- configure_retries(opts)
51
- begin
52
- @xfer = send "init_#{transport}_transport", opts.merge({endpoint: endpoint})
53
- rescue NoMethodError => e
54
- raise "Invalid transport '#{transport}' specified, expected: negotiate, kerberos, plaintext, ssl."
55
- end
56
- end
57
-
58
- def init_negotiate_transport(opts)
59
- HTTP::HttpNegotiate.new(opts[:endpoint], opts[:user], opts[:pass], opts)
60
- end
61
-
62
- def init_kerberos_transport(opts)
63
- require 'gssapi'
64
- require 'gssapi/extensions'
65
- HTTP::HttpGSSAPI.new(opts[:endpoint], opts[:realm], opts[:service], opts[:keytab], opts)
66
- end
67
-
68
- def init_plaintext_transport(opts)
69
- HTTP::HttpPlaintext.new(opts[:endpoint], opts[:user], opts[:pass], opts)
70
- end
71
-
72
- def init_ssl_transport(opts)
73
- if opts[:basic_auth_only]
74
- HTTP::BasicAuthSSL.new(opts[:endpoint], opts[:user], opts[:pass], opts)
75
- else
76
- HTTP::HttpNegotiate.new(opts[:endpoint], opts[:user], opts[:pass], opts)
77
- end
78
- end
79
-
80
- # Operation timeout.
81
- #
82
- # Unless specified the client receive timeout will be 10s + the operation
83
- # timeout.
84
- #
85
- # @see http://msdn.microsoft.com/en-us/library/ee916629(v=PROT.13).aspx
86
- #
87
- # @param [Fixnum] The number of seconds to set the WinRM operation timeout
88
- # @param [Fixnum] The number of seconds to set the Ruby receive timeout
89
- # @return [String] The ISO 8601 formatted operation timeout
90
- def set_timeout(op_timeout_sec, receive_timeout_sec=nil)
91
- @timeout = Iso8601Duration.sec_to_dur(op_timeout_sec)
92
- @xfer.receive_timeout = receive_timeout_sec || op_timeout_sec + 10
93
- @timeout
94
- end
95
- alias :op_timeout :set_timeout
96
-
97
- # Max envelope size
98
- # @see http://msdn.microsoft.com/en-us/library/ee916127(v=PROT.13).aspx
99
- # @param [Fixnum] byte_sz the max size in bytes to allow for the response
100
- def max_env_size(byte_sz)
101
- @max_env_sz = byte_sz
102
- end
103
-
104
- # Set the locale
105
- # @see http://msdn.microsoft.com/en-us/library/gg567404(v=PROT.13).aspx
106
- # @param [String] locale the locale to set for future messages
107
- def locale(locale)
108
- @locale = locale
109
- end
110
-
111
- # Create a Shell on the destination host
112
- # @param [Hash<optional>] shell_opts additional shell options you can pass
113
- # @option shell_opts [String] :i_stream Which input stream to open. Leave this alone unless you know what you're doing (default: stdin)
114
- # @option shell_opts [String] :o_stream Which output stream to open. Leave this alone unless you know what you're doing (default: stdout stderr)
115
- # @option shell_opts [String] :working_directory the directory to create the shell in
116
- # @option shell_opts [Hash] :env_vars environment variables to set for the shell. For instance;
117
- # :env_vars => {:myvar1 => 'val1', :myvar2 => 'var2'}
118
- # @return [String] The ShellId from the SOAP response. This is our open shell instance on the remote machine.
119
- def open_shell(shell_opts = {}, &block)
120
- logger.debug("[WinRM] opening remote shell on #{@endpoint}")
121
- i_stream = shell_opts.has_key?(:i_stream) ? shell_opts[:i_stream] : 'stdin'
122
- o_stream = shell_opts.has_key?(:o_stream) ? shell_opts[:o_stream] : 'stdout stderr'
123
- 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)
124
- noprofile = shell_opts.has_key?(:noprofile) ? shell_opts[:noprofile] : 'FALSE'
125
- h_opts = { "#{NS_WSMAN_DMTF}:OptionSet" => { "#{NS_WSMAN_DMTF}:Option" => [noprofile, codepage],
126
- :attributes! => {"#{NS_WSMAN_DMTF}:Option" => {'Name' => ['WINRS_NOPROFILE','WINRS_CODEPAGE']}}}}
127
- shell_body = {
128
- "#{NS_WIN_SHELL}:InputStreams" => i_stream,
129
- "#{NS_WIN_SHELL}:OutputStreams" => o_stream
130
- }
131
- shell_body["#{NS_WIN_SHELL}:WorkingDirectory"] = shell_opts[:working_directory] if shell_opts.has_key?(:working_directory)
132
- shell_body["#{NS_WIN_SHELL}:IdleTimeOut"] = shell_opts[:idle_timeout] if(shell_opts.has_key?(:idle_timeout) && shell_opts[:idle_timeout].is_a?(String))
133
- if(shell_opts.has_key?(:env_vars) && shell_opts[:env_vars].is_a?(Hash))
134
- keys = shell_opts[:env_vars].keys
135
- vals = shell_opts[:env_vars].values
136
- shell_body["#{NS_WIN_SHELL}:Environment"] = {
137
- "#{NS_WIN_SHELL}:Variable" => vals,
138
- :attributes! => {"#{NS_WIN_SHELL}:Variable" => {'Name' => keys}}
139
- }
140
- end
141
- builder = Builder::XmlMarkup.new
142
- builder.instruct!(:xml, :encoding => 'UTF-8')
143
- builder.tag! :env, :Envelope, namespaces do |env|
144
- env.tag!(:env, :Header) { |h| h << Gyoku.xml(merge_headers(header,resource_uri_cmd,action_create,h_opts)) }
145
- env.tag! :env, :Body do |body|
146
- body.tag!("#{NS_WIN_SHELL}:Shell") { |s| s << Gyoku.xml(shell_body)}
147
- end
148
- end
149
-
150
- resp_doc = send_message(builder.target!)
151
- shell_id = REXML::XPath.first(resp_doc, "//*[@Name='ShellId']").text
152
- logger.debug("[WinRM] remote shell #{shell_id} is open on #{@endpoint}")
153
-
154
- if block_given?
155
- begin
156
- yield shell_id
157
- ensure
158
- close_shell(shell_id)
159
- end
160
- else
161
- shell_id
162
- end
163
- end
164
-
165
- # Run a command on a machine with an open shell
166
- # @param [String] shell_id The shell id on the remote machine. See #open_shell
167
- # @param [String] command The command to run on the remote machine
168
- # @param [Array<String>] arguments An array of arguments for this command
169
- # @return [String] The CommandId from the SOAP response. This is the ID we need to query in order to get output.
170
- def run_command(shell_id, command, arguments = [], cmd_opts = {}, &block)
171
- consolemode = cmd_opts.has_key?(:console_mode_stdin) ? cmd_opts[:console_mode_stdin] : 'TRUE'
172
- skipcmd = cmd_opts.has_key?(:skip_cmd_shell) ? cmd_opts[:skip_cmd_shell] : 'FALSE'
173
-
174
- h_opts = { "#{NS_WSMAN_DMTF}:OptionSet" => {
175
- "#{NS_WSMAN_DMTF}:Option" => [consolemode, skipcmd],
176
- :attributes! => {"#{NS_WSMAN_DMTF}:Option" => {'Name' => ['WINRS_CONSOLEMODE_STDIN','WINRS_SKIP_CMD_SHELL']}}}
177
- }
178
- body = { "#{NS_WIN_SHELL}:Command" => "\"#{command}\"", "#{NS_WIN_SHELL}:Arguments" => arguments}
179
-
180
- builder = Builder::XmlMarkup.new
181
- builder.instruct!(:xml, :encoding => 'UTF-8')
182
- builder.tag! :env, :Envelope, namespaces do |env|
183
- env.tag!(:env, :Header) { |h| h << Gyoku.xml(merge_headers(header,resource_uri_cmd,action_command,h_opts,selector_shell_id(shell_id))) }
184
- env.tag!(:env, :Body) do |env_body|
185
- env_body.tag!("#{NS_WIN_SHELL}:CommandLine") { |cl| cl << Gyoku.xml(body) }
186
- end
187
- end
188
-
189
- # Grab the command element and unescape any single quotes - issue 69
190
- xml = builder.target!
191
- escaped_cmd = /<#{NS_WIN_SHELL}:Command>(.+)<\/#{NS_WIN_SHELL}:Command>/m.match(xml)[1]
192
- xml[escaped_cmd] = escaped_cmd.gsub(/&#39;/, "'")
193
-
194
- resp_doc = send_message(xml)
195
- command_id = REXML::XPath.first(resp_doc, "//#{NS_WIN_SHELL}:CommandId").text
196
-
197
- if block_given?
198
- begin
199
- yield command_id
200
- ensure
201
- cleanup_command(shell_id, command_id)
202
- end
203
- else
204
- command_id
205
- end
206
- end
207
-
208
- def write_stdin(shell_id, command_id, stdin)
209
- # Signal the Command references to terminate (close stdout/stderr)
210
- body = {
211
- "#{NS_WIN_SHELL}:Send" => {
212
- "#{NS_WIN_SHELL}:Stream" => {
213
- "@Name" => 'stdin',
214
- "@CommandId" => command_id,
215
- :content! => Base64.encode64(stdin)
216
- }
217
- }
218
- }
219
- builder = Builder::XmlMarkup.new
220
- builder.instruct!(:xml, :encoding => 'UTF-8')
221
- builder.tag! :env, :Envelope, namespaces do |env|
222
- env.tag!(:env, :Header) { |h| h << Gyoku.xml(merge_headers(header,resource_uri_cmd,action_send,selector_shell_id(shell_id))) }
223
- env.tag!(:env, :Body) do |env_body|
224
- env_body << Gyoku.xml(body)
225
- end
226
- end
227
- resp = send_message(builder.target!)
228
- true
229
- end
230
-
231
- # Get the Output of the given shell and command
232
- # @param [String] shell_id The shell id on the remote machine. See #open_shell
233
- # @param [String] command_id The command id on the remote machine. See #run_command
234
- # @return [Hash] Returns a Hash with a key :exitcode and :data. Data is an Array of Hashes where the cooresponding key
235
- # 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
236
- # the console.
237
- def get_command_output(shell_id, command_id, &block)
238
- body = { "#{NS_WIN_SHELL}:DesiredStream" => 'stdout stderr',
239
- :attributes! => {"#{NS_WIN_SHELL}:DesiredStream" => {'CommandId' => command_id}}}
240
-
241
- builder = Builder::XmlMarkup.new
242
- builder.instruct!(:xml, :encoding => 'UTF-8')
243
- builder.tag! :env, :Envelope, namespaces do |env|
244
- env.tag!(:env, :Header) { |h| h << Gyoku.xml(merge_headers(header,resource_uri_cmd,action_receive,selector_shell_id(shell_id))) }
245
- env.tag!(:env, :Body) do |env_body|
246
- env_body.tag!("#{NS_WIN_SHELL}:Receive") { |cl| cl << Gyoku.xml(body) }
247
- end
248
- end
249
-
250
- resp_doc = nil
251
- request_msg = builder.target!
252
- done_elems = []
253
- output = Output.new
254
-
255
- while done_elems.empty?
256
- resp_doc = send_get_output_message(request_msg)
257
-
258
- REXML::XPath.match(resp_doc, "//#{NS_WIN_SHELL}:Stream").each do |n|
259
- next if n.text.nil? || n.text.empty?
260
-
261
- decoded_text = output_decoder.decode(n.text)
262
- stream = { n.attributes['Name'].to_sym => decoded_text }
263
- output[:data] << stream
264
- yield stream[:stdout], stream[:stderr] if block_given?
265
- end
266
-
267
- # We may need to get additional output if the stream has not finished.
268
- # The CommandState will change from Running to Done like so:
269
- # @example
270
- # from...
271
- # <rsp:CommandState CommandId="..." State="http://schemas.microsoft.com/wbem/wsman/1/windows/shell/CommandState/Running"/>
272
- # to...
273
- # <rsp:CommandState CommandId="..." State="http://schemas.microsoft.com/wbem/wsman/1/windows/shell/CommandState/Done">
274
- # <rsp:ExitCode>0</rsp:ExitCode>
275
- # </rsp:CommandState>
276
- done_elems = REXML::XPath.match(resp_doc, "//*[@State='http://schemas.microsoft.com/wbem/wsman/1/windows/shell/CommandState/Done']")
277
- end
278
-
279
- output[:exitcode] = REXML::XPath.first(resp_doc, "//#{NS_WIN_SHELL}:ExitCode").text.to_i
280
- output
281
- end
282
-
283
- # Clean-up after a command.
284
- # @see #run_command
285
- # @param [String] shell_id The shell id on the remote machine. See #open_shell
286
- # @param [String] command_id The command id on the remote machine. See #run_command
287
- # @return [true] This should have more error checking but it just returns true for now.
288
- def cleanup_command(shell_id, command_id)
289
- # Signal the Command references to terminate (close stdout/stderr)
290
- body = { "#{NS_WIN_SHELL}:Code" => 'http://schemas.microsoft.com/wbem/wsman/1/windows/shell/signal/terminate' }
291
- builder = Builder::XmlMarkup.new
292
- builder.instruct!(:xml, :encoding => 'UTF-8')
293
- builder.tag! :env, :Envelope, namespaces do |env|
294
- env.tag!(:env, :Header) { |h| h << Gyoku.xml(merge_headers(header,resource_uri_cmd,action_signal,selector_shell_id(shell_id))) }
295
- env.tag!(:env, :Body) do |env_body|
296
- env_body.tag!("#{NS_WIN_SHELL}:Signal", {'CommandId' => command_id}) { |cl| cl << Gyoku.xml(body) }
297
- end
298
- end
299
- resp = send_message(builder.target!)
300
- true
301
- end
302
-
303
- # Close the shell
304
- # @param [String] shell_id The shell id on the remote machine. See #open_shell
305
- # @return [true] This should have more error checking but it just returns true for now.
306
- def close_shell(shell_id)
307
- logger.debug("[WinRM] closing remote shell #{shell_id} on #{@endpoint}")
308
- builder = Builder::XmlMarkup.new
309
- builder.instruct!(:xml, :encoding => 'UTF-8')
310
-
311
- builder.tag!('env:Envelope', namespaces) do |env|
312
- env.tag!('env:Header') { |h| h << Gyoku.xml(merge_headers(header,resource_uri_cmd,action_delete,selector_shell_id(shell_id))) }
313
- env.tag!('env:Body')
314
- end
315
-
316
- resp = send_message(builder.target!)
317
- logger.debug("[WinRM] remote shell #{shell_id} closed")
318
- true
319
- end
320
-
321
- # DEPRECATED: Use WinRM::CommandExecutor#run_cmd instead
322
- # Run a CMD command
323
- # @param [String] command The command to run on the remote system
324
- # @param [Array <String>] arguments arguments to the command
325
- # @param [String] an existing and open shell id to reuse
326
- # @return [Hash] :stdout and :stderr
327
- def run_cmd(command, arguments = [], &block)
328
- logger.warn("WinRM::WinRMWebService#run_cmd is deprecated. Use WinRM::CommandExecutor#run_cmd instead")
329
- create_executor do |executor|
330
- executor.run_cmd(command, arguments, &block)
331
- end
332
- end
333
- alias :cmd :run_cmd
334
-
335
- # DEPRECATED: Use WinRM::CommandExecutor#run_powershell_script instead
336
- # Run a Powershell script that resides on the local box.
337
- # @param [IO,String] script_file an IO reference for reading the Powershell script or the actual file contents
338
- # @param [String] an existing and open shell id to reuse
339
- # @return [Hash] :stdout and :stderr
340
- def run_powershell_script(script_file, &block)
341
- logger.warn("WinRM::WinRMWebService#run_powershell_script is deprecated. Use WinRM::CommandExecutor#run_powershell_script instead")
342
- create_executor do |executor|
343
- executor.run_powershell_script(script_file, &block)
344
- end
345
- end
346
- alias :powershell :run_powershell_script
347
-
348
- # Creates a CommandExecutor initialized with this WinRMWebService
349
- # If called with a block, create_executor yields an executor and
350
- # ensures that the executor is closed after the block completes.
351
- # The CommandExecutor is simply returned if no block is given.
352
- # @yieldparam [CommandExecutor] a CommandExecutor instance
353
- # @return [CommandExecutor] a CommandExecutor instance
354
- def create_executor(&block)
355
- executor = CommandExecutor.new(self)
356
- executor.open
357
-
358
- if block_given?
359
- begin
360
- yield executor
361
- ensure
362
- executor.close
363
- end
364
- else
365
- executor
366
- end
367
- end
368
-
369
- # Run a WQL Query
370
- # @see http://msdn.microsoft.com/en-us/library/aa394606(VS.85).aspx
371
- # @param [String] wql The WQL query
372
- # @return [Hash] Returns a Hash that contain the key/value pairs returned from the query.
373
- def run_wql(wql)
374
-
375
- body = { "#{NS_WSMAN_DMTF}:OptimizeEnumeration" => nil,
376
- "#{NS_WSMAN_DMTF}:MaxElements" => '32000',
377
- "#{NS_WSMAN_DMTF}:Filter" => wql,
378
- :attributes! => { "#{NS_WSMAN_DMTF}:Filter" => {'Dialect' => 'http://schemas.microsoft.com/wbem/wsman/1/WQL'}}
379
- }
380
-
381
- builder = Builder::XmlMarkup.new
382
- builder.instruct!(:xml, :encoding => 'UTF-8')
383
- builder.tag! :env, :Envelope, namespaces do |env|
384
- env.tag!(:env, :Header) { |h| h << Gyoku.xml(merge_headers(header,resource_uri_wmi,action_enumerate)) }
385
- env.tag!(:env, :Body) do |env_body|
386
- env_body.tag!("#{NS_ENUM}:Enumerate") { |en| en << Gyoku.xml(body) }
387
- end
388
- end
389
-
390
- resp = send_message(builder.target!)
391
- parser = Nori.new(:parser => :rexml, :advanced_typecasting => false, :convert_tags_to => lambda { |tag| tag.snakecase.to_sym }, :strip_namespaces => true)
392
- hresp = parser.parse(resp.to_s)[:envelope][:body]
393
-
394
- # Normalize items so the type always has an array even if it's just a single item.
395
- items = {}
396
- if hresp[:enumerate_response][:items]
397
- hresp[:enumerate_response][:items].each_pair do |k,v|
398
- if v.is_a?(Array)
399
- items[k] = v
400
- else
401
- items[k] = [v]
402
- end
403
- end
404
- end
405
- items
406
- end
407
- alias :wql :run_wql
408
-
409
- def toggle_nori_type_casting(to)
410
- logger.warn('toggle_nori_type_casting is deprecated and has no effect, ' +
411
- 'please remove calls to it')
412
- end
413
-
414
- private
415
-
416
- def setup_logger
417
- @logger = Logging.logger[self]
418
- @logger.level = :warn
419
- @logger.add_appenders(Logging.appenders.stdout)
420
- end
421
-
422
- def configure_retries(opts)
423
- @retry_delay = opts[:retry_delay] || 10
424
- @retry_limit = opts[:retry_limit] || 3
425
- end
426
-
427
- def namespaces
428
- {
429
- 'xmlns:xsd' => 'http://www.w3.org/2001/XMLSchema',
430
- 'xmlns:xsi' => 'http://www.w3.org/2001/XMLSchema-instance',
431
- 'xmlns:env' => 'http://www.w3.org/2003/05/soap-envelope',
432
- 'xmlns:a' => 'http://schemas.xmlsoap.org/ws/2004/08/addressing',
433
- 'xmlns:b' => 'http://schemas.dmtf.org/wbem/wsman/1/cimbinding.xsd',
434
- 'xmlns:n' => 'http://schemas.xmlsoap.org/ws/2004/09/enumeration',
435
- 'xmlns:x' => 'http://schemas.xmlsoap.org/ws/2004/09/transfer',
436
- 'xmlns:w' => 'http://schemas.dmtf.org/wbem/wsman/1/wsman.xsd',
437
- 'xmlns:p' => 'http://schemas.microsoft.com/wbem/wsman/1/wsman.xsd',
438
- 'xmlns:rsp' => 'http://schemas.microsoft.com/wbem/wsman/1/windows/shell',
439
- 'xmlns:cfg' => 'http://schemas.microsoft.com/wbem/wsman/1/config',
440
- }
441
- end
442
-
443
- def header
444
- { "#{NS_ADDRESSING}:To" => "#{@xfer.endpoint.to_s}",
445
- "#{NS_ADDRESSING}:ReplyTo" => {
446
- "#{NS_ADDRESSING}:Address" => 'http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous',
447
- :attributes! => {"#{NS_ADDRESSING}:Address" => {'mustUnderstand' => true}}},
448
- "#{NS_WSMAN_DMTF}:MaxEnvelopeSize" => @max_env_sz,
449
- "#{NS_ADDRESSING}:MessageID" => "uuid:#{SecureRandom.uuid.to_s.upcase}",
450
- "#{NS_WSMAN_DMTF}:Locale/" => '',
451
- "#{NS_WSMAN_MSFT}:DataLocale/" => '',
452
- "#{NS_WSMAN_DMTF}:OperationTimeout" => @timeout,
453
- :attributes! => {
454
- "#{NS_WSMAN_DMTF}:MaxEnvelopeSize" => {'mustUnderstand' => true},
455
- "#{NS_WSMAN_DMTF}:Locale/" => {'xml:lang' => @locale, 'mustUnderstand' => false},
456
- "#{NS_WSMAN_MSFT}:DataLocale/" => {'xml:lang' => @locale, 'mustUnderstand' => false}
457
- }}
458
- end
459
-
460
- # merge the various header hashes and make sure we carry all of the attributes
461
- # through instead of overwriting them.
462
- def merge_headers(*headers)
463
- hdr = {}
464
- headers.each do |h|
465
- hdr.merge!(h) do |k,v1,v2|
466
- v1.merge!(v2) if k == :attributes!
467
- end
468
- end
469
- hdr
470
- end
471
-
472
- def send_get_output_message(message)
473
- send_message(message)
474
- rescue WinRMWSManFault => e
475
- # If no output is available before the wsman:OperationTimeout expires,
476
- # the server MUST return a WSManFault with the Code attribute equal to
477
- # 2150858793. When the client receives this fault, it SHOULD issue
478
- # another Receive request.
479
- # http://msdn.microsoft.com/en-us/library/cc251676.aspx
480
- if e.fault_code == '2150858793'
481
- logger.debug("[WinRM] retrying receive request after timeout")
482
- retry
483
- else
484
- raise
485
- end
486
- end
487
-
488
- def send_message(message)
489
- @xfer.send_request(message)
490
- end
491
-
492
-
493
- # Helper methods for SOAP Headers
494
-
495
- def resource_uri_cmd
496
- {"#{NS_WSMAN_DMTF}:ResourceURI" => 'http://schemas.microsoft.com/wbem/wsman/1/windows/shell/cmd',
497
- :attributes! => {"#{NS_WSMAN_DMTF}:ResourceURI" => {'mustUnderstand' => true}}}
498
- end
499
-
500
- def resource_uri_wmi(namespace = 'root/cimv2/*')
501
- {"#{NS_WSMAN_DMTF}:ResourceURI" => "http://schemas.microsoft.com/wbem/wsman/1/wmi/#{namespace}",
502
- :attributes! => {"#{NS_WSMAN_DMTF}:ResourceURI" => {'mustUnderstand' => true}}}
503
- end
504
-
505
- def action_create
506
- {"#{NS_ADDRESSING}:Action" => 'http://schemas.xmlsoap.org/ws/2004/09/transfer/Create',
507
- :attributes! => {"#{NS_ADDRESSING}:Action" => {'mustUnderstand' => true}}}
508
- end
509
-
510
- def action_delete
511
- {"#{NS_ADDRESSING}:Action" => 'http://schemas.xmlsoap.org/ws/2004/09/transfer/Delete',
512
- :attributes! => {"#{NS_ADDRESSING}:Action" => {'mustUnderstand' => true}}}
513
- end
514
-
515
- def action_command
516
- {"#{NS_ADDRESSING}:Action" => 'http://schemas.microsoft.com/wbem/wsman/1/windows/shell/Command',
517
- :attributes! => {"#{NS_ADDRESSING}:Action" => {'mustUnderstand' => true}}}
518
- end
519
-
520
- def action_receive
521
- {"#{NS_ADDRESSING}:Action" => 'http://schemas.microsoft.com/wbem/wsman/1/windows/shell/Receive',
522
- :attributes! => {"#{NS_ADDRESSING}:Action" => {'mustUnderstand' => true}}}
523
- end
524
-
525
- def action_signal
526
- {"#{NS_ADDRESSING}:Action" => 'http://schemas.microsoft.com/wbem/wsman/1/windows/shell/Signal',
527
- :attributes! => {"#{NS_ADDRESSING}:Action" => {'mustUnderstand' => true}}}
528
- end
529
-
530
- def action_send
531
- {"#{NS_ADDRESSING}:Action" => 'http://schemas.microsoft.com/wbem/wsman/1/windows/shell/Send',
532
- :attributes! => {"#{NS_ADDRESSING}:Action" => {'mustUnderstand' => true}}}
533
- end
534
-
535
- def action_enumerate
536
- {"#{NS_ADDRESSING}:Action" => 'http://schemas.xmlsoap.org/ws/2004/09/enumeration/Enumerate',
537
- :attributes! => {"#{NS_ADDRESSING}:Action" => {'mustUnderstand' => true}}}
538
- end
539
-
540
- def selector_shell_id(shell_id)
541
- {"#{NS_WSMAN_DMTF}:SelectorSet" =>
542
- {"#{NS_WSMAN_DMTF}:Selector" => shell_id, :attributes! => {"#{NS_WSMAN_DMTF}:Selector" => {'Name' => 'ShellId'}}}
543
- }
544
- end
545
-
546
- end # WinRMWebService
547
- 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 'securerandom'
20
+ require_relative 'command_executor'
21
+ require_relative 'command_output_decoder'
22
+ require_relative 'helpers/powershell_script'
23
+
24
+ module WinRM
25
+ # This is the main class that does the SOAP request/response logic. There are a few helper
26
+ # classes, but pretty much everything comes through here first.
27
+ class WinRMWebService
28
+ DEFAULT_TIMEOUT = 'PT60S'
29
+ DEFAULT_MAX_ENV_SIZE = 153600
30
+ DEFAULT_LOCALE = 'en-US'
31
+
32
+ attr_reader :endpoint, :timeout, :retry_limit, :retry_delay, :output_decoder
33
+
34
+ attr_accessor :logger
35
+
36
+ # @param [String,URI] endpoint the WinRM webservice endpoint
37
+ # @param [Symbol] transport either :kerberos(default)/:ssl/:plaintext
38
+ # @param [Hash] opts Misc opts for the various transports.
39
+ # @see WinRM::HTTP::HttpTransport
40
+ # @see WinRM::HTTP::HttpGSSAPI
41
+ # @see WinRM::HTTP::HttpNegotiate
42
+ # @see WinRM::HTTP::HttpSSL
43
+ def initialize(endpoint, transport = :kerberos, opts = {})
44
+ @endpoint = endpoint
45
+ @timeout = DEFAULT_TIMEOUT
46
+ @max_env_sz = DEFAULT_MAX_ENV_SIZE
47
+ @locale = DEFAULT_LOCALE
48
+ @output_decoder = CommandOutputDecoder.new
49
+ setup_logger
50
+ configure_retries(opts)
51
+ begin
52
+ @xfer = send "init_#{transport}_transport", opts.merge({endpoint: endpoint})
53
+ rescue NoMethodError => e
54
+ raise "Invalid transport '#{transport}' specified, expected: negotiate, kerberos, plaintext, ssl."
55
+ end
56
+ end
57
+
58
+ def init_negotiate_transport(opts)
59
+ HTTP::HttpNegotiate.new(opts[:endpoint], opts[:user], opts[:pass], opts)
60
+ end
61
+
62
+ def init_kerberos_transport(opts)
63
+ require 'gssapi'
64
+ require 'gssapi/extensions'
65
+ HTTP::HttpGSSAPI.new(opts[:endpoint], opts[:realm], opts[:service], opts[:keytab], opts)
66
+ end
67
+
68
+ def init_plaintext_transport(opts)
69
+ HTTP::HttpPlaintext.new(opts[:endpoint], opts[:user], opts[:pass], opts)
70
+ end
71
+
72
+ def init_ssl_transport(opts)
73
+ if opts[:basic_auth_only]
74
+ HTTP::BasicAuthSSL.new(opts[:endpoint], opts[:user], opts[:pass], opts)
75
+ elsif opts[:client_cert]
76
+ HTTP::ClientCertAuthSSL.new(opts[:endpoint], opts[:client_cert],
77
+ opts[:client_key], opts[:key_pass], opts)
78
+ else
79
+ HTTP::HttpNegotiate.new(opts[:endpoint], opts[:user], opts[:pass], opts)
80
+ end
81
+ end
82
+
83
+ # Operation timeout.
84
+ #
85
+ # Unless specified the client receive timeout will be 10s + the operation
86
+ # timeout.
87
+ #
88
+ # @see http://msdn.microsoft.com/en-us/library/ee916629(v=PROT.13).aspx
89
+ #
90
+ # @param [Fixnum] The number of seconds to set the WinRM operation timeout
91
+ # @param [Fixnum] The number of seconds to set the Ruby receive timeout
92
+ # @return [String] The ISO 8601 formatted operation timeout
93
+ def set_timeout(op_timeout_sec, receive_timeout_sec=nil)
94
+ @timeout = Iso8601Duration.sec_to_dur(op_timeout_sec)
95
+ @xfer.receive_timeout = receive_timeout_sec || op_timeout_sec + 10
96
+ @timeout
97
+ end
98
+ alias :op_timeout :set_timeout
99
+
100
+ # Max envelope size
101
+ # @see http://msdn.microsoft.com/en-us/library/ee916127(v=PROT.13).aspx
102
+ # @param [Fixnum] byte_sz the max size in bytes to allow for the response
103
+ def max_env_size(byte_sz)
104
+ @max_env_sz = byte_sz
105
+ end
106
+
107
+ # Set the locale
108
+ # @see http://msdn.microsoft.com/en-us/library/gg567404(v=PROT.13).aspx
109
+ # @param [String] locale the locale to set for future messages
110
+ def locale(locale)
111
+ @locale = locale
112
+ end
113
+
114
+ # Create a Shell on the destination host
115
+ # @param [Hash<optional>] shell_opts additional shell options you can pass
116
+ # @option shell_opts [String] :i_stream Which input stream to open. Leave this alone unless you know what you're doing (default: stdin)
117
+ # @option shell_opts [String] :o_stream Which output stream to open. Leave this alone unless you know what you're doing (default: stdout stderr)
118
+ # @option shell_opts [String] :working_directory the directory to create the shell in
119
+ # @option shell_opts [Hash] :env_vars environment variables to set for the shell. For instance;
120
+ # :env_vars => {:myvar1 => 'val1', :myvar2 => 'var2'}
121
+ # @return [String] The ShellId from the SOAP response. This is our open shell instance on the remote machine.
122
+ def open_shell(shell_opts = {}, &block)
123
+ logger.debug("[WinRM] opening remote shell on #{@endpoint}")
124
+ i_stream = shell_opts.has_key?(:i_stream) ? shell_opts[:i_stream] : 'stdin'
125
+ o_stream = shell_opts.has_key?(:o_stream) ? shell_opts[:o_stream] : 'stdout stderr'
126
+ 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)
127
+ noprofile = shell_opts.has_key?(:noprofile) ? shell_opts[:noprofile] : 'FALSE'
128
+ h_opts = { "#{NS_WSMAN_DMTF}:OptionSet" => { "#{NS_WSMAN_DMTF}:Option" => [noprofile, codepage],
129
+ :attributes! => {"#{NS_WSMAN_DMTF}:Option" => {'Name' => ['WINRS_NOPROFILE','WINRS_CODEPAGE']}}}}
130
+ shell_body = {
131
+ "#{NS_WIN_SHELL}:InputStreams" => i_stream,
132
+ "#{NS_WIN_SHELL}:OutputStreams" => o_stream
133
+ }
134
+ shell_body["#{NS_WIN_SHELL}:WorkingDirectory"] = shell_opts[:working_directory] if shell_opts.has_key?(:working_directory)
135
+ shell_body["#{NS_WIN_SHELL}:IdleTimeOut"] = shell_opts[:idle_timeout] if(shell_opts.has_key?(:idle_timeout) && shell_opts[:idle_timeout].is_a?(String))
136
+ if(shell_opts.has_key?(:env_vars) && shell_opts[:env_vars].is_a?(Hash))
137
+ keys = shell_opts[:env_vars].keys
138
+ vals = shell_opts[:env_vars].values
139
+ shell_body["#{NS_WIN_SHELL}:Environment"] = {
140
+ "#{NS_WIN_SHELL}:Variable" => vals,
141
+ :attributes! => {"#{NS_WIN_SHELL}:Variable" => {'Name' => keys}}
142
+ }
143
+ end
144
+ builder = Builder::XmlMarkup.new
145
+ builder.instruct!(:xml, :encoding => 'UTF-8')
146
+ builder.tag! :env, :Envelope, namespaces do |env|
147
+ env.tag!(:env, :Header) { |h| h << Gyoku.xml(merge_headers(header,resource_uri_cmd,action_create,h_opts)) }
148
+ env.tag! :env, :Body do |body|
149
+ body.tag!("#{NS_WIN_SHELL}:Shell") { |s| s << Gyoku.xml(shell_body)}
150
+ end
151
+ end
152
+
153
+ resp_doc = send_message(builder.target!)
154
+ shell_id = REXML::XPath.first(resp_doc, "//*[@Name='ShellId']").text
155
+ logger.debug("[WinRM] remote shell #{shell_id} is open on #{@endpoint}")
156
+
157
+ if block_given?
158
+ begin
159
+ yield shell_id
160
+ ensure
161
+ close_shell(shell_id)
162
+ end
163
+ else
164
+ shell_id
165
+ end
166
+ end
167
+
168
+ # Run a command on a machine with an open shell
169
+ # @param [String] shell_id The shell id on the remote machine. See #open_shell
170
+ # @param [String] command The command to run on the remote machine
171
+ # @param [Array<String>] arguments An array of arguments for this command
172
+ # @return [String] The CommandId from the SOAP response. This is the ID we need to query in order to get output.
173
+ def run_command(shell_id, command, arguments = [], cmd_opts = {}, &block)
174
+ consolemode = cmd_opts.has_key?(:console_mode_stdin) ? cmd_opts[:console_mode_stdin] : 'TRUE'
175
+ skipcmd = cmd_opts.has_key?(:skip_cmd_shell) ? cmd_opts[:skip_cmd_shell] : 'FALSE'
176
+
177
+ h_opts = { "#{NS_WSMAN_DMTF}:OptionSet" => {
178
+ "#{NS_WSMAN_DMTF}:Option" => [consolemode, skipcmd],
179
+ :attributes! => {"#{NS_WSMAN_DMTF}:Option" => {'Name' => ['WINRS_CONSOLEMODE_STDIN','WINRS_SKIP_CMD_SHELL']}}}
180
+ }
181
+ body = { "#{NS_WIN_SHELL}:Command" => "\"#{command}\"", "#{NS_WIN_SHELL}:Arguments" => arguments}
182
+
183
+ builder = Builder::XmlMarkup.new
184
+ builder.instruct!(:xml, :encoding => 'UTF-8')
185
+ builder.tag! :env, :Envelope, namespaces do |env|
186
+ env.tag!(:env, :Header) { |h| h << Gyoku.xml(merge_headers(header,resource_uri_cmd,action_command,h_opts,selector_shell_id(shell_id))) }
187
+ env.tag!(:env, :Body) do |env_body|
188
+ env_body.tag!("#{NS_WIN_SHELL}:CommandLine") { |cl| cl << Gyoku.xml(body) }
189
+ end
190
+ end
191
+
192
+ # Grab the command element and unescape any single quotes - issue 69
193
+ xml = builder.target!
194
+ escaped_cmd = /<#{NS_WIN_SHELL}:Command>(.+)<\/#{NS_WIN_SHELL}:Command>/m.match(xml)[1]
195
+ xml[escaped_cmd] = escaped_cmd.gsub(/&#39;/, "'")
196
+
197
+ resp_doc = send_message(xml)
198
+ command_id = REXML::XPath.first(resp_doc, "//#{NS_WIN_SHELL}:CommandId").text
199
+
200
+ if block_given?
201
+ begin
202
+ yield command_id
203
+ ensure
204
+ cleanup_command(shell_id, command_id)
205
+ end
206
+ else
207
+ command_id
208
+ end
209
+ end
210
+
211
+ def write_stdin(shell_id, command_id, stdin)
212
+ # Signal the Command references to terminate (close stdout/stderr)
213
+ body = {
214
+ "#{NS_WIN_SHELL}:Send" => {
215
+ "#{NS_WIN_SHELL}:Stream" => {
216
+ "@Name" => 'stdin',
217
+ "@CommandId" => command_id,
218
+ :content! => Base64.encode64(stdin)
219
+ }
220
+ }
221
+ }
222
+ builder = Builder::XmlMarkup.new
223
+ builder.instruct!(:xml, :encoding => 'UTF-8')
224
+ builder.tag! :env, :Envelope, namespaces do |env|
225
+ env.tag!(:env, :Header) { |h| h << Gyoku.xml(merge_headers(header,resource_uri_cmd,action_send,selector_shell_id(shell_id))) }
226
+ env.tag!(:env, :Body) do |env_body|
227
+ env_body << Gyoku.xml(body)
228
+ end
229
+ end
230
+ resp = send_message(builder.target!)
231
+ true
232
+ end
233
+
234
+ # Get the Output of the given shell and command
235
+ # @param [String] shell_id The shell id on the remote machine. See #open_shell
236
+ # @param [String] command_id The command id on the remote machine. See #run_command
237
+ # @return [Hash] Returns a Hash with a key :exitcode and :data. Data is an Array of Hashes where the cooresponding key
238
+ # 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
239
+ # the console.
240
+ def get_command_output(shell_id, command_id, &block)
241
+ body = { "#{NS_WIN_SHELL}:DesiredStream" => 'stdout stderr',
242
+ :attributes! => {"#{NS_WIN_SHELL}:DesiredStream" => {'CommandId' => command_id}}}
243
+
244
+ builder = Builder::XmlMarkup.new
245
+ builder.instruct!(:xml, :encoding => 'UTF-8')
246
+ builder.tag! :env, :Envelope, namespaces do |env|
247
+ env.tag!(:env, :Header) { |h| h << Gyoku.xml(merge_headers(header,resource_uri_cmd,action_receive,selector_shell_id(shell_id))) }
248
+ env.tag!(:env, :Body) do |env_body|
249
+ env_body.tag!("#{NS_WIN_SHELL}:Receive") { |cl| cl << Gyoku.xml(body) }
250
+ end
251
+ end
252
+
253
+ resp_doc = nil
254
+ request_msg = builder.target!
255
+ done_elems = []
256
+ output = Output.new
257
+
258
+ while done_elems.empty?
259
+ resp_doc = send_get_output_message(request_msg)
260
+
261
+ REXML::XPath.match(resp_doc, "//#{NS_WIN_SHELL}:Stream").each do |n|
262
+ next if n.text.nil? || n.text.empty?
263
+
264
+ decoded_text = output_decoder.decode(n.text)
265
+ stream = { n.attributes['Name'].to_sym => decoded_text }
266
+ output[:data] << stream
267
+ yield stream[:stdout], stream[:stderr] if block_given?
268
+ end
269
+
270
+ # We may need to get additional output if the stream has not finished.
271
+ # The CommandState will change from Running to Done like so:
272
+ # @example
273
+ # from...
274
+ # <rsp:CommandState CommandId="..." State="http://schemas.microsoft.com/wbem/wsman/1/windows/shell/CommandState/Running"/>
275
+ # to...
276
+ # <rsp:CommandState CommandId="..." State="http://schemas.microsoft.com/wbem/wsman/1/windows/shell/CommandState/Done">
277
+ # <rsp:ExitCode>0</rsp:ExitCode>
278
+ # </rsp:CommandState>
279
+ done_elems = REXML::XPath.match(resp_doc, "//*[@State='http://schemas.microsoft.com/wbem/wsman/1/windows/shell/CommandState/Done']")
280
+ end
281
+
282
+ output[:exitcode] = REXML::XPath.first(resp_doc, "//#{NS_WIN_SHELL}:ExitCode").text.to_i
283
+ output
284
+ end
285
+
286
+ # Clean-up after a command.
287
+ # @see #run_command
288
+ # @param [String] shell_id The shell id on the remote machine. See #open_shell
289
+ # @param [String] command_id The command id on the remote machine. See #run_command
290
+ # @return [true] This should have more error checking but it just returns true for now.
291
+ def cleanup_command(shell_id, command_id)
292
+ # Signal the Command references to terminate (close stdout/stderr)
293
+ body = { "#{NS_WIN_SHELL}:Code" => 'http://schemas.microsoft.com/wbem/wsman/1/windows/shell/signal/terminate' }
294
+ builder = Builder::XmlMarkup.new
295
+ builder.instruct!(:xml, :encoding => 'UTF-8')
296
+ builder.tag! :env, :Envelope, namespaces do |env|
297
+ env.tag!(:env, :Header) { |h| h << Gyoku.xml(merge_headers(header,resource_uri_cmd,action_signal,selector_shell_id(shell_id))) }
298
+ env.tag!(:env, :Body) do |env_body|
299
+ env_body.tag!("#{NS_WIN_SHELL}:Signal", {'CommandId' => command_id}) { |cl| cl << Gyoku.xml(body) }
300
+ end
301
+ end
302
+ resp = send_message(builder.target!)
303
+ true
304
+ end
305
+
306
+ # Close the shell
307
+ # @param [String] shell_id The shell id on the remote machine. See #open_shell
308
+ # @return [true] This should have more error checking but it just returns true for now.
309
+ def close_shell(shell_id)
310
+ logger.debug("[WinRM] closing remote shell #{shell_id} on #{@endpoint}")
311
+ builder = Builder::XmlMarkup.new
312
+ builder.instruct!(:xml, :encoding => 'UTF-8')
313
+
314
+ builder.tag!('env:Envelope', namespaces) do |env|
315
+ env.tag!('env:Header') { |h| h << Gyoku.xml(merge_headers(header,resource_uri_cmd,action_delete,selector_shell_id(shell_id))) }
316
+ env.tag!('env:Body')
317
+ end
318
+
319
+ resp = send_message(builder.target!)
320
+ logger.debug("[WinRM] remote shell #{shell_id} closed")
321
+ true
322
+ end
323
+
324
+ # DEPRECATED: Use WinRM::CommandExecutor#run_cmd instead
325
+ # Run a CMD command
326
+ # @param [String] command The command to run on the remote system
327
+ # @param [Array <String>] arguments arguments to the command
328
+ # @param [String] an existing and open shell id to reuse
329
+ # @return [Hash] :stdout and :stderr
330
+ def run_cmd(command, arguments = [], &block)
331
+ logger.warn("WinRM::WinRMWebService#run_cmd is deprecated. Use WinRM::CommandExecutor#run_cmd instead")
332
+ create_executor do |executor|
333
+ executor.run_cmd(command, arguments, &block)
334
+ end
335
+ end
336
+ alias :cmd :run_cmd
337
+
338
+ # DEPRECATED: Use WinRM::CommandExecutor#run_powershell_script instead
339
+ # Run a Powershell script that resides on the local box.
340
+ # @param [IO,String] script_file an IO reference for reading the Powershell script or the actual file contents
341
+ # @param [String] an existing and open shell id to reuse
342
+ # @return [Hash] :stdout and :stderr
343
+ def run_powershell_script(script_file, &block)
344
+ logger.warn("WinRM::WinRMWebService#run_powershell_script is deprecated. Use WinRM::CommandExecutor#run_powershell_script instead")
345
+ create_executor do |executor|
346
+ executor.run_powershell_script(script_file, &block)
347
+ end
348
+ end
349
+ alias :powershell :run_powershell_script
350
+
351
+ # Creates a CommandExecutor initialized with this WinRMWebService
352
+ # If called with a block, create_executor yields an executor and
353
+ # ensures that the executor is closed after the block completes.
354
+ # The CommandExecutor is simply returned if no block is given.
355
+ # @yieldparam [CommandExecutor] a CommandExecutor instance
356
+ # @return [CommandExecutor] a CommandExecutor instance
357
+ def create_executor(&block)
358
+ executor = CommandExecutor.new(self)
359
+ executor.open
360
+
361
+ if block_given?
362
+ begin
363
+ yield executor
364
+ ensure
365
+ executor.close
366
+ end
367
+ else
368
+ executor
369
+ end
370
+ end
371
+
372
+ # Run a WQL Query
373
+ # @see http://msdn.microsoft.com/en-us/library/aa394606(VS.85).aspx
374
+ # @param [String] wql The WQL query
375
+ # @return [Hash] Returns a Hash that contain the key/value pairs returned from the query.
376
+ def run_wql(wql)
377
+
378
+ body = { "#{NS_WSMAN_DMTF}:OptimizeEnumeration" => nil,
379
+ "#{NS_WSMAN_DMTF}:MaxElements" => '32000',
380
+ "#{NS_WSMAN_DMTF}:Filter" => wql,
381
+ :attributes! => { "#{NS_WSMAN_DMTF}:Filter" => {'Dialect' => 'http://schemas.microsoft.com/wbem/wsman/1/WQL'}}
382
+ }
383
+
384
+ builder = Builder::XmlMarkup.new
385
+ builder.instruct!(:xml, :encoding => 'UTF-8')
386
+ builder.tag! :env, :Envelope, namespaces do |env|
387
+ env.tag!(:env, :Header) { |h| h << Gyoku.xml(merge_headers(header,resource_uri_wmi,action_enumerate)) }
388
+ env.tag!(:env, :Body) do |env_body|
389
+ env_body.tag!("#{NS_ENUM}:Enumerate") { |en| en << Gyoku.xml(body) }
390
+ end
391
+ end
392
+
393
+ resp = send_message(builder.target!)
394
+ parser = Nori.new(:parser => :rexml, :advanced_typecasting => false, :convert_tags_to => lambda { |tag| tag.snakecase.to_sym }, :strip_namespaces => true)
395
+ hresp = parser.parse(resp.to_s)[:envelope][:body]
396
+
397
+ # Normalize items so the type always has an array even if it's just a single item.
398
+ items = {}
399
+ if hresp[:enumerate_response][:items]
400
+ hresp[:enumerate_response][:items].each_pair do |k,v|
401
+ if v.is_a?(Array)
402
+ items[k] = v
403
+ else
404
+ items[k] = [v]
405
+ end
406
+ end
407
+ end
408
+ items
409
+ end
410
+ alias :wql :run_wql
411
+
412
+ def toggle_nori_type_casting(to)
413
+ logger.warn('toggle_nori_type_casting is deprecated and has no effect, ' +
414
+ 'please remove calls to it')
415
+ end
416
+
417
+ private
418
+
419
+ def setup_logger
420
+ @logger = Logging.logger[self]
421
+ @logger.level = :warn
422
+ @logger.add_appenders(Logging.appenders.stdout)
423
+ end
424
+
425
+ def configure_retries(opts)
426
+ @retry_delay = opts[:retry_delay] || 10
427
+ @retry_limit = opts[:retry_limit] || 3
428
+ end
429
+
430
+ def namespaces
431
+ {
432
+ 'xmlns:xsd' => 'http://www.w3.org/2001/XMLSchema',
433
+ 'xmlns:xsi' => 'http://www.w3.org/2001/XMLSchema-instance',
434
+ 'xmlns:env' => 'http://www.w3.org/2003/05/soap-envelope',
435
+ 'xmlns:a' => 'http://schemas.xmlsoap.org/ws/2004/08/addressing',
436
+ 'xmlns:b' => 'http://schemas.dmtf.org/wbem/wsman/1/cimbinding.xsd',
437
+ 'xmlns:n' => 'http://schemas.xmlsoap.org/ws/2004/09/enumeration',
438
+ 'xmlns:x' => 'http://schemas.xmlsoap.org/ws/2004/09/transfer',
439
+ 'xmlns:w' => 'http://schemas.dmtf.org/wbem/wsman/1/wsman.xsd',
440
+ 'xmlns:p' => 'http://schemas.microsoft.com/wbem/wsman/1/wsman.xsd',
441
+ 'xmlns:rsp' => 'http://schemas.microsoft.com/wbem/wsman/1/windows/shell',
442
+ 'xmlns:cfg' => 'http://schemas.microsoft.com/wbem/wsman/1/config',
443
+ }
444
+ end
445
+
446
+ def header
447
+ { "#{NS_ADDRESSING}:To" => "#{@xfer.endpoint.to_s}",
448
+ "#{NS_ADDRESSING}:ReplyTo" => {
449
+ "#{NS_ADDRESSING}:Address" => 'http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous',
450
+ :attributes! => {"#{NS_ADDRESSING}:Address" => {'mustUnderstand' => true}}},
451
+ "#{NS_WSMAN_DMTF}:MaxEnvelopeSize" => @max_env_sz,
452
+ "#{NS_ADDRESSING}:MessageID" => "uuid:#{SecureRandom.uuid.to_s.upcase}",
453
+ "#{NS_WSMAN_DMTF}:Locale/" => '',
454
+ "#{NS_WSMAN_MSFT}:DataLocale/" => '',
455
+ "#{NS_WSMAN_DMTF}:OperationTimeout" => @timeout,
456
+ :attributes! => {
457
+ "#{NS_WSMAN_DMTF}:MaxEnvelopeSize" => {'mustUnderstand' => true},
458
+ "#{NS_WSMAN_DMTF}:Locale/" => {'xml:lang' => @locale, 'mustUnderstand' => false},
459
+ "#{NS_WSMAN_MSFT}:DataLocale/" => {'xml:lang' => @locale, 'mustUnderstand' => false}
460
+ }}
461
+ end
462
+
463
+ # merge the various header hashes and make sure we carry all of the attributes
464
+ # through instead of overwriting them.
465
+ def merge_headers(*headers)
466
+ hdr = {}
467
+ headers.each do |h|
468
+ hdr.merge!(h) do |k,v1,v2|
469
+ v1.merge!(v2) if k == :attributes!
470
+ end
471
+ end
472
+ hdr
473
+ end
474
+
475
+ def send_get_output_message(message)
476
+ send_message(message)
477
+ rescue WinRMWSManFault => e
478
+ # If no output is available before the wsman:OperationTimeout expires,
479
+ # the server MUST return a WSManFault with the Code attribute equal to
480
+ # 2150858793. When the client receives this fault, it SHOULD issue
481
+ # another Receive request.
482
+ # http://msdn.microsoft.com/en-us/library/cc251676.aspx
483
+ if e.fault_code == '2150858793'
484
+ logger.debug("[WinRM] retrying receive request after timeout")
485
+ retry
486
+ else
487
+ raise
488
+ end
489
+ end
490
+
491
+ def send_message(message)
492
+ @xfer.send_request(message)
493
+ end
494
+
495
+
496
+ # Helper methods for SOAP Headers
497
+
498
+ def resource_uri_cmd
499
+ {"#{NS_WSMAN_DMTF}:ResourceURI" => 'http://schemas.microsoft.com/wbem/wsman/1/windows/shell/cmd',
500
+ :attributes! => {"#{NS_WSMAN_DMTF}:ResourceURI" => {'mustUnderstand' => true}}}
501
+ end
502
+
503
+ def resource_uri_wmi(namespace = 'root/cimv2/*')
504
+ {"#{NS_WSMAN_DMTF}:ResourceURI" => "http://schemas.microsoft.com/wbem/wsman/1/wmi/#{namespace}",
505
+ :attributes! => {"#{NS_WSMAN_DMTF}:ResourceURI" => {'mustUnderstand' => true}}}
506
+ end
507
+
508
+ def action_create
509
+ {"#{NS_ADDRESSING}:Action" => 'http://schemas.xmlsoap.org/ws/2004/09/transfer/Create',
510
+ :attributes! => {"#{NS_ADDRESSING}:Action" => {'mustUnderstand' => true}}}
511
+ end
512
+
513
+ def action_delete
514
+ {"#{NS_ADDRESSING}:Action" => 'http://schemas.xmlsoap.org/ws/2004/09/transfer/Delete',
515
+ :attributes! => {"#{NS_ADDRESSING}:Action" => {'mustUnderstand' => true}}}
516
+ end
517
+
518
+ def action_command
519
+ {"#{NS_ADDRESSING}:Action" => 'http://schemas.microsoft.com/wbem/wsman/1/windows/shell/Command',
520
+ :attributes! => {"#{NS_ADDRESSING}:Action" => {'mustUnderstand' => true}}}
521
+ end
522
+
523
+ def action_receive
524
+ {"#{NS_ADDRESSING}:Action" => 'http://schemas.microsoft.com/wbem/wsman/1/windows/shell/Receive',
525
+ :attributes! => {"#{NS_ADDRESSING}:Action" => {'mustUnderstand' => true}}}
526
+ end
527
+
528
+ def action_signal
529
+ {"#{NS_ADDRESSING}:Action" => 'http://schemas.microsoft.com/wbem/wsman/1/windows/shell/Signal',
530
+ :attributes! => {"#{NS_ADDRESSING}:Action" => {'mustUnderstand' => true}}}
531
+ end
532
+
533
+ def action_send
534
+ {"#{NS_ADDRESSING}:Action" => 'http://schemas.microsoft.com/wbem/wsman/1/windows/shell/Send',
535
+ :attributes! => {"#{NS_ADDRESSING}:Action" => {'mustUnderstand' => true}}}
536
+ end
537
+
538
+ def action_enumerate
539
+ {"#{NS_ADDRESSING}:Action" => 'http://schemas.xmlsoap.org/ws/2004/09/enumeration/Enumerate',
540
+ :attributes! => {"#{NS_ADDRESSING}:Action" => {'mustUnderstand' => true}}}
541
+ end
542
+
543
+ def selector_shell_id(shell_id)
544
+ {"#{NS_WSMAN_DMTF}:SelectorSet" =>
545
+ {"#{NS_WSMAN_DMTF}:Selector" => shell_id, :attributes! => {"#{NS_WSMAN_DMTF}:Selector" => {'Name' => 'ShellId'}}}
546
+ }
547
+ end
548
+
549
+ end # WinRMWebService
550
+ end # WinRM