winrm 1.7.0 → 1.7.1

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