winrm 1.7.0 → 1.7.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (49) 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 +74 -71
  14. data/lib/winrm.rb +42 -42
  15. data/lib/winrm/command_executor.rb +224 -224
  16. data/lib/winrm/exceptions/exceptions.rb +57 -57
  17. data/lib/winrm/helpers/iso8601_duration.rb +58 -58
  18. data/lib/winrm/helpers/powershell_script.rb +42 -42
  19. data/lib/winrm/http/response_handler.rb +82 -82
  20. data/lib/winrm/http/transport.rb +421 -421
  21. data/lib/winrm/output.rb +43 -43
  22. data/lib/winrm/soap_provider.rb +39 -39
  23. data/lib/winrm/version.rb +7 -7
  24. data/lib/winrm/winrm_service.rb +556 -556
  25. data/preamble +17 -17
  26. data/spec/auth_timeout_spec.rb +16 -16
  27. data/spec/cmd_spec.rb +102 -102
  28. data/spec/command_executor_spec.rb +429 -429
  29. data/spec/config-example.yml +19 -19
  30. data/spec/exception_spec.rb +50 -50
  31. data/spec/issue_184_spec.rb +67 -67
  32. data/spec/issue_59_spec.rb +23 -23
  33. data/spec/matchers.rb +74 -74
  34. data/spec/output_spec.rb +110 -110
  35. data/spec/powershell_spec.rb +97 -97
  36. data/spec/response_handler_spec.rb +59 -59
  37. data/spec/spec_helper.rb +73 -73
  38. data/spec/stubs/responses/get_command_output_response.xml.erb +13 -13
  39. data/spec/stubs/responses/open_shell_v1.xml +19 -19
  40. data/spec/stubs/responses/open_shell_v2.xml +20 -20
  41. data/spec/stubs/responses/soap_fault_v1.xml +36 -36
  42. data/spec/stubs/responses/soap_fault_v2.xml +42 -42
  43. data/spec/stubs/responses/wmi_error_v2.xml +41 -41
  44. data/spec/transport_spec.rb +124 -124
  45. data/spec/winrm_options_spec.rb +76 -76
  46. data/spec/winrm_primitives_spec.rb +51 -51
  47. data/spec/wql_spec.rb +14 -14
  48. data/winrm.gemspec +40 -40
  49. metadata +2 -2
@@ -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'
@@ -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.0'
7
- end
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,556 +1,556 @@
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 '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