winrm 1.7.1 → 1.7.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +10 -10
  3. data/.rspec +3 -3
  4. data/.rubocop.yml +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