winrm 1.7.1 → 1.7.2

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