winrm 1.8.1 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
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