winrm 1.8.1 → 2.0.0

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 (139) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +10 -11
  3. data/.rubocop.yml +26 -22
  4. data/.travis.yml +11 -12
  5. data/Gemfile +3 -9
  6. data/LICENSE +202 -202
  7. data/README.md +232 -215
  8. data/Rakefile +34 -36
  9. data/Vagrantfile +6 -9
  10. data/WinrmAppveyor.psm1 +31 -31
  11. data/appveyor.yml +51 -51
  12. data/bin/rwinrm +97 -97
  13. data/changelog.md +86 -86
  14. data/lib/winrm.rb +39 -42
  15. data/lib/winrm/connection.rb +82 -0
  16. data/lib/winrm/connection_opts.rb +87 -0
  17. data/lib/winrm/{exceptions/exceptions.rb → exceptions.rb} +76 -57
  18. data/lib/winrm/http/response_handler.rb +96 -82
  19. data/lib/winrm/http/transport.rb +424 -435
  20. data/lib/winrm/http/transport_factory.rb +68 -0
  21. data/lib/winrm/output.rb +59 -43
  22. data/lib/winrm/psrp/create_pipeline.xml.erb +167 -0
  23. data/lib/winrm/psrp/fragment.rb +70 -0
  24. data/lib/winrm/psrp/init_runspace_pool.xml.erb +224 -0
  25. data/lib/winrm/psrp/message.rb +130 -0
  26. data/lib/winrm/psrp/message_data.rb +41 -0
  27. data/lib/winrm/psrp/message_data/base.rb +49 -0
  28. data/lib/winrm/psrp/message_data/error_record.rb +68 -0
  29. data/lib/winrm/psrp/message_data/pipeline_host_call.rb +32 -0
  30. data/lib/winrm/psrp/message_data/pipeline_output.rb +49 -0
  31. data/lib/winrm/psrp/message_data/runspacepool_host_call.rb +32 -0
  32. data/lib/winrm/psrp/message_data/runspacepool_state.rb +39 -0
  33. data/lib/winrm/psrp/message_data/session_capability.rb +36 -0
  34. data/lib/winrm/psrp/message_defragmenter.rb +62 -0
  35. data/lib/winrm/psrp/message_factory.rb +75 -0
  36. data/lib/winrm/psrp/message_fragmenter.rb +60 -0
  37. data/lib/winrm/psrp/powershell_output_decoder.rb +120 -0
  38. data/lib/winrm/psrp/receive_response_reader.rb +93 -0
  39. data/lib/winrm/psrp/session_capability.xml.erb +7 -0
  40. data/lib/winrm/psrp/uuid.rb +40 -0
  41. data/lib/winrm/shells/base.rb +175 -0
  42. data/lib/winrm/shells/cmd.rb +65 -0
  43. data/lib/winrm/shells/power_shell.rb +201 -0
  44. data/lib/winrm/shells/retryable.rb +45 -0
  45. data/lib/winrm/shells/shell_factory.rb +50 -0
  46. data/lib/winrm/version.rb +7 -7
  47. data/lib/winrm/wsmv/base.rb +59 -0
  48. data/lib/winrm/wsmv/cleanup_command.rb +61 -0
  49. data/lib/winrm/wsmv/close_shell.rb +50 -0
  50. data/lib/winrm/wsmv/command.rb +101 -0
  51. data/lib/winrm/wsmv/command_output.rb +76 -0
  52. data/lib/winrm/wsmv/command_output_decoder.rb +55 -0
  53. data/lib/winrm/wsmv/configuration.rb +46 -0
  54. data/lib/winrm/wsmv/create_pipeline.rb +66 -0
  55. data/lib/winrm/wsmv/create_shell.rb +119 -0
  56. data/lib/winrm/wsmv/header.rb +203 -0
  57. data/lib/winrm/wsmv/init_runspace_pool.rb +95 -0
  58. data/lib/winrm/wsmv/iso8601_duration.rb +60 -0
  59. data/lib/winrm/wsmv/keep_alive.rb +68 -0
  60. data/lib/winrm/wsmv/receive_response_reader.rb +128 -0
  61. data/lib/winrm/wsmv/send_data.rb +68 -0
  62. data/lib/winrm/wsmv/soap.rb +51 -0
  63. data/lib/winrm/wsmv/wql_query.rb +79 -0
  64. data/lib/winrm/wsmv/write_stdin.rb +88 -0
  65. data/preamble +17 -17
  66. data/{spec → tests/integration}/auth_timeout_spec.rb +18 -16
  67. data/{spec → tests/integration}/cmd_spec.rb +104 -102
  68. data/{spec → tests/integration}/config-example.yml +16 -19
  69. data/{spec → tests/integration}/issue_59_spec.rb +26 -23
  70. data/tests/integration/powershell_spec.rb +154 -0
  71. data/{spec → tests/integration}/spec_helper.rb +65 -73
  72. data/{spec → tests/integration}/transport_spec.rb +99 -139
  73. data/{spec → tests/integration}/wql_spec.rb +16 -14
  74. data/{spec → tests}/matchers.rb +60 -74
  75. data/tests/spec/configuration_spec.rb +93 -0
  76. data/tests/spec/connection_spec.rb +39 -0
  77. data/{spec → tests/spec}/exception_spec.rb +50 -50
  78. data/tests/spec/http/transport_factory_spec.rb +68 -0
  79. data/tests/spec/http/transport_spec.rb +44 -0
  80. data/{spec → tests/spec}/output_spec.rb +127 -110
  81. data/tests/spec/psrp/fragment_spec.rb +62 -0
  82. data/tests/spec/psrp/message_data/base_spec.rb +13 -0
  83. data/tests/spec/psrp/message_data/error_record_spec.rb +41 -0
  84. data/tests/spec/psrp/message_data/pipeline_host_call_spec.rb +25 -0
  85. data/tests/spec/psrp/message_data/pipeline_output_spec.rb +32 -0
  86. data/tests/spec/psrp/message_data/runspace_pool_host_call_spec.rb +25 -0
  87. data/tests/spec/psrp/message_data/runspacepool_state_spec.rb +16 -0
  88. data/tests/spec/psrp/message_data/session_capability_spec.rb +30 -0
  89. data/tests/spec/psrp/message_data_spec.rb +35 -0
  90. data/tests/spec/psrp/message_defragmenter_spec.rb +47 -0
  91. data/tests/spec/psrp/message_fragmenter_spec.rb +105 -0
  92. data/tests/spec/psrp/powershell_output_decoder_spec.rb +84 -0
  93. data/tests/spec/psrp/psrp_message_spec.rb +70 -0
  94. data/tests/spec/psrp/recieve_response_reader_spec.rb +154 -0
  95. data/tests/spec/psrp/uuid_spec.rb +28 -0
  96. data/{spec → tests/spec}/response_handler_spec.rb +61 -61
  97. data/tests/spec/shells/base_spec.rb +202 -0
  98. data/tests/spec/shells/cmd_spec.rb +75 -0
  99. data/tests/spec/shells/powershell_spec.rb +175 -0
  100. data/tests/spec/spec_helper.rb +47 -0
  101. data/tests/spec/stubs/clixml/error_record.xml.erb +84 -0
  102. data/{spec → tests/spec}/stubs/responses/get_command_output_response.xml.erb +13 -13
  103. data/tests/spec/stubs/responses/get_command_output_response_not_done.xml.erb +10 -0
  104. data/tests/spec/stubs/responses/get_powershell_keepalive_response.xml.erb +10 -0
  105. data/tests/spec/stubs/responses/get_powershell_output_response.xml.erb +12 -0
  106. data/tests/spec/stubs/responses/get_powershell_output_response_not_done.xml.erb +9 -0
  107. data/{spec → tests/spec}/stubs/responses/open_shell_v1.xml +19 -19
  108. data/{spec → tests/spec}/stubs/responses/open_shell_v2.xml +20 -20
  109. data/{spec → tests/spec}/stubs/responses/soap_fault_v1.xml +36 -36
  110. data/{spec → tests/spec}/stubs/responses/soap_fault_v2.xml +42 -42
  111. data/{spec → tests/spec}/stubs/responses/wmi_error_v2.xml +41 -41
  112. data/tests/spec/wsmv/cleanup_command_spec.rb +22 -0
  113. data/tests/spec/wsmv/close_shell_spec.rb +17 -0
  114. data/{spec → tests/spec/wsmv}/command_output_decoder_spec.rb +37 -37
  115. data/tests/spec/wsmv/command_output_spec.rb +45 -0
  116. data/tests/spec/wsmv/command_spec.rb +19 -0
  117. data/tests/spec/wsmv/configuration_spec.rb +17 -0
  118. data/tests/spec/wsmv/create_pipeline_spec.rb +31 -0
  119. data/tests/spec/wsmv/create_shell_spec.rb +38 -0
  120. data/tests/spec/wsmv/init_runspace_pool_spec.rb +36 -0
  121. data/tests/spec/wsmv/keep_alive_spec.rb +21 -0
  122. data/tests/spec/wsmv/receive_response_reader_spec.rb +123 -0
  123. data/tests/spec/wsmv/send_data_spec.rb +30 -0
  124. data/tests/spec/wsmv/wql_query_spec.rb +13 -0
  125. data/tests/spec/wsmv/write_stdin_spec.rb +22 -0
  126. data/winrm.gemspec +42 -40
  127. metadata +140 -38
  128. data/.rspec +0 -3
  129. data/lib/winrm/command_executor.rb +0 -243
  130. data/lib/winrm/command_output_decoder.rb +0 -53
  131. data/lib/winrm/helpers/iso8601_duration.rb +0 -58
  132. data/lib/winrm/helpers/powershell_script.rb +0 -42
  133. data/lib/winrm/soap_provider.rb +0 -39
  134. data/lib/winrm/winrm_service.rb +0 -550
  135. data/spec/command_executor_spec.rb +0 -475
  136. data/spec/issue_184_spec.rb +0 -67
  137. data/spec/powershell_spec.rb +0 -97
  138. data/spec/winrm_options_spec.rb +0 -76
  139. data/spec/winrm_primitives_spec.rb +0 -51
