winrm 1.7.2 → 1.7.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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