winrm 1.7.1 → 1.7.2

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