winrm 1.7.1 → 1.7.2

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