@@ -1,435 +1,424 @@
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_relative 'response_handler'
18
-
19
- module WinRM
20
- module HTTP
21
- # A generic HTTP transport that utilized HTTPClient to send messages back and forth.
22
- # This backend will maintain state for every WinRMWebService instance that is instantiated so it
23
- # is possible to use GSSAPI with Keep-Alive.
24
- class HttpTransport
25
-
26
- attr_reader :endpoint
27
-
28
- def initialize(endpoint)
29
- @endpoint = endpoint.is_a?(String) ? URI.parse(endpoint) : endpoint
30
- @httpcli = HTTPClient.new(agent_name: 'Ruby WinRM Client')
31
- @logger = Logging.logger[self]
32
- end
33
-
34
- # Sends the SOAP payload to the WinRM service and returns the service's
35
- # SOAP response. If an error occurrs an appropriate error is raised.
36
- #
37
- # @param [String] The XML SOAP message
38
- # @returns [REXML::Document] The parsed response body
39
- def send_request(message)
40
- ssl_peer_fingerprint_verification!
41
- log_soap_message(message)
42
- hdr = { 'Content-Type' => 'application/soap+xml;charset=UTF-8',
43
- 'Content-Length' => message.bytesize }
44
- # We need to add this header if using Client Certificate authentication
45
- unless @httpcli.ssl_config.client_cert.nil?
46
- hdr['Authorization'] = 'http://schemas.dmtf.org/wbem/wsman/1/wsman/secprofile/https/mutual'
47
- end
48
-
49
- resp = @httpcli.post(@endpoint, message, hdr)
50
- log_soap_message(resp.http_body.content)
51
- verify_ssl_fingerprint(resp.peer_cert)
52
- handler = WinRM::ResponseHandler.new(resp.http_body.content, resp.status)
53
- handler.parse_to_xml
54
- end
55
-
56
- # We'll need this to force basic authentication if desired
57
- def basic_auth_only!
58
- auths = @httpcli.www_auth.instance_variable_get('@authenticator')
59
- auths.delete_if { |i| i.scheme !~ /basic/i }
60
- end
61
-
62
- # Disable SSPI Auth
63
- def no_sspi_auth!
64
- auths = @httpcli.www_auth.instance_variable_get('@authenticator')
65
- auths.delete_if { |i| i.is_a? HTTPClient::SSPINegotiateAuth }
66
- end
67
-
68
- # Disable SSL Peer Verification
69
- def no_ssl_peer_verification!
70
- @httpcli.ssl_config.verify_mode = OpenSSL::SSL::VERIFY_NONE
71
- end
72
-
73
- # SSL Peer Fingerprint Verification prior to connecting
74
- def ssl_peer_fingerprint_verification!
75
- return unless @ssl_peer_fingerprint && ! @ssl_peer_fingerprint_verified
76
-
77
- with_untrusted_ssl_connection do |connection|
78
- connection_cert = connection.peer_cert_chain.last
79
- verify_ssl_fingerprint(connection_cert)
80
- end
81
- @logger.info("initial ssl fingerprint #{@ssl_peer_fingerprint} verified\n")
82
- @ssl_peer_fingerprint_verified = true
83
- no_ssl_peer_verification!
84
- end
85
-
86
- # Connect without verification to retrieve untrusted ssl context
87
- def with_untrusted_ssl_connection
88
- noverify_peer_context = OpenSSL::SSL::SSLContext.new
89
- noverify_peer_context.verify_mode = OpenSSL::SSL::VERIFY_NONE
90
- tcp_connection = TCPSocket.new(@endpoint.host, @endpoint.port)
91
- begin
92
- ssl_connection = OpenSSL::SSL::SSLSocket.new(tcp_connection, noverify_peer_context)
93
- ssl_connection.connect
94
- yield ssl_connection
95
- ensure
96
- tcp_connection.close
97
- end
98
- end
99
-
100
- # compare @ssl_peer_fingerprint to current ssl context
101
- def verify_ssl_fingerprint(cert)
102
- return unless @ssl_peer_fingerprint
103
- conn_fingerprint = OpenSSL::Digest::SHA1.new(cert.to_der).to_s
104
- return unless @ssl_peer_fingerprint.casecmp(conn_fingerprint) != 0
105
- fail "ssl fingerprint mismatch!!!!\n"
106
- end
107
-
108
- # HTTP Client receive timeout. How long should a remote call wait for a
109
- # for a response from WinRM?
110
- def receive_timeout=(sec)
111
- @httpcli.receive_timeout = sec
112
- end
113
-
114
- def receive_timeout
115
- @httpcli.receive_timeout
116
- end
117
-
118
- protected
119
-
120
- def log_soap_message(message)
121
- return unless @logger.debug?
122
-
123
- xml_msg = REXML::Document.new(message)
124
- formatter = REXML::Formatters::Pretty.new(2)
125
- formatter.compact = true
126
- formatter.write(xml_msg, @logger)
127
- @logger.debug("\n")
128
- rescue StandardError => e
129
- @logger.debug("Couldn't log SOAP request/response: #{e.message} - #{message}")
130
- end
131
- end
132
-
133
-
134
- # Plain text, insecure, HTTP transport
135
- class HttpPlaintext < HttpTransport
136
- def initialize(endpoint, user, pass, opts)
137
- super(endpoint)
138
- @httpcli.set_auth(nil, user, pass)
139
- no_sspi_auth! if opts[:disable_sspi]
140
- basic_auth_only! if opts[:basic_auth_only]
141
- no_ssl_peer_verification! if opts[:no_ssl_peer_verification]
142
- end
143
- end
144
-
145
-
146
- # NTLM/Negotiate, secure, HTTP transport
147
- class HttpNegotiate < HttpTransport
148
- def initialize(endpoint, user, pass, opts)
149
- super(endpoint)
150
- require 'rubyntlm'
151
- no_sspi_auth!
152
-
153
- user_parts = user.split('\\')
154
- if(user_parts.length > 1)
155
- opts[:domain] = user_parts[0]
156
- user = user_parts[1]
157
- end
158
-
159
- @ntlmcli = Net::NTLM::Client.new(user, pass, opts)
160
- @retryable = true
161
- no_ssl_peer_verification! if opts[:no_ssl_peer_verification]
162
- @ssl_peer_fingerprint = opts[:ssl_peer_fingerprint]
163
- @httpcli.ssl_config.set_trust_ca(opts[:ca_trust_path]) if opts[:ca_trust_path]
164
- end
165
-
166
- def send_request(message, auth_header = nil)
167
- ssl_peer_fingerprint_verification!
168
- auth_header = init_auth if @ntlmcli.session.nil?
169
-
170
- original_length = message.bytesize
171
-
172
- emessage = @ntlmcli.session.seal_message message
173
- signature = @ntlmcli.session.sign_message message
174
- seal = "\x10\x00\x00\x00#{signature}#{emessage}"
175
-
176
- hdr = {
177
- "Content-Type" => "multipart/encrypted;protocol=\"application/HTTP-SPNEGO-session-encrypted\";boundary=\"Encrypted Boundary\""
178
- }
179
- hdr.merge!(auth_header) if auth_header
180
-
181
- body = [
182
- "--Encrypted Boundary",
183
- "Content-Type: application/HTTP-SPNEGO-session-encrypted",
184
- "OriginalContent: type=application/soap+xml;charset=UTF-8;Length=#{original_length}",
185
- "--Encrypted Boundary",
186
- "Content-Type: application/octet-stream",
187
- "#{seal}--Encrypted Boundary--",
188
- ""
189
- ].join("\r\n")
190
-
191
- resp = @httpcli.post(@endpoint, body, hdr)
192
- verify_ssl_fingerprint(resp.peer_cert)
193
- if resp.status == 401 && @retryable
194
- @retryable = false
195
- send_request(message, init_auth)
196
- else
197
- @retryable = true
198
- decrypted_body = resp.body.empty? ? '' : winrm_decrypt(resp.body)
199
- handler = WinRM::ResponseHandler.new(decrypted_body, resp.status)
200
- handler.parse_to_xml()
201
- end
202
- end
203
-
204
- private
205
-
206
- def winrm_decrypt(str)
207
- str.force_encoding('BINARY')
208
- str.sub!(/^.*Content-Type: application\/octet-stream\r\n(.*)--Encrypted.*$/m, '\1')
209
-
210
- signature = str[4..19]
211
- message = @ntlmcli.session.unseal_message str[20..-1]
212
- if @ntlmcli.session.verify_signature(signature, message)
213
- message
214
- else
215
- raise WinRMWebServiceError, "Could not verify SOAP message."
216
- end
217
- end
218
-
219
- def init_auth
220
- @logger.debug "Initializing Negotiate for #{@service}"
221
- auth1 = @ntlmcli.init_context
222
- hdr = {"Authorization" => "Negotiate #{auth1.encode64}",
223
- "Content-Type" => "application/soap+xml;charset=UTF-8"
224
- }
225
- @logger.debug "Sending HTTP POST for Negotiate Authentication"
226
- r = @httpcli.post(@endpoint, "", hdr)
227
- verify_ssl_fingerprint(r.peer_cert)
228
- itok = r.header["WWW-Authenticate"].pop.split.last
229
- binding = r.peer_cert.nil? ? nil : Net::NTLM::ChannelBinding.create(r.peer_cert)
230
- auth3 = @ntlmcli.init_context(itok, binding)
231
- { "Authorization" => "Negotiate #{auth3.encode64}" }
232
- end
233
- end
234
-
235
- # Uses SSL to secure the transport
236
- class BasicAuthSSL < HttpTransport
237
- def initialize(endpoint, user, pass, opts)
238
- super(endpoint)
239
- @httpcli.set_auth(endpoint, user, pass)
240
- basic_auth_only!
241
- no_ssl_peer_verification! if opts[:no_ssl_peer_verification]
242
- @ssl_peer_fingerprint = opts[:ssl_peer_fingerprint]
243
- @httpcli.ssl_config.set_trust_ca(opts[:ca_trust_path]) if opts[:ca_trust_path]
244
- end
245
- end
246
-
247
- # Uses Client Certificate to authenticate and SSL to secure the transport
248
- class ClientCertAuthSSL < HttpTransport
249
- def initialize(endpoint, client_cert, client_key, key_pass, opts)
250
- super(endpoint)
251
- @httpcli.ssl_config.set_client_cert_file(client_cert, client_key, key_pass)
252
- @httpcli.www_auth.instance_variable_set('@authenticator', [])
253
- no_ssl_peer_verification! if opts[:no_ssl_peer_verification]
254
- @ssl_peer_fingerprint = opts[:ssl_peer_fingerprint]
255
- @httpcli.ssl_config.set_trust_ca(opts[:ca_trust_path]) if opts[:ca_trust_path]
256
- end
257
- end
258
-
259
- # Uses Kerberos/GSSAPI to authenticate and encrypt messages
260
- # rubocop:disable Metrics/ClassLength
261
- class HttpGSSAPI < HttpTransport
262
- # @param [String,URI] endpoint the WinRM webservice endpoint
263
- # @param [String] realm the Kerberos realm we are authenticating to
264
- # @param [String<optional>] service the service name, default is HTTP
265
- # @param [String<optional>] keytab the path to a keytab file if you are using one
266
- # rubocop:disable Lint/UnusedMethodArgument
267
- def initialize(endpoint, realm, service = nil, keytab = nil, opts)
268
- # rubocop:enable Lint/UnusedMethodArgument
269
- super(endpoint)
270
- # Remove the GSSAPI auth from HTTPClient because we are doing our own thing
271
- no_sspi_auth!
272
- service ||= 'HTTP'
273
- @service = "#{service}/#{@endpoint.host}@#{realm}"
274
- init_krb
275
- end
276
-
277
- # Sends the SOAP payload to the WinRM service and returns the service's
278
- # SOAP response. If an error occurrs an appropriate error is raised.
279
- #
280
- # @param [String] The XML SOAP message
281
- # @returns [REXML::Document] The parsed response body
282
- def send_request(message)
283
- resp = send_kerberos_request(message)
284
-
285
- if resp.status == 401
286
- @logger.debug 'Got 401 - reinitializing Kerberos and retrying one more time'
287
- init_krb
288
- resp = send_kerberos_request(message)
289
- end
290
-
291
- handler = WinRM::ResponseHandler.new(winrm_decrypt(resp.http_body.content), resp.status)
292
- handler.parse_to_xml
293
- end
294
-
295
- private
296
-
297
- # rubocop:disable Metrics/MethodLength
298
- # rubocop:disable Metrics/AbcSize
299
-
300
- # Sends the SOAP payload to the WinRM service and returns the service's
301
- # HTTP response.
302
- #
303
- # @param [String] The XML SOAP message
304
- # @returns [Object] The HTTP response object
305
- def send_kerberos_request(message)
306
- log_soap_message(message)
307
- original_length = message.bytesize
308
- pad_len, emsg = winrm_encrypt(message)
309
- hdr = {
310
- 'Connection' => 'Keep-Alive',
311
- 'Content-Type' =>
312
- 'multipart/encrypted;' \
313
- 'protocol="application/HTTP-Kerberos-session-encrypted";' \
314
- 'boundary="Encrypted Boundary"'
315
- }
316
- body = [
317
- "--Encrypted Boundary",
318
- "Content-Type: application/HTTP-Kerberos-session-encrypted",
319
- "OriginalContent: type=application/soap+xml;charset=UTF-8;Length=#{original_length + pad_len}",
320
- "--Encrypted Boundary",
321
- "Content-Type: application/octet-stream",
322
- "#{emsg}--Encrypted Boundary--",
323
- ""
324
- ].join("\r\n")
325
-
326
- resp = @httpcli.post(@endpoint, body, hdr)
327
- log_soap_message(resp.http_body.content)
328
- resp
329
- end
330
-
331
- def init_krb
332
- @logger.debug "Initializing Kerberos for #{@service}"
333
- @gsscli = GSSAPI::Simple.new(@endpoint.host, @service)
334
- token = @gsscli.init_context
335
- auth = Base64.strict_encode64 token
336
-
337
- hdr = {
338
- 'Authorization' => "Kerberos #{auth}",
339
- 'Connection' => 'Keep-Alive',
340
- 'Content-Type' => 'application/soap+xml;charset=UTF-8'
341
- }
342
- @logger.debug 'Sending HTTP POST for Kerberos Authentication'
343
- r = @httpcli.post(@endpoint, '', hdr)
344
- itok = r.header['WWW-Authenticate'].pop
345
- itok = itok.split.last
346
- itok = Base64.strict_decode64(itok)
347
- @gsscli.init_context(itok)
348
- end
349
-
350
- # @return [String] the encrypted request string
351
- def winrm_encrypt(str)
352
- @logger.debug "Encrypting SOAP message:\n#{str}"
353
- iov_cnt = 3
354
- iov = FFI::MemoryPointer.new(GSSAPI::LibGSSAPI::GssIOVBufferDesc.size * iov_cnt)
355
-
356
- iov0 = GSSAPI::LibGSSAPI::GssIOVBufferDesc.new(FFI::Pointer.new(iov.address))
357
- iov0[:type] = (GSSAPI::LibGSSAPI::GSS_IOV_BUFFER_TYPE_HEADER | \
358
- GSSAPI::LibGSSAPI::GSS_IOV_BUFFER_FLAG_ALLOCATE)
359
-
360
- iov1 = GSSAPI::LibGSSAPI::GssIOVBufferDesc.new(
361
- FFI::Pointer.new(iov.address + (GSSAPI::LibGSSAPI::GssIOVBufferDesc.size * 1)))
362
- iov1[:type] = (GSSAPI::LibGSSAPI::GSS_IOV_BUFFER_TYPE_DATA)
363
- iov1[:buffer].value = str
364
-
365
- iov2 = GSSAPI::LibGSSAPI::GssIOVBufferDesc.new(
366
- FFI::Pointer.new(iov.address + (GSSAPI::LibGSSAPI::GssIOVBufferDesc.size * 2)))
367
- iov2[:type] = (GSSAPI::LibGSSAPI::GSS_IOV_BUFFER_TYPE_PADDING | \
368
- GSSAPI::LibGSSAPI::GSS_IOV_BUFFER_FLAG_ALLOCATE)
369
-
370
- conf_state = FFI::MemoryPointer.new :uint32
371
- min_stat = FFI::MemoryPointer.new :uint32
372
-
373
- GSSAPI::LibGSSAPI.gss_wrap_iov(
374
- min_stat,
375
- @gsscli.context,
376
- 1,
377
- GSSAPI::LibGSSAPI::GSS_C_QOP_DEFAULT,
378
- conf_state,
379
- iov,
380
- iov_cnt)
381
-
382
- token = [iov0[:buffer].length].pack('L')
383
- token += iov0[:buffer].value
384
- token += iov1[:buffer].value
385
- pad_len = iov2[:buffer].length
386
- token += iov2[:buffer].value if pad_len > 0
387
- [pad_len, token]
388
- end
389
-
390
- # @return [String] the unencrypted response string
391
- def winrm_decrypt(str)
392
- @logger.debug "Decrypting SOAP message:\n#{str}"
393
- iov_cnt = 3
394
- iov = FFI::MemoryPointer.new(GSSAPI::LibGSSAPI::GssIOVBufferDesc.size * iov_cnt)
395
-
396
- iov0 = GSSAPI::LibGSSAPI::GssIOVBufferDesc.new(FFI::Pointer.new(iov.address))
397
- iov0[:type] = (GSSAPI::LibGSSAPI::GSS_IOV_BUFFER_TYPE_HEADER | \
398
- GSSAPI::LibGSSAPI::GSS_IOV_BUFFER_FLAG_ALLOCATE)
399
-
400
- iov1 = GSSAPI::LibGSSAPI::GssIOVBufferDesc.new(
401
- FFI::Pointer.new(iov.address + (GSSAPI::LibGSSAPI::GssIOVBufferDesc.size * 1)))
402
- iov1[:type] = (GSSAPI::LibGSSAPI::GSS_IOV_BUFFER_TYPE_DATA)
403
-
404
- iov2 = GSSAPI::LibGSSAPI::GssIOVBufferDesc.new(
405
- FFI::Pointer.new(iov.address + (GSSAPI::LibGSSAPI::GssIOVBufferDesc.size * 2)))
406
- iov2[:type] = (GSSAPI::LibGSSAPI::GSS_IOV_BUFFER_TYPE_DATA)
407
-
408
- str.force_encoding('BINARY')
409
- str.sub!(/^.*Content-Type: application\/octet-stream\r\n(.*)--Encrypted.*$/m, '\1')
410
-
411
- len = str.unpack('L').first
412
- iov_data = str.unpack("LA#{len}A*")
413
- iov0[:buffer].value = iov_data[1]
414
- iov1[:buffer].value = iov_data[2]
415
-
416
- min_stat = FFI::MemoryPointer.new :uint32
417
- conf_state = FFI::MemoryPointer.new :uint32
418
- conf_state.write_int(1)
419
- qop_state = FFI::MemoryPointer.new :uint32
420
- qop_state.write_int(0)
421
-
422
- maj_stat = GSSAPI::LibGSSAPI.gss_unwrap_iov(
423
- min_stat, @gsscli.context, conf_state, qop_state, iov, iov_cnt)
424
-
425
- @logger.debug "SOAP message decrypted (MAJ: #{maj_stat}, " \
426
- "MIN: #{min_stat.read_int}):\n#{iov1[:buffer].value}"
427
-
428
- iov1[:buffer].value
429
- end
430
- # rubocop:enable Metrics/MethodLength
431
- # rubocop:enable Metrics/AbcSize
432
- end
433
- # rubocop:enable Metrics/ClassLength
434
- end
435
- 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 'httpclient'
18
+ require_relative 'response_handler'
19
+
20
+ module WinRM
21
+ module HTTP
22
+ # A generic HTTP transport that utilized HTTPClient to send messages back and forth.
23
+ # This backend will maintain state for every WinRMWebService instance that is instantiated so it
24
+ # is possible to use GSSAPI with Keep-Alive.
25
+ class HttpTransport
26
+ attr_reader :endpoint
27
+
28
+ def initialize(endpoint, options)
29
+ @endpoint = endpoint.is_a?(String) ? URI.parse(endpoint) : endpoint
30
+ @httpcli = HTTPClient.new(agent_name: 'Ruby WinRM Client')
31
+ @logger = Logging.logger[self]
32
+ @httpcli.receive_timeout = options[:receive_timeout]
33
+ end
34
+
35
+ # Sends the SOAP payload to the WinRM service and returns the service's
36
+ # SOAP response. If an error occurrs an appropriate error is raised.
37
+ #
38
+ # @param [String] The XML SOAP message
39
+ # @returns [REXML::Document] The parsed response body
40
+ def send_request(message)
41
+ ssl_peer_fingerprint_verification!
42
+ log_soap_message(message)
43
+ hdr = { 'Content-Type' => 'application/soap+xml;charset=UTF-8',
44
+ 'Content-Length' => message.bytesize }
45
+ # We need to add this header if using Client Certificate authentication
46
+ unless @httpcli.ssl_config.client_cert.nil?
47
+ hdr['Authorization'] = 'http://schemas.dmtf.org/wbem/wsman/1/wsman/secprofile/https/mutual'
48
+ end
49
+
50
+ resp = @httpcli.post(@endpoint, message, hdr)
51
+ log_soap_message(resp.http_body.content)
52
+ verify_ssl_fingerprint(resp.peer_cert)
53
+ handler = WinRM::ResponseHandler.new(resp.http_body.content, resp.status)
54
+ handler.parse_to_xml
55
+ end
56
+
57
+ # We'll need this to force basic authentication if desired
58
+ def basic_auth_only!
59
+ auths = @httpcli.www_auth.instance_variable_get('@authenticator')
60
+ auths.delete_if { |i| i.scheme !~ /basic/i }
61
+ end
62
+
63
+ # Disable SSPI Auth
64
+ def no_sspi_auth!
65
+ auths = @httpcli.www_auth.instance_variable_get('@authenticator')
66
+ auths.delete_if { |i| i.is_a? HTTPClient::SSPINegotiateAuth }
67
+ end
68
+
69
+ # Disable SSL Peer Verification
70
+ def no_ssl_peer_verification!
71
+ @httpcli.ssl_config.verify_mode = OpenSSL::SSL::VERIFY_NONE
72
+ end
73
+
74
+ # SSL Peer Fingerprint Verification prior to connecting
75
+ def ssl_peer_fingerprint_verification!
76
+ return unless @ssl_peer_fingerprint && ! @ssl_peer_fingerprint_verified
77
+
78
+ with_untrusted_ssl_connection do |connection|
79
+ connection_cert = connection.peer_cert_chain.last
80
+ verify_ssl_fingerprint(connection_cert)
81
+ end
82
+ @logger.info("initial ssl fingerprint #{@ssl_peer_fingerprint} verified\n")
83
+ @ssl_peer_fingerprint_verified = true
84
+ no_ssl_peer_verification!
85
+ end
86
+
87
+ # Connect without verification to retrieve untrusted ssl context
88
+ def with_untrusted_ssl_connection
89
+ noverify_peer_context = OpenSSL::SSL::SSLContext.new
90
+ noverify_peer_context.verify_mode = OpenSSL::SSL::VERIFY_NONE
91
+ tcp_connection = TCPSocket.new(@endpoint.host, @endpoint.port)
92
+ begin
93
+ ssl_connection = OpenSSL::SSL::SSLSocket.new(tcp_connection, noverify_peer_context)
94
+ ssl_connection.connect
95
+ yield ssl_connection
96
+ ensure
97
+ tcp_connection.close
98
+ end
99
+ end
100
+
101
+ # compare @ssl_peer_fingerprint to current ssl context
102
+ def verify_ssl_fingerprint(cert)
103
+ return unless @ssl_peer_fingerprint
104
+ conn_fingerprint = OpenSSL::Digest::SHA1.new(cert.to_der).to_s
105
+ return unless @ssl_peer_fingerprint.casecmp(conn_fingerprint) != 0
106
+ raise "ssl fingerprint mismatch!!!!\n"
107
+ end
108
+
109
+ protected
110
+
111
+ def body(message, length, type = 'application/HTTP-SPNEGO-session-encrypted')
112
+ [
113
+ '--Encrypted Boundary',
114
+ "Content-Type: #{type}",
115
+ "OriginalContent: type=application/soap+xml;charset=UTF-8;Length=#{length}",
116
+ '--Encrypted Boundary',
117
+ 'Content-Type: application/octet-stream',
118
+ "#{message}--Encrypted Boundary--"
119
+ ].join("\r\n").concat("\r\n")
120
+ end
121
+
122
+ def log_soap_message(message)
123
+ return unless @logger.debug?
124
+
125
+ xml_msg = REXML::Document.new(message)
126
+ formatter = REXML::Formatters::Pretty.new(2)
127
+ formatter.compact = true
128
+ formatter.write(xml_msg, @logger)
129
+ @logger.debug("\n")
130
+ rescue StandardError => e
131
+ @logger.debug("Couldn't log SOAP request/response: #{e.message} - #{message}")
132
+ end
133
+ end
134
+
135
+ # Plain text, insecure, HTTP transport
136
+ class HttpPlaintext < HttpTransport
137
+ def initialize(endpoint, user, pass, opts)
138
+ super(endpoint, opts)
139
+ @httpcli.set_auth(nil, user, pass)
140
+ no_sspi_auth! if opts[:disable_sspi]
141
+ basic_auth_only! if opts[:basic_auth_only]
142
+ no_ssl_peer_verification! if opts[:no_ssl_peer_verification]
143
+ end
144
+ end
145
+
146
+ # NTLM/Negotiate, secure, HTTP transport
147
+ class HttpNegotiate < HttpTransport
148
+ def initialize(endpoint, user, pass, opts)
149
+ super(endpoint, opts)
150
+ require 'rubyntlm'
151
+ no_sspi_auth!
152
+
153
+ user_parts = user.split('\\')
154
+ if user_parts.length > 1
155
+ opts[:domain] = user_parts[0]
156
+ user = user_parts[1]
157
+ end
158
+
159
+ @ntlmcli = Net::NTLM::Client.new(user, pass, opts)
160
+ @retryable = true
161
+ no_ssl_peer_verification! if opts[:no_ssl_peer_verification]
162
+ @ssl_peer_fingerprint = opts[:ssl_peer_fingerprint]
163
+ @httpcli.ssl_config.set_trust_ca(opts[:ca_trust_path]) if opts[:ca_trust_path]
164
+ end
165
+
166
+ def send_request(message, auth_header = nil)
167
+ ssl_peer_fingerprint_verification!
168
+ auth_header = init_auth if @ntlmcli.session.nil?
169
+ log_soap_message(message)
170
+
171
+ hdr = {
172
+ 'Content-Type' => 'multipart/encrypted;'\
173
+ 'protocol="application/HTTP-SPNEGO-session-encrypted";boundary="Encrypted Boundary"'
174
+ }
175
+ hdr.merge!(auth_header) if auth_header
176
+
177
+ resp = @httpcli.post(@endpoint, body(seal(message), message.bytesize), hdr)
178
+ verify_ssl_fingerprint(resp.peer_cert)
179
+ if resp.status == 401 && @retryable
180
+ @retryable = false
181
+ send_request(message, init_auth)
182
+ else
183
+ @retryable = true
184
+ decrypted_body = winrm_decrypt(resp.body)
185
+ log_soap_message(decrypted_body)
186
+ WinRM::ResponseHandler.new(decrypted_body, resp.status).parse_to_xml
187
+ end
188
+ end
189
+
190
+ private
191
+
192
+ def seal(message)
193
+ emessage = @ntlmcli.session.seal_message message
194
+ signature = @ntlmcli.session.sign_message message
195
+ "\x10\x00\x00\x00#{signature}#{emessage}"
196
+ end
197
+
198
+ def winrm_decrypt(str)
199
+ return '' if str.empty?
200
+ str.force_encoding('BINARY')
201
+ str.sub!(%r{^.*Content-Type: application\/octet-stream\r\n(.*)--Encrypted.*$}m, '\1')
202
+
203
+ signature = str[4..19]
204
+ message = @ntlmcli.session.unseal_message str[20..-1]
205
+ if @ntlmcli.session.verify_signature(signature, message)
206
+ message
207
+ else
208
+ raise WinRMHTTPTransportError, 'Could not decrypt NTLM message.'
209
+ end
210
+ end
211
+
212
+ def init_auth
213
+ @logger.debug "Initializing Negotiate for #{@endpoint}"
214
+ auth1 = @ntlmcli.init_context
215
+ hdr = {
216
+ 'Authorization' => "Negotiate #{auth1.encode64}",
217
+ 'Content-Type' => 'application/soap+xml;charset=UTF-8'
218
+ }
219
+ @logger.debug 'Sending HTTP POST for Negotiate Authentication'
220
+ r = @httpcli.post(@endpoint, '', hdr)
221
+ verify_ssl_fingerprint(r.peer_cert)
222
+ auth_header = r.header['WWW-Authenticate'].pop
223
+ unless auth_header
224
+ msg = "Unable to parse authorization header. Headers: #{r.headers}\r\nBody: #{r.body}"
225
+ raise WinRMHTTPTransportError.new(msg, r.status_code)
226
+ end
227
+ itok = auth_header.split.last
228
+ binding = r.peer_cert.nil? ? nil : Net::NTLM::ChannelBinding.create(r.peer_cert)
229
+ auth3 = @ntlmcli.init_context(itok, binding)
230
+ { 'Authorization' => "Negotiate #{auth3.encode64}" }
231
+ end
232
+ end
233
+
234
+ # Uses SSL to secure the transport
235
+ class BasicAuthSSL < HttpTransport
236
+ def initialize(endpoint, user, pass, opts)
237
+ super(endpoint, opts)
238
+ @httpcli.set_auth(endpoint, user, pass)
239
+ basic_auth_only!
240
+ no_ssl_peer_verification! if opts[:no_ssl_peer_verification]
241
+ @ssl_peer_fingerprint = opts[:ssl_peer_fingerprint]
242
+ @httpcli.ssl_config.set_trust_ca(opts[:ca_trust_path]) if opts[:ca_trust_path]
243
+ end
244
+ end
245
+
246
+ # Uses Client Certificate to authenticate and SSL to secure the transport
247
+ class ClientCertAuthSSL < HttpTransport
248
+ def initialize(endpoint, client_cert, client_key, key_pass, opts)
249
+ super(endpoint, opts)
250
+ @httpcli.ssl_config.set_client_cert_file(client_cert, client_key, key_pass)
251
+ @httpcli.www_auth.instance_variable_set('@authenticator', [])
252
+ no_ssl_peer_verification! if opts[:no_ssl_peer_verification]
253
+ @ssl_peer_fingerprint = opts[:ssl_peer_fingerprint]
254
+ @httpcli.ssl_config.set_trust_ca(opts[:ca_trust_path]) if opts[:ca_trust_path]
255
+ end
256
+ end
257
+
258
+ # Uses Kerberos/GSSAPI to authenticate and encrypt messages
259
+ class HttpGSSAPI < HttpTransport
260
+ # @param [String,URI] endpoint the WinRM webservice endpoint
261
+ # @param [String] realm the Kerberos realm we are authenticating to
262
+ # @param [String<optional>] service the service name, default is HTTP
263
+ def initialize(endpoint, realm, opts, service = nil)
264
+ require 'gssapi'
265
+ require 'gssapi/extensions'
266
+
267
+ super(endpoint, opts)
268
+ # Remove the GSSAPI auth from HTTPClient because we are doing our own thing
269
+ no_sspi_auth!
270
+ service ||= 'HTTP'
271
+ @service = "#{service}/#{@endpoint.host}@#{realm}"
272
+ init_krb
273
+ end
274
+
275
+ # Sends the SOAP payload to the WinRM service and returns the service's
276
+ # SOAP response. If an error occurrs an appropriate error is raised.
277
+ #
278
+ # @param [String] The XML SOAP message
279
+ # @returns [REXML::Document] The parsed response body
280
+ def send_request(message)
281
+ resp = send_kerberos_request(message)
282
+
283
+ if resp.status == 401
284
+ @logger.debug 'Got 401 - reinitializing Kerberos and retrying one more time'
285
+ init_krb
286
+ resp = send_kerberos_request(message)
287
+ end
288
+
289
+ handler = WinRM::ResponseHandler.new(winrm_decrypt(resp.http_body.content), resp.status)
290
+ handler.parse_to_xml
291
+ end
292
+
293
+ private
294
+
295
+ # Sends the SOAP payload to the WinRM service and returns the service's
296
+ # HTTP response.
297
+ #
298
+ # @param [String] The XML SOAP message
299
+ # @returns [Object] The HTTP response object
300
+ def send_kerberos_request(message)
301
+ log_soap_message(message)
302
+ original_length = message.bytesize
303
+ pad_len, emsg = winrm_encrypt(message)
304
+ req_length = original_length + pad_len
305
+ hdr = {
306
+ 'Connection' => 'Keep-Alive',
307
+ 'Content-Type' => 'multipart/encrypted;' \
308
+ 'protocol="application/HTTP-Kerberos-session-encrypted";boundary="Encrypted Boundary"'
309
+ }
310
+
311
+ resp = @httpcli.post(
312
+ @endpoint,
313
+ body(emsg, req_length, 'application/HTTP-Kerberos-session-encrypted'),
314
+ hdr
315
+ )
316
+ log_soap_message(resp.http_body.content)
317
+ resp
318
+ end
319
+
320
+ def init_krb
321
+ @logger.debug "Initializing Kerberos for #{@service}"
322
+ @gsscli = GSSAPI::Simple.new(@endpoint.host, @service)
323
+ token = @gsscli.init_context
324
+ auth = Base64.strict_encode64 token
325
+
326
+ hdr = {
327
+ 'Authorization' => "Kerberos #{auth}",
328
+ 'Connection' => 'Keep-Alive',
329
+ 'Content-Type' => 'application/soap+xml;charset=UTF-8'
330
+ }
331
+ @logger.debug 'Sending HTTP POST for Kerberos Authentication'
332
+ r = @httpcli.post(@endpoint, '', hdr)
333
+ itok = r.header['WWW-Authenticate'].pop
334
+ itok = itok.split.last
335
+ itok = Base64.strict_decode64(itok)
336
+ @gsscli.init_context(itok)
337
+ end
338
+
339
+ # @return [String] the encrypted request string
340
+ def winrm_encrypt(str)
341
+ @logger.debug "Encrypting SOAP message:\n#{str}"
342
+ iov = iov_pointer
343
+
344
+ iov0 = create_iov(iov.address, 0, :header)[:buffer]
345
+ iov1 = create_iov(iov.address, 1, :data, str)[:buffer]
346
+ iov2 = create_iov(iov.address, 2, :padding)[:buffer]
347
+
348
+ gss_wrap(iov)
349
+
350
+ token = [iov0.length].pack('L')
351
+ token += iov0.value
352
+ token += iov1.value
353
+ pad_len = iov2.length
354
+ token += iov2.value if pad_len > 0
355
+ [pad_len, token]
356
+ end
357
+
358
+ # @return [String] the unencrypted response string
359
+ def winrm_decrypt(str)
360
+ @logger.debug "Decrypting SOAP message:\n#{str}"
361
+
362
+ str.force_encoding('BINARY')
363
+ str.sub!(%r{^.*Content-Type: application\/octet-stream\r\n(.*)--Encrypted.*$}m, '\1')
364
+ iov_data = str.unpack("LA#{str.unpack('L').first}A*")
365
+
366
+ iov = iov_pointer
367
+
368
+ create_iov(iov.address, 0, :header, iov_data[1])
369
+ ret = create_iov(iov.address, 1, :data, iov_data[2])[:buffer].value
370
+ create_iov(iov.address, 2, :data)
371
+
372
+ maj_stat = gss_unwrap(iov)
373
+
374
+ @logger.debug "SOAP message decrypted (MAJ: #{maj_stat}, " \
375
+ "MIN: #{min_stat.read_int}):\n#{ret}"
376
+
377
+ ret
378
+ end
379
+
380
+ def iov_pointer
381
+ FFI::MemoryPointer.new(GSSAPI::LibGSSAPI::GssIOVBufferDesc.size * 3)
382
+ end
383
+
384
+ def gss_unwrap(iov)
385
+ min_stat = FFI::MemoryPointer.new :uint32
386
+ conf_state = FFI::MemoryPointer.new :uint32
387
+ conf_state.write_int(1)
388
+ qop_state = FFI::MemoryPointer.new :uint32
389
+ qop_state.write_int(0)
390
+
391
+ GSSAPI::LibGSSAPI.gss_unwrap_iov(
392
+ min_stat, @gsscli.context, conf_state, qop_state, iov, 3)
393
+ end
394
+
395
+ def gss_wrap(iov)
396
+ GSSAPI::LibGSSAPI.gss_wrap_iov(
397
+ FFI::MemoryPointer.new(:uint32),
398
+ @gsscli.context,
399
+ 1,
400
+ GSSAPI::LibGSSAPI::GSS_C_QOP_DEFAULT,
401
+ FFI::MemoryPointer.new(:uint32),
402
+ iov,
403
+ 3)
404
+ end
405
+
406
+ def create_iov(address, offset, type, buffer = nil)
407
+ iov = GSSAPI::LibGSSAPI::GssIOVBufferDesc.new(
408
+ FFI::Pointer.new(address + (GSSAPI::LibGSSAPI::GssIOVBufferDesc.size * offset)))
409
+ case type
410
+ when :data
411
+ iov[:type] = GSSAPI::LibGSSAPI::GSS_IOV_BUFFER_TYPE_DATA
412
+ when :header
413
+ iov[:type] = (GSSAPI::LibGSSAPI::GSS_IOV_BUFFER_TYPE_HEADER | \
414
+ GSSAPI::LibGSSAPI::GSS_IOV_BUFFER_FLAG_ALLOCATE)
415
+ when :padding
416
+ iov[:type] = (GSSAPI::LibGSSAPI::GSS_IOV_BUFFER_TYPE_PADDING | \
417
+ GSSAPI::LibGSSAPI::GSS_IOV_BUFFER_FLAG_ALLOCATE)
418
+ end
419
+ iov[:buffer].value = buffer if buffer
420
+ iov
421
+ end
422
+ end
423
+ end
424
+ end # WinRM