winrm 1.7.2 → 1.7.3

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/.rubocop.yml +10 -0
  5. data/.travis.yml +12 -12
  6. data/Gemfile +9 -9
  7. data/LICENSE +202 -202
  8. data/README.md +194 -194
  9. data/Rakefile +36 -36
  10. data/Vagrantfile +9 -9
  11. data/appveyor.yml +42 -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 +15 -0
  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 +3 -3
  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 +547 -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 +29 -0
  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 +124 -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 +2 -2
@@ -43,7 +43,7 @@ module WinRM
43
43
  ssl_peer_fingerprint_verification!
44
44
  log_soap_message(message)
45
45
  hdr = { 'Content-Type' => 'application/soap+xml;charset=UTF-8',
46
- 'Content-Length' => message.length }
46
+ 'Content-Length' => message.bytesize }
47
47
  resp = @httpcli.post(@endpoint, message, hdr)
48
48
  log_soap_message(resp.http_body.content)
49
49
  verify_ssl_fingerprint(resp.peer_cert)
@@ -165,7 +165,7 @@ module WinRM
165
165
  ssl_peer_fingerprint_verification!
166
166
  auth_header = init_auth if @ntlmcli.session.nil?
167
167
 
168
- original_length = message.length
168
+ original_length = message.bytesize
169
169
 
170
170
  emessage = @ntlmcli.session.seal_message message
171
171
  signature = @ntlmcli.session.sign_message message
@@ -290,7 +290,7 @@ module WinRM
290
290
  # @returns [Object] The HTTP response object
291
291
  def send_kerberos_request(message)
292
292
  log_soap_message(message)
293
- original_length = message.length
293
+ original_length = message.bytesize
294
294
  pad_len, emsg = winrm_encrypt(message)
295
295
  hdr = {
296
296
  'Connection' => 'Keep-Alive',
data/lib/winrm/output.rb CHANGED
@@ -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'
data/lib/winrm/version.rb CHANGED
@@ -3,5 +3,5 @@
3
3
  # WinRM module
4
4
  module WinRM
5
5
  # The version of the WinRM library
6
- VERSION = '1.7.2'
6
+ VERSION = '1.7.3'
7
7
  end
@@ -1,547 +1,547 @@
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
+ 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