ovirt-engine-sdk 4.0.1 → 4.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,695 @@
1
+ #
2
+ # Copyright (c) 2015-2017 Red Hat, Inc.
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ #
16
+
17
+ require 'json'
18
+ require 'tempfile'
19
+ require 'uri'
20
+
21
+ module OvirtSDK4
22
+ #
23
+ # This class is responsible for managing an HTTP connection to the engine server. It is intended as the entry
24
+ # point for the SDK, and it provides access to the `system` service and, from there, to the rest of the services
25
+ # provided by the API.
26
+ #
27
+ class Connection
28
+ #
29
+ # Creates a new connection to the API server.
30
+ #
31
+ # [source,ruby]
32
+ # ----
33
+ # connection = OvirtSDK4::Connection.new(
34
+ # url: 'https://engine.example.com/ovirt-engine/api',
35
+ # username: 'admin@internal',
36
+ # password: '...',
37
+ # ca_file:'/etc/pki/ovirt-engine/ca.pem'
38
+ # )
39
+ # ----
40
+ #
41
+ # @param opts [Hash] The options used to create the connection.
42
+ #
43
+ # @option opts [String] :url A string containing the base URL of the server, usually something like
44
+ # `\https://server.example.com/ovirt-engine/api`.
45
+ #
46
+ # @option opts [String] :username The name of the user, something like `admin@internal`.
47
+ #
48
+ # @option opts [String] :password The password of the user.
49
+ #
50
+ # @option opts [String] :token The token used to authenticate. Optionally the caller can explicitly provide
51
+ # the token, instead of the user name and password. If the token isn't provided then it will be automatically
52
+ # created.
53
+ #
54
+ # @option opts [Boolean] :insecure (false) A boolean flag that indicates if the server TLS certificate and host
55
+ # name should be checked.
56
+ #
57
+ # @option opts [String] :ca_file The name of a PEM file containing the trusted CA certificates. The certificate
58
+ # presented by the server will be verified using these CA certificates. If neither this nor the `ca_certs`
59
+ # options are provided, then the system wide CA certificates store is used. If both options are provided,
60
+ # then the certificates from both options will be trusted.
61
+ #
62
+ # @option opts [Array<String>] :ca_certs An array of strings containing the trusted CA certificates, in PEM
63
+ # format. The certificate presented by the server will be verified using these CA certificates. If neither this
64
+ # nor the `ca_file` options are provided, then the system wide CA certificates store is used. If both options
65
+ # are provided, then the certificates from both options will be trusted.
66
+ #
67
+ # @option opts [Boolean] :debug (false) A boolean flag indicating if debug output should be generated. If the
68
+ # values is `true` and the `log` parameter isn't `nil` then the data sent to and received from the server will be
69
+ # written to the log. Be aware that user names and passwords will also be written, so handle with care.
70
+ #
71
+ # @option opts [Logger] :log The logger where the log messages will be written.
72
+ #
73
+ # @option opts [Boolean] :kerberos (false) A boolean flag indicating if Kerberos authentication should be used
74
+ # instead of user name and password to obtain the OAuth token.
75
+ #
76
+ # @option opts [Integer] :timeout (0) The maximun total time to wait for the response, in seconds. A value of zero
77
+ # (the default) means wait for ever. If the timeout expires before the response is received a `TimeoutError`
78
+ # exception will be raised.
79
+ #
80
+ # @option opts [Integer] :connect_timeout (300) The maximun time to wait for connection establishment, in seconds.
81
+ # If the timeout expires before the connection is established a `TimeoutError` exception will be raised.
82
+ #
83
+ # @option opts [Boolean] :compress (true) A boolean flag indicating if the SDK should ask the server to send
84
+ # compressed responses. Note that this is a hint for the server, and that it may return uncompressed data even
85
+ # when this parameter is set to `true`. Also, compression will be automatically disabled when the `debug`
86
+ # parameter is set to `true`, as otherwise the debug output will be compressed as well, and then it isn't
87
+ # useful.
88
+ #
89
+ # @option opts [String] :proxy_url A string containing the protocol, address and port number of the proxy server
90
+ # to use to connect to the server. For example, in order to use the HTTP proxy `proxy.example.com` that is
91
+ # listening on port `3128` the value should be `http://proxy.example.com:3128`. This is optional, and if not
92
+ # given the connection will go directly to the server specified in the `url` parameter.
93
+ #
94
+ # @option opts [String] :proxy_username The name of the user to authenticate to the proxy server.
95
+ #
96
+ # @option opts [String] :proxy_password The password of the user to authenticate to the proxy server.
97
+ #
98
+ # @option opts [Hash] :headers Custom HTTP headers to send with all requests. The keys of the hash can be
99
+ # strings of symbols, and they will be used as the names of the headers. The values of the hash will be used
100
+ # as the names of the headers. If the same header is provided here and in the `headers` parameter of a specific
101
+ # method call, then the `headers` parameter of the specific method call will have precedence.
102
+ #
103
+ # @option opts [Integer] :connections (1) The maximum number of connections to open to the host. The value must
104
+ # be greater than 0.
105
+ #
106
+ # @option opts [Integer] :pipeline (0) The maximum number of request to put in an HTTP pipeline without waiting for
107
+ # the response. If the value is `0` (the default) then pipelining is disabled.
108
+ #
109
+ def initialize(opts = {})
110
+ # Get the values of the parameters and assign default values:
111
+ @url = opts[:url]
112
+ @username = opts[:username]
113
+ @password = opts[:password]
114
+ @token = opts[:token]
115
+ @insecure = opts[:insecure] || false
116
+ @ca_file = opts[:ca_file]
117
+ @ca_certs = opts[:ca_certs]
118
+ @debug = opts[:debug] || false
119
+ @log = opts[:log]
120
+ @kerberos = opts[:kerberos] || false
121
+ @timeout = opts[:timeout] || 0
122
+ @connect_timeout = opts[:connect_timeout] || 0
123
+ @compress = opts[:compress] || true
124
+ @proxy_url = opts[:proxy_url]
125
+ @proxy_username = opts[:proxy_username]
126
+ @proxy_password = opts[:proxy_password]
127
+ @headers = opts[:headers]
128
+ @connections = opts[:connections] || 1
129
+ @pipeline = opts[:pipeline] || 0
130
+
131
+ # Check that the URL has been provided:
132
+ raise ArgumentError, "The 'url' option is mandatory" unless @url
133
+
134
+ # Automatically disable compression when debug is enabled, as otherwise the debug output generated by
135
+ # libcurl is also compressed, and that isn't useful for debugging:
136
+ @compress = false if @debug
137
+
138
+ # Create a temporary file to store the CA certificates, and populate it with the contents of the 'ca_file' and
139
+ # 'ca_certs' options. The file will be removed when the connection is closed.
140
+ @ca_store = nil
141
+ if @ca_file || @ca_certs
142
+ @ca_store = Tempfile.new('ca_store')
143
+ @ca_store.write(::File.read(@ca_file)) if @ca_file
144
+ if @ca_certs
145
+ @ca_certs.each do |ca_cert|
146
+ @ca_store.write(ca_cert)
147
+ end
148
+ end
149
+ @ca_store.close
150
+ end
151
+
152
+ # Create the mutex that will be used to prevents simultaneous access to the same HTTP client by multiple threads:
153
+ @mutex = Mutex.new
154
+
155
+ # Create the HTTP client:
156
+ @client = HttpClient.new(
157
+ insecure: @insecure,
158
+ ca_file: @ca_store ? @ca_store.path : nil,
159
+ debug: @debug,
160
+ log: @log,
161
+ timeout: @timeout,
162
+ connect_timeout: @connect_timeout,
163
+ compress: @compress,
164
+ proxy_url: @proxy_url,
165
+ proxy_username: @proxy_username,
166
+ proxy_password: @proxy_password,
167
+ connections: @connections,
168
+ pipeline: @pipeline
169
+ )
170
+ end
171
+
172
+ #
173
+ # Returns a reference to the root of the services tree.
174
+ #
175
+ # @return [SystemService]
176
+ #
177
+ def system_service
178
+ @system_service ||= SystemService.new(self, '')
179
+ end
180
+
181
+ #
182
+ # Returns a reference to the service corresponding to the given path. For example, if the `path` parameter
183
+ # is `vms/123/diskattachments` then it will return a reference to the service that manages the disk
184
+ # attachments for the virtual machine with identifier `123`.
185
+ #
186
+ # @param path [String] The path of the service, for example `vms/123/diskattachments`.
187
+ # @return [Service]
188
+ # @raise [Error] If there is no service corresponding to the given path.
189
+ #
190
+ def service(path)
191
+ system_service.service(path)
192
+ end
193
+
194
+ #
195
+ # Sends an HTTP request, making sure that multiple threads are coordinated correctly.
196
+ #
197
+ # @param request [HttpRequest] The request object containing the details of the HTTP request to send.
198
+ #
199
+ # @api private
200
+ #
201
+ def send(request)
202
+ @mutex.synchronize { internal_send(request) }
203
+ end
204
+
205
+ #
206
+ # Waits for the response to the given request, making sure that multiple threads are coordinated correctly.
207
+ #
208
+ # @param request [HttpRequest] The request object whose corresponding response you want to wait for.
209
+ # @return [HttpResponse] A request object containing the details of the HTTP response received.
210
+ #
211
+ # @api private
212
+ #
213
+ def wait(request)
214
+ @mutex.synchronize { internal_wait(request) }
215
+ end
216
+
217
+ #
218
+ # Tests the connectivity with the server. If connectivity works correctly it returns `true`. If there is any
219
+ # connectivity problem it will either return `false` or raise an exception if the `raise_exception` parameter is
220
+ # `true`.
221
+ #
222
+ # @param raise_exception [Boolean]
223
+ #
224
+ # @param timeout [Integer] (nil) The maximun total time to wait for the test to complete, in seconds. If the value
225
+ # is `nil` (the default) then the timeout set globally for the connection will be used.
226
+ #
227
+ # @return [Boolean]
228
+ #
229
+ def test(raise_exception = false, timeout = nil)
230
+ system_service.get(timeout: timeout)
231
+ true
232
+ rescue StandardError
233
+ raise if raise_exception
234
+
235
+ false
236
+ end
237
+
238
+ #
239
+ # Performs the authentication process and returns the authentication token. Usually there is no need to
240
+ # call this method, as authentication is performed automatically when needed. But in some situations it
241
+ # may be useful to perform authentication explicitly, and then use the obtained token to create other
242
+ # connections, using the `token` parameter of the constructor instead of the user name and password.
243
+ #
244
+ # @return [String]
245
+ #
246
+ def authenticate
247
+ # rubocop:disable Naming/MemoizedInstanceVariableName
248
+ @token ||= create_access_token
249
+ # rubocop:enable Naming/MemoizedInstanceVariableName
250
+ end
251
+
252
+ #
253
+ # Indicates if the given object is a link. An object is a link if it has an `href` attribute.
254
+ #
255
+ # @return [Boolean]
256
+ #
257
+ def link?(object)
258
+ !object.href.nil?
259
+ end
260
+
261
+ #
262
+ # The `link?` method used to be named `is_link?`, and we need to preserve it for backwards compatibility, but try to
263
+ # avoid using it.
264
+ #
265
+ # @return [Boolean]
266
+ #
267
+ # @deprecated Please use `link?` instead.
268
+ #
269
+ alias is_link? link?
270
+
271
+ #
272
+ # Follows the `href` attribute of the given object, retrieves the target object and returns it.
273
+ #
274
+ # @param object [Type] The object containing the `href` attribute.
275
+ # @raise [Error] If the `href` attribute has no value, or the link can't be followed.
276
+ #
277
+ def follow_link(object)
278
+ # Check that the "href" has a value, as it is needed in order to retrieve the representation of the object:
279
+ href = object.href
280
+ raise Error, "Can't follow link because the 'href' attribute doesn't have a value" if href.nil?
281
+
282
+ # Check that the value of the "href" attribute is compatible with the base URL of the connection:
283
+ prefix = URI(@url).path
284
+ prefix += '/' unless prefix.end_with?('/')
285
+ unless href.start_with?(prefix)
286
+ raise Error, "The URL '#{href}' isn't compatible with the base URL of the connection"
287
+ end
288
+
289
+ # Remove the prefix from the URL, follow the path to the relevant service and invoke the "get" or "list" method
290
+ # to retrieve its representation:
291
+ path = href[prefix.length..-1]
292
+ service = service(path)
293
+ if object.is_a?(Array)
294
+ service.list
295
+ else
296
+ service.get
297
+ end
298
+ end
299
+
300
+ #
301
+ # Releases the resources used by this connection, making sure that multiple threads are coordinated correctly.
302
+ #
303
+ def close
304
+ @mutex.synchronize { internal_close }
305
+ end
306
+
307
+ #
308
+ # Checks that the content type of the given response is JSON. If it is JSON then it does nothing. If it isn't
309
+ # JSON then it raises an exception.
310
+ #
311
+ # @param response [HttpResponse] The HTTP response to check.
312
+ #
313
+ # @api private
314
+ #
315
+ def check_json_content_type(response)
316
+ check_content_type(JSON_CONTENT_TYPE_RE, 'JSON', response)
317
+ end
318
+
319
+ #
320
+ # Checks that the content type of the given response is XML. If it is XML then it does nothing. If it isn't
321
+ # XML then it raises an exception.
322
+ #
323
+ # @param response [HttpResponse] The HTTP response to check.
324
+ #
325
+ # @api private
326
+ #
327
+ def check_xml_content_type(response)
328
+ check_content_type(XML_CONTENT_TYPE_RE, 'XML', response)
329
+ end
330
+
331
+ #
332
+ # Creates and raises an error containing the details of the given HTTP response.
333
+ #
334
+ # @param response [HttpResponse] The HTTP response where the details of the raised error will be taken from.
335
+ # @param detail [String, Fault] (nil) The detail of the error. It can be a string or a `Fault` object.
336
+ #
337
+ # @api private
338
+ #
339
+ def raise_error(response, detail = nil)
340
+ # Check if the detail is a fault:
341
+ fault = detail.is_a?(Fault) ? detail : nil
342
+
343
+ # Build the error message from the response and the fault:
344
+ message = ''
345
+ unless fault.nil?
346
+ unless fault.reason.nil?
347
+ message << ' ' unless message.empty?
348
+ message << "Fault reason is \"#{fault.reason}\"."
349
+ end
350
+ unless fault.detail.nil?
351
+ message << ' ' unless message.empty?
352
+ message << "Fault detail is \"#{fault.detail}\"."
353
+ end
354
+ end
355
+ unless response.nil?
356
+ unless response.code.nil?
357
+ message << ' ' unless message.empty?
358
+ message << "HTTP response code is #{response.code}."
359
+ end
360
+ unless response.message.nil?
361
+ message << ' ' unless message.empty?
362
+ message << "HTTP response message is \"#{response.message}\"."
363
+ end
364
+ end
365
+
366
+ # If the detail is a string, append it to the message:
367
+ if detail.is_a?(String)
368
+ message << ' ' unless message.empty?
369
+ message << detail
370
+ message << '.'
371
+ end
372
+
373
+ # Create and populate the error:
374
+ klass = Error
375
+ unless response.nil?
376
+ case response.code
377
+ when 401, 403
378
+ klass = AuthError
379
+ when 404
380
+ klass = NotFoundError
381
+ end
382
+ end
383
+ error = klass.new(message)
384
+ error.code = response.code if response
385
+ error.fault = fault
386
+
387
+ raise error
388
+ end
389
+
390
+ #
391
+ # Returns a string representation of the connection.
392
+ #
393
+ # @return [String] The string representation.
394
+ #
395
+ def inspect
396
+ "#<#{self.class.name}:#{@url}>"
397
+ end
398
+
399
+ #
400
+ # Returns a string representation of the connection.
401
+ #
402
+ # @return [String] The string representation.
403
+ #
404
+ def to_s
405
+ inspect
406
+ end
407
+
408
+ #
409
+ # Returns a string representation of the connection.
410
+ #
411
+ # @return [String] The string representation.
412
+ #
413
+
414
+ private
415
+
416
+ #
417
+ # Regular expression used to check JSON content type.
418
+ #
419
+ # @api private
420
+ #
421
+ JSON_CONTENT_TYPE_RE = %r{^\s*(application|text)/json\s*(;.*)?$}i.freeze
422
+
423
+ #
424
+ # Regular expression used to check XML content type.
425
+ #
426
+ # @api private
427
+ #
428
+ XML_CONTENT_TYPE_RE = %r{^\s*(application|text)/xml\s*(;.*)?$}i.freeze
429
+
430
+ #
431
+ # The typical URL path, used just to generate informative error messages.
432
+ #
433
+ # @api private
434
+ #
435
+ TYPICAL_PATH = '/ovirt-engine/api'.freeze
436
+
437
+ #
438
+ # Checks the content type of the given HTTP response and raises an exception if it isn't the expected one.
439
+ #
440
+ # @param expected_re [Regex] The regular expression used to check the expected content type.
441
+ # @param expected_name [String] The name of the expected content type.
442
+ # @param response [HttpResponse] The HTTP response to check.
443
+ #
444
+ # @api private
445
+ #
446
+ def check_content_type(expected_re, expected_name, response)
447
+ content_type = response.headers['content-type']
448
+ return if expected_re =~ content_type
449
+
450
+ detail = "The response content type '#{content_type}' isn't #{expected_name}"
451
+ url = URI(@url)
452
+ if url.path != TYPICAL_PATH
453
+ detail << ". Is the path '#{url.path}' included in the 'url' parameter correct?"
454
+ detail << " The typical one is '#{TYPICAL_PATH}'"
455
+ end
456
+ raise_error(response, detail)
457
+ end
458
+
459
+ #
460
+ # Obtains the access token from SSO to be used for bearer authentication.
461
+ #
462
+ # @return [String] The access token.
463
+ #
464
+ # @api private
465
+ #
466
+ def create_access_token
467
+ # Build the URL and parameters required for the request:
468
+ url, parameters = build_sso_auth_request
469
+
470
+ # Send the request and wait for the request:
471
+ response = get_sso_response(url, parameters)
472
+ response = response[0] if response.is_a?(Array)
473
+
474
+ # Check the response and raise an error if it contains an error code:
475
+ error = get_sso_error_message(response)
476
+ raise AuthError, "Error during SSO authentication: #{error}" if error
477
+
478
+ response['access_token']
479
+ end
480
+
481
+ #
482
+ # Revoke the SSO access token.
483
+ #
484
+ # @api private
485
+ #
486
+ def revoke_access_token
487
+ # Build the URL and parameters required for the request:
488
+ url, parameters = build_sso_revoke_request
489
+
490
+ # Send the request and wait for the response:
491
+ response = get_sso_response(url, parameters)
492
+ response = response[0] if response.is_a?(Array)
493
+
494
+ # Check the response and raise an error if it contains an error code:
495
+ error = get_sso_error_message(response)
496
+ raise AuthError, "Error during SSO revoke: #{error}" if error
497
+ end
498
+
499
+ #
500
+ # Execute a get request to the SSO server and return the response.
501
+ #
502
+ # @param url [String] The URL of the SSO server.
503
+ #
504
+ # @param parameters [Hash] The parameters to send to the SSO server.
505
+ #
506
+ # @return [Hash] The JSON response.
507
+ #
508
+ # @api private
509
+ #
510
+ def get_sso_response(url, parameters)
511
+ # Create the request:
512
+ request = HttpRequest.new
513
+ request.method = :POST
514
+ request.url = url
515
+ request.headers = {
516
+ 'User-Agent' => "RubySDK/#{VERSION}",
517
+ 'Content-Type' => 'application/x-www-form-urlencoded',
518
+ 'Accept' => 'application/json'
519
+ }
520
+ request.body = URI.encode_www_form(parameters)
521
+
522
+ # Add the global headers:
523
+ request.headers.merge!(@headers) if @headers
524
+
525
+ # Send the request and wait for the response:
526
+ @client.send(request)
527
+ response = @client.wait(request)
528
+ raise response if response.is_a?(Exception)
529
+
530
+ # Check the returned content type:
531
+ check_json_content_type(response)
532
+
533
+ # Parse and return the JSON response:
534
+ JSON.parse(response.body)
535
+ end
536
+
537
+ #
538
+ # Builds a the URL and parameters to acquire the access token from SSO.
539
+ #
540
+ # @return [Array] An array containing two elements, the first is the URL of the SSO service and the second is a hash
541
+ # containing the parameters required to perform authentication.
542
+ #
543
+ # @api private
544
+ #
545
+ def build_sso_auth_request
546
+ # Compute the entry point and the parameters:
547
+ parameters = {
548
+ scope: 'ovirt-app-api'
549
+ }
550
+ if @kerberos
551
+ entry_point = 'token-http-auth'
552
+ parameters[:grant_type] = 'urn:ovirt:params:oauth:grant-type:http'
553
+ else
554
+ entry_point = 'token'
555
+ parameters.merge!(
556
+ grant_type: 'password',
557
+ username: @username,
558
+ password: @password
559
+ )
560
+ end
561
+
562
+ # Compute the URL:
563
+ url = URI(@url.to_s)
564
+ url.path = "/ovirt-engine/sso/oauth/#{entry_point}"
565
+ url = url.to_s
566
+
567
+ # Return the pair containing the URL and the parameters:
568
+ [url, parameters]
569
+ end
570
+
571
+ #
572
+ # Builds a the URL and parameters to revoke the SSO access token
573
+ #
574
+ # @return [Array] An array containing two elements, the first is the URL of the SSO service and the second is a hash
575
+ # containing the parameters required to perform the revoke.
576
+ #
577
+ # @api private
578
+ #
579
+ def build_sso_revoke_request
580
+ # Compute the parameters:
581
+ parameters = {
582
+ scope: '',
583
+ token: @token
584
+ }
585
+
586
+ # Compute the URL:
587
+ url = URI(@url.to_s)
588
+ url.path = '/ovirt-engine/services/sso-logout'
589
+ url = url.to_s
590
+
591
+ # Return the pair containing the URL and the parameters:
592
+ [url, parameters]
593
+ end
594
+
595
+ #
596
+ # Extrats the error message from the given SSO response.
597
+ #
598
+ # @param response [Hash] The result of parsing the JSON document returned by the SSO server.
599
+ # @return [String] The error message, or `nil` if there was no error.
600
+ #
601
+ def get_sso_error_message(response)
602
+ # OAuth uses the 'error_code' attribute for the error code, and 'error' for the error description. But OpenID uses
603
+ # 'error' for the error code and 'error_description' for the description. So we need to check if the
604
+ # 'error_description' attribute is present, and extract the code and description accordingly.
605
+ description = response['error_description']
606
+ if description.nil?
607
+ code = response['error_code']
608
+ description = response['error']
609
+ else
610
+ code = response['error']
611
+ end
612
+ "#{code}: #{description}" if code
613
+ end
614
+
615
+ #
616
+ # Sends an HTTP request.
617
+ #
618
+ # @param request [HttpRequest] The request object containing the details of the HTTP request to send.
619
+ #
620
+ # @api private
621
+ #
622
+ def internal_send(request)
623
+ # Add the base URL to the request:
624
+ request.url = request.url.nil? ? request.url = @url : "#{@url}/#{request.url}"
625
+
626
+ # Set the headers common to all requests:
627
+ request.headers.merge!(
628
+ 'User-Agent' => "RubySDK/#{VERSION}",
629
+ 'Version' => '4',
630
+ 'Content-Type' => 'application/xml',
631
+ 'Accept' => 'application/xml'
632
+ )
633
+
634
+ # Older versions of the engine (before 4.1) required the 'all_content' as an HTTP header instead of a query
635
+ # parameter. In order to better support those older versions of the engine we need to check if this parameter is
636
+ # included in the request, and add the corresponding header.
637
+ unless request.query.nil?
638
+ all_content = request.query[:all_content]
639
+ request.headers['All-Content'] = all_content unless all_content.nil?
640
+ end
641
+
642
+ # Add the global headers, but without replacing the values that may already exist:
643
+ request.headers.merge!(@headers) { |_name, local, _global| local } if @headers
644
+
645
+ # Set the authentication token:
646
+ @token ||= create_access_token
647
+ request.token = @token
648
+
649
+ # Send the request:
650
+ @client.send(request)
651
+ end
652
+
653
+ #
654
+ # Waits for the response to the given request.
655
+ #
656
+ # @param request [HttpRequest] The request object whose corresponding response you want to wait for.
657
+ # @return [Response] A request object containing the details of the HTTP response received.
658
+ #
659
+ # @api private
660
+ #
661
+ def internal_wait(request)
662
+ # Wait for the response:
663
+ response = @client.wait(request)
664
+ raise response if response.is_a?(Exception)
665
+
666
+ # If the request failed because of authentication, and it wasn't a request to the SSO service, then the
667
+ # most likely cause is an expired SSO token. In this case we need to request a new token, and try the original
668
+ # request again, but only once. It if fails again, we just return the failed response.
669
+ if response.code == 401 && request.token
670
+ @token = create_access_token
671
+ request.token = @token
672
+ @client.send(request)
673
+ response = @client.wait(request)
674
+ end
675
+
676
+ response
677
+ end
678
+
679
+ #
680
+ # Releases the resources used by this connection.
681
+ #
682
+ # @api private
683
+ #
684
+ def internal_close
685
+ # Revoke the SSO access token:
686
+ revoke_access_token if @token
687
+
688
+ # Close the HTTP client:
689
+ @client.close if @client
690
+
691
+ # Remove the temporary file that contains the trusted CA certificates:
692
+ @ca_store.unlink if @ca_store
693
+ end
694
+ end
695
+ end