1# Licensed to the Apache Software Foundation (ASF) under one or more
2# contributor license agreements.  See the NOTICE file distributed with
3# this work for additional information regarding copyright ownership.
4# The ASF licenses this file to You under the Apache License, Version 2.0
5# (the "License"); you may not use this file except in compliance with
6# the License.  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
16from typing import Union
17from typing import Type
18from typing import Optional
19
20import json
21import os
22import ssl
23import socket
24import copy
25import binascii
26import time
27
28from libcloud.utils.py3 import ET
29
30import libcloud
31
32from libcloud.utils.py3 import httplib
33from libcloud.utils.py3 import urlparse
34from libcloud.utils.py3 import urlencode
35
36from libcloud.utils.misc import lowercase_keys
37from libcloud.utils.retry import Retry
38from libcloud.common.exceptions import exception_from_message
39from libcloud.common.types import LibcloudError, MalformedResponseError
40from libcloud.http import LibcloudConnection, HttpLibResponseProxy
41
42__all__ = [
43    'RETRY_FAILED_HTTP_REQUESTS',
44
45    'BaseDriver',
46
47    'Connection',
48    'PollingConnection',
49    'ConnectionKey',
50    'ConnectionUserAndKey',
51    'CertificateConnection',
52
53    'Response',
54    'HTTPResponse',
55    'JsonResponse',
56    'XmlResponse',
57    'RawResponse'
58]
59
60# Module level variable indicates if the failed HTTP requests should be retried
61RETRY_FAILED_HTTP_REQUESTS = False
62
63# Set to True to allow double slashes in the URL path. This way
64# morph_action_hook() won't strip potentially double slashes in the URLs.
65# This is to support scenarios such as this one -
66# https://github.com/apache/libcloud/issues/1529.
67# We default it to False for backward compatibility reasons.
68ALLOW_PATH_DOUBLE_SLASHES = False
69
70
71class LazyObject(object):
72    """An object that doesn't get initialized until accessed."""
73
74    @classmethod
75    def _proxy(cls, *lazy_init_args, **lazy_init_kwargs):
76        class Proxy(cls, object):
77            _lazy_obj = None
78
79            def __init__(self):
80                # Must override the lazy_cls __init__
81                pass
82
83            def __getattribute__(self, attr):
84                lazy_obj = object.__getattribute__(self, '_get_lazy_obj')()
85                return getattr(lazy_obj, attr)
86
87            def __setattr__(self, attr, value):
88                lazy_obj = object.__getattribute__(self, '_get_lazy_obj')()
89                setattr(lazy_obj, attr, value)
90
91            def _get_lazy_obj(self):
92                lazy_obj = object.__getattribute__(self, '_lazy_obj')
93                if lazy_obj is None:
94                    lazy_obj = cls(*lazy_init_args, **lazy_init_kwargs)
95                    object.__setattr__(self, '_lazy_obj', lazy_obj)
96                return lazy_obj
97
98        return Proxy()
99
100    @classmethod
101    def lazy(cls, *lazy_init_args, **lazy_init_kwargs):
102        """Create a lazily instantiated instance of the subclass, cls."""
103        return cls._proxy(*lazy_init_args, **lazy_init_kwargs)
104
105
106class HTTPResponse(httplib.HTTPResponse):
107    # On python 2.6 some calls can hang because HEAD isn't quite properly
108    # supported.
109    # In particular this happens on S3 when calls are made to get_object to
110    # objects that don't exist.
111    # This applies the behaviour from 2.7, fixing the hangs.
112    def read(self, amt=None):
113        if self.fp is None:
114            return ''
115
116        if self._method == 'HEAD':
117            self.close()
118            return ''
119
120        return httplib.HTTPResponse.read(self, amt)
121
122
123class Response(object):
124    """
125    A base Response class to derive from.
126    """
127
128    # Response status code
129    status = httplib.OK  # type: int
130    # Response headers
131    headers = {}  # type: dict
132
133    # Raw response body
134    body = None
135
136    # Parsed response body
137    object = None
138
139    error = None  # Reason returned by the server.
140    connection = None  # Parent connection class
141    parse_zero_length_body = False
142
143    def __init__(self, response, connection):
144        """
145        :param response: HTTP response object. (optional)
146        :type response: :class:`httplib.HTTPResponse`
147
148        :param connection: Parent connection object.
149        :type connection: :class:`.Connection`
150        """
151        self.connection = connection
152
153        # http.client In Python 3 doesn't automatically lowercase the header
154        # names
155        self.headers = lowercase_keys(dict(response.headers))
156        self.error = response.reason
157        self.status = response.status_code
158        self.request = response.request
159        self.iter_content = response.iter_content
160
161        self.body = response.text.strip() \
162            if response.text is not None and hasattr(response.text, 'strip') \
163            else ''
164
165        if not self.success():
166            raise exception_from_message(code=self.status,
167                                         message=self.parse_error(),
168                                         headers=self.headers)
169
170        self.object = self.parse_body()
171
172    def parse_body(self):
173        """
174        Parse response body.
175
176        Override in a provider's subclass.
177
178        :return: Parsed body.
179        :rtype: ``str``
180        """
181        return self.body if self.body is not None else ''
182
183    def parse_error(self):
184        """
185        Parse the error messages.
186
187        Override in a provider's subclass.
188
189        :return: Parsed error.
190        :rtype: ``str``
191        """
192        return self.body
193
194    def success(self):
195        """
196        Determine if our request was successful.
197
198        The meaning of this can be arbitrary; did we receive OK status? Did
199        the node get created? Were we authenticated?
200
201        :rtype: ``bool``
202        :return: ``True`` or ``False``
203        """
204        # pylint: disable=E1101
205        import requests
206        return self.status in [requests.codes.ok, requests.codes.created,
207                               httplib.OK, httplib.CREATED, httplib.ACCEPTED]
208
209
210class JsonResponse(Response):
211    """
212    A Base JSON Response class to derive from.
213    """
214
215    def parse_body(self):
216        if len(self.body) == 0 and not self.parse_zero_length_body:
217            return self.body
218
219        try:
220            body = json.loads(self.body)
221        except Exception:
222            raise MalformedResponseError(
223                'Failed to parse JSON',
224                body=self.body,
225                driver=self.connection.driver)
226        return body
227
228    parse_error = parse_body
229
230
231class XmlResponse(Response):
232    """
233    A Base XML Response class to derive from.
234    """
235
236    def parse_body(self):
237        if len(self.body) == 0 and not self.parse_zero_length_body:
238            return self.body
239
240        try:
241            try:
242                body = ET.XML(self.body)
243            except ValueError:
244                # lxml wants a bytes and tests are basically hard-coded to str
245                body = ET.XML(self.body.encode('utf-8'))
246        except Exception:
247            raise MalformedResponseError('Failed to parse XML',
248                                         body=self.body,
249                                         driver=self.connection.driver)
250        return body
251    parse_error = parse_body
252
253
254class RawResponse(Response):
255    def __init__(self, connection, response=None):
256        """
257        :param connection: Parent connection object.
258        :type connection: :class:`.Connection`
259        """
260        self._status = None
261        self._response = None
262        self._headers = {}
263        self._error = None
264        self._reason = None
265        self.connection = connection
266        if response is not None:
267            self.headers = lowercase_keys(dict(response.headers))
268            self.error = response.reason
269            self.status = response.status_code
270            self.request = response.request
271            self.iter_content = response.iter_content
272
273    def success(self):
274        """
275        Determine if our request was successful.
276
277        The meaning of this can be arbitrary; did we receive OK status? Did
278        the node get created? Were we authenticated?
279
280        :rtype: ``bool``
281        :return: ``True`` or ``False``
282        """
283        # pylint: disable=E1101
284        import requests
285        return self.status in [requests.codes.ok, requests.codes.created,
286                               httplib.OK, httplib.CREATED, httplib.ACCEPTED]
287
288    @property
289    def response(self):
290        if not self._response:
291            response = self.connection.connection.getresponse()
292            self._response = HttpLibResponseProxy(response)
293            if not self.success():
294                self.parse_error()
295        return self._response
296
297    @property
298    def body(self):
299        # Note: We use property to avoid saving whole response body into RAM
300        # See https://github.com/apache/libcloud/pull/1132 for details
301        return self.response.body
302
303    @property
304    def reason(self):
305        if not self._reason:
306            self._reason = self.response.reason
307        return self._reason
308
309
310class Connection(object):
311    """
312    A Base Connection class to derive from.
313    """
314    conn_class = LibcloudConnection
315
316    responseCls = Response
317    rawResponseCls = RawResponse
318    retryCls = Retry
319    connection = None
320    host = '127.0.0.1'  # type: str
321    port = 443
322    timeout = None  # type: Optional[Union[int, float]]
323    secure = 1
324    driver = None  # type:  Type[BaseDriver]
325    action = None
326    cache_busting = False
327    backoff = None
328    retry_delay = None
329
330    allow_insecure = True
331
332    def __init__(self, secure=True, host=None, port=None, url=None,
333                 timeout=None, proxy_url=None, retry_delay=None, backoff=None):
334        self.secure = secure and 1 or 0
335        self.ua = []
336        self.context = {}
337
338        if not self.allow_insecure and not secure:
339            # TODO: We should eventually switch to whitelist instead of
340            # blacklist approach
341            raise ValueError('Non https connections are not allowed (use '
342                             'secure=True)')
343
344        self.request_path = ''
345
346        if host:
347            self.host = host
348
349        if port is not None:
350            self.port = port
351        else:
352            if self.secure == 1:
353                self.port = 443
354            else:
355                self.port = 80
356
357        if url:
358            (self.host, self.port, self.secure,
359             self.request_path) = self._tuple_from_url(url)
360
361        self.timeout = timeout or self.timeout
362        self.retry_delay = retry_delay
363        self.backoff = backoff
364        self.proxy_url = proxy_url
365
366    def set_http_proxy(self, proxy_url):
367        """
368        Set a HTTP / HTTPS proxy which will be used with this connection.
369
370        :param proxy_url: Proxy URL (e.g. http://<hostname>:<port> without
371                          authentication and
372                          <scheme>://<username>:<password>@<hostname>:<port>
373                          for basic auth authentication information.
374        :type proxy_url: ``str``
375        """
376        self.proxy_url = proxy_url
377
378        # NOTE: Because of the way connection instantion works, we need to call
379        # self.connection.set_http_proxy() here. Just setting "self.proxy_url"
380        # won't work.
381        self.connection.set_http_proxy(proxy_url=proxy_url)
382
383    def set_context(self, context):
384        if not isinstance(context, dict):
385            raise TypeError('context needs to be a dictionary')
386
387        self.context = context
388
389    def reset_context(self):
390        self.context = {}
391
392    def _tuple_from_url(self, url):
393        secure = 1
394        port = None
395        (scheme, netloc, request_path, param,
396         query, fragment) = urlparse.urlparse(url)
397
398        if scheme not in ['http', 'https']:
399            raise LibcloudError('Invalid scheme: %s in url %s' % (scheme, url))
400
401        if scheme == "http":
402            secure = 0
403
404        if ":" in netloc:
405            netloc, port = netloc.rsplit(":")
406            port = int(port)
407
408        if not port:
409            if scheme == "http":
410                port = 80
411            else:
412                port = 443
413
414        host = netloc
415        port = int(port)
416
417        return (host, port, secure, request_path)
418
419    def connect(self, host=None, port=None, base_url=None, **kwargs):
420        """
421        Establish a connection with the API server.
422
423        :type host: ``str``
424        :param host: Optional host to override our default
425
426        :type port: ``int``
427        :param port: Optional port to override our default
428
429        :returns: A connection
430        """
431        # prefer the attribute base_url if its set or sent
432        connection = None
433        secure = self.secure
434
435        if getattr(self, 'base_url', None) and base_url is None:
436            (host, port,
437             secure, request_path) = \
438                self._tuple_from_url(getattr(self, 'base_url'))
439        elif base_url is not None:
440            (host, port,
441             secure, request_path) = self._tuple_from_url(base_url)
442        else:
443            host = host or self.host
444            port = port or self.port
445
446        # Make sure port is an int
447        port = int(port)
448
449        if not hasattr(kwargs, 'host'):
450            kwargs.update({'host': host})
451
452        if not hasattr(kwargs, 'port'):
453            kwargs.update({'port': port})
454
455        if not hasattr(kwargs, 'secure'):
456            kwargs.update({'secure': self.secure})
457
458        if not hasattr(kwargs, 'key_file') and hasattr(self, 'key_file'):
459            kwargs.update({'key_file': getattr(self, 'key_file')})
460
461        if not hasattr(kwargs, 'cert_file') and hasattr(self, 'cert_file'):
462            kwargs.update({'cert_file': getattr(self, 'cert_file')})
463
464        if self.timeout:
465            kwargs.update({'timeout': self.timeout})
466
467        if self.proxy_url:
468            kwargs.update({'proxy_url': self.proxy_url})
469
470        connection = self.conn_class(**kwargs)
471        # You can uncoment this line, if you setup a reverse proxy server
472        # which proxies to your endpoint, and lets you easily capture
473        # connections in cleartext when you setup the proxy to do SSL
474        # for you
475        # connection = self.conn_class("127.0.0.1", 8080)
476
477        self.connection = connection
478
479    def _user_agent(self):
480        user_agent_suffix = ' '.join(['(%s)' % x for x in self.ua])
481
482        if self.driver:
483            user_agent = 'libcloud/%s (%s) %s' % (
484                libcloud.__version__,
485                self.driver.name, user_agent_suffix)
486        else:
487            user_agent = 'libcloud/%s %s' % (
488                libcloud.__version__, user_agent_suffix)
489
490        return user_agent
491
492    def user_agent_append(self, token):
493        """
494        Append a token to a user agent string.
495
496        Users of the library should call this to uniquely identify their
497        requests to a provider.
498
499        :type token: ``str``
500        :param token: Token to add to the user agent.
501        """
502        self.ua.append(token)
503
504    def request(self, action, params=None, data=None, headers=None,
505                method='GET', raw=False, stream=False, json=None,
506                retry_failed=None):
507        """
508        Request a given `action`.
509
510        Basically a wrapper around the connection
511        object's `request` that does some helpful pre-processing.
512
513        :type action: ``str``
514        :param action: A path. This can include arguments. If included,
515            any extra parameters are appended to the existing ones.
516
517        :type params: ``dict``
518        :param params: Optional mapping of additional parameters to send. If
519            None, leave as an empty ``dict``.
520
521        :type data: ``unicode``
522        :param data: A body of data to send with the request.
523
524        :type headers: ``dict``
525        :param headers: Extra headers to add to the request
526            None, leave as an empty ``dict``.
527
528        :type method: ``str``
529        :param method: An HTTP method such as "GET" or "POST".
530
531        :type raw: ``bool``
532        :param raw: True to perform a "raw" request aka only send the headers
533                     and use the rawResponseCls class. This is used with
534                     storage API when uploading a file.
535
536        :type stream: ``bool``
537        :param stream: True to return an iterator in Response.iter_content
538                    and allow streaming of the response data
539                    (for downloading large files)
540
541        :param retry_failed: True if failed requests should be retried. This
542                              argument can override module level constant and
543                              environment variable value on per-request basis.
544
545        :return: An :class:`Response` instance.
546        :rtype: :class:`Response` instance
547
548        """
549        if params is None:
550            params = {}
551        else:
552            params = copy.copy(params)
553
554        if headers is None:
555            headers = {}
556        else:
557            headers = copy.copy(headers)
558
559        retry_enabled = os.environ.get('LIBCLOUD_RETRY_FAILED_HTTP_REQUESTS',
560                                       False) or RETRY_FAILED_HTTP_REQUESTS
561
562        # Method level argument has precedence over module level constant and
563        # environment variable
564        if retry_failed is not None:
565            retry_enabled = retry_failed
566
567        action = self.morph_action_hook(action)
568        self.action = action
569        self.method = method
570        self.data = data
571
572        # Extend default parameters
573        params = self.add_default_params(params)
574
575        # Add cache busting parameters (if enabled)
576        if self.cache_busting and method == 'GET':
577            params = self._add_cache_busting_to_params(params=params)
578
579        # Extend default headers
580        headers = self.add_default_headers(headers)
581
582        # We always send a user-agent header
583        headers.update({'User-Agent': self._user_agent()})
584
585        # Indicate that we support gzip and deflate compression
586        headers.update({'Accept-Encoding': 'gzip,deflate'})
587
588        port = int(self.port)
589
590        if port not in (80, 443):
591            headers.update({'Host': "%s:%d" % (self.host, port)})
592        else:
593            headers.update({'Host': self.host})
594
595        if data:
596            data = self.encode_data(data)
597
598        params, headers = self.pre_connect_hook(params, headers)
599
600        if params:
601            if '?' in action:
602                url = '&'.join((action, urlencode(params, doseq=True)))
603            else:
604                url = '?'.join((action, urlencode(params, doseq=True)))
605        else:
606            url = action
607
608        # IF connection has not yet been established
609        if self.connection is None:
610            self.connect()
611
612        try:
613            # @TODO: Should we just pass File object as body to request method
614            # instead of dealing with splitting and sending the file ourselves?
615            if raw:
616                self.connection.prepared_request(
617                    method=method,
618                    url=url,
619                    body=data,
620                    headers=headers,
621                    raw=raw,
622                    stream=stream)
623            else:
624                if retry_enabled:
625                    retry_request = self.retryCls(retry_delay=self.retry_delay,
626                                                  timeout=self.timeout,
627                                                  backoff=self.backoff)
628                    retry_request(self.connection.request)(method=method,
629                                                           url=url,
630                                                           body=data,
631                                                           headers=headers,
632                                                           stream=stream)
633                else:
634                    self.connection.request(method=method, url=url, body=data,
635                                            headers=headers, stream=stream)
636        except socket.gaierror as e:
637            message = str(e)
638            errno = getattr(e, 'errno', None)
639
640            if errno == -5:
641                # Throw a more-friendly exception on "no address associated
642                # with hostname" error. This error could simply indicate that
643                # "host" Connection class attribute is set to an incorrect
644                # value
645                class_name = self.__class__.__name__
646                msg = ('%s. Perhaps "host" Connection class attribute '
647                       '(%s.connection) is set to an invalid, non-hostname '
648                       'value (%s)?' %
649                       (message, class_name, self.host))
650                raise socket.gaierror(msg)
651            self.reset_context()
652            raise e
653        except ssl.SSLError as e:
654            self.reset_context()
655            raise ssl.SSLError(str(e))
656
657        if raw:
658            responseCls = self.rawResponseCls
659            kwargs = {'connection': self,
660                      'response': self.connection.getresponse()}
661        else:
662            responseCls = self.responseCls
663            kwargs = {'connection': self,
664                      'response': self.connection.getresponse()}
665
666        try:
667            response = responseCls(**kwargs)
668        finally:
669            # Always reset the context after the request has completed
670            self.reset_context()
671
672        return response
673
674    def morph_action_hook(self, action):
675        """
676        Here we strip any duplicated leading or traling slashes to
677        prevent typos and other issues where some APIs don't correctly
678        handle double slashes.
679
680        Keep in mind that in some situations, "/" is a vallid path name
681        so we have a module flag which disables this behavior
682        (https://github.com/apache/libcloud/issues/1529).
683        """
684        if ALLOW_PATH_DOUBLE_SLASHES:
685            # Special case to support scenarios where double slashes are
686            # valid - e.g. for S3 paths - /bucket//path1/path2.txt
687            return self.request_path + action
688
689        url = urlparse.urljoin(self.request_path.lstrip('/').rstrip('/') +
690                               '/', action.lstrip('/'))
691
692        if not url.startswith('/'):
693            return '/' + url
694        else:
695            return url
696
697    def add_default_params(self, params):
698        """
699        Adds default parameters (such as API key, version, etc.)
700        to the passed `params`
701
702        Should return a dictionary.
703        """
704        return params
705
706    def add_default_headers(self, headers):
707        """
708        Adds default headers (such as Authorization, X-Foo-Bar)
709        to the passed `headers`
710
711        Should return a dictionary.
712        """
713        return headers
714
715    def pre_connect_hook(self, params, headers):
716        """
717        A hook which is called before connecting to the remote server.
718        This hook can perform a final manipulation on the params, headers and
719        url parameters.
720
721        :type params: ``dict``
722        :param params: Request parameters.
723
724        :type headers: ``dict``
725        :param headers: Request headers.
726        """
727        return params, headers
728
729    def encode_data(self, data):
730        """
731        Encode body data.
732
733        Override in a provider's subclass.
734        """
735        return data
736
737    def _add_cache_busting_to_params(self, params):
738        """
739        Add cache busting parameter to the query parameters of a GET request.
740
741        Parameters are only added if "cache_busting" class attribute is set to
742        True.
743
744        Note: This should only be used with *naughty* providers which use
745        excessive caching of responses.
746        """
747        cache_busting_value = binascii.hexlify(os.urandom(8)).decode('ascii')
748
749        if isinstance(params, dict):
750            params['cache-busting'] = cache_busting_value
751        else:
752            params.append(('cache-busting', cache_busting_value))
753
754        return params
755
756
757class PollingConnection(Connection):
758    """
759    Connection class which can also work with the async APIs.
760
761    After initial requests, this class periodically polls for jobs status and
762    waits until the job has finished.
763    If job doesn't finish in timeout seconds, an Exception thrown.
764    """
765    poll_interval = 0.5
766    timeout = 200
767    request_method = 'request'
768
769    def async_request(self, action, params=None, data=None, headers=None,
770                      method='GET', context=None):
771        """
772        Perform an 'async' request to the specified path. Keep in mind that
773        this function is *blocking* and 'async' in this case means that the
774        hit URL only returns a job ID which is the periodically polled until
775        the job has completed.
776
777        This function works like this:
778
779        - Perform a request to the specified path. Response should contain a
780          'job_id'.
781
782        - Returned 'job_id' is then used to construct a URL which is used for
783          retrieving job status. Constructed URL is then periodically polled
784          until the response indicates that the job has completed or the
785          timeout of 'self.timeout' seconds has been reached.
786
787        :type action: ``str``
788        :param action: A path
789
790        :type params: ``dict``
791        :param params: Optional mapping of additional parameters to send. If
792            None, leave as an empty ``dict``.
793
794        :type data: ``unicode``
795        :param data: A body of data to send with the request.
796
797        :type headers: ``dict``
798        :param headers: Extra headers to add to the request
799            None, leave as an empty ``dict``.
800
801        :type method: ``str``
802        :param method: An HTTP method such as "GET" or "POST".
803
804        :type context: ``dict``
805        :param context: Context dictionary which is passed to the functions
806                        which construct initial and poll URL.
807
808        :return: An :class:`Response` instance.
809        :rtype: :class:`Response` instance
810        """
811
812        request = getattr(self, self.request_method)
813        kwargs = self.get_request_kwargs(action=action, params=params,
814                                         data=data, headers=headers,
815                                         method=method,
816                                         context=context)
817        response = request(**kwargs)
818        kwargs = self.get_poll_request_kwargs(response=response,
819                                              context=context,
820                                              request_kwargs=kwargs)
821
822        end = time.time() + self.timeout
823        completed = False
824        while time.time() < end and not completed:
825            response = request(**kwargs)
826            completed = self.has_completed(response=response)
827            if not completed:
828                time.sleep(self.poll_interval)
829
830        if not completed:
831            raise LibcloudError('Job did not complete in %s seconds' %
832                                (self.timeout))
833
834        return response
835
836    def get_request_kwargs(self, action, params=None, data=None, headers=None,
837                           method='GET', context=None):
838        """
839        Arguments which are passed to the initial request() call inside
840        async_request.
841        """
842        kwargs = {'action': action, 'params': params, 'data': data,
843                  'headers': headers, 'method': method}
844        return kwargs
845
846    def get_poll_request_kwargs(self, response, context, request_kwargs):
847        """
848        Return keyword arguments which are passed to the request() method when
849        polling for the job status.
850
851        :param response: Response object returned by poll request.
852        :type response: :class:`HTTPResponse`
853
854        :param request_kwargs: Kwargs previously used to initiate the
855                                  poll request.
856        :type response: ``dict``
857
858        :return ``dict`` Keyword arguments
859        """
860        raise NotImplementedError('get_poll_request_kwargs not implemented')
861
862    def has_completed(self, response):
863        """
864        Return job completion status.
865
866        :param response: Response object returned by poll request.
867        :type response: :class:`HTTPResponse`
868
869        :return ``bool`` True if the job has completed, False otherwise.
870        """
871        raise NotImplementedError('has_completed not implemented')
872
873
874class ConnectionKey(Connection):
875    """
876    Base connection class which accepts a single ``key`` argument.
877    """
878    def __init__(self, key, secure=True, host=None, port=None, url=None,
879                 timeout=None, proxy_url=None, backoff=None, retry_delay=None):
880        """
881        Initialize `user_id` and `key`; set `secure` to an ``int`` based on
882        passed value.
883        """
884        super(ConnectionKey, self).__init__(secure=secure, host=host,
885                                            port=port, url=url,
886                                            timeout=timeout,
887                                            proxy_url=proxy_url,
888                                            backoff=backoff,
889                                            retry_delay=retry_delay)
890        self.key = key
891
892
893class CertificateConnection(Connection):
894    """
895    Base connection class which accepts a single ``cert_file`` argument.
896    """
897    def __init__(self, cert_file, secure=True, host=None, port=None, url=None,
898                 proxy_url=None, timeout=None, backoff=None, retry_delay=None):
899        """
900        Initialize `cert_file`; set `secure` to an ``int`` based on
901        passed value.
902        """
903        super(CertificateConnection, self).__init__(secure=secure, host=host,
904                                                    port=port, url=url,
905                                                    timeout=timeout,
906                                                    backoff=backoff,
907                                                    retry_delay=retry_delay,
908                                                    proxy_url=proxy_url)
909
910        self.cert_file = cert_file
911
912
913class KeyCertificateConnection(CertificateConnection):
914    """
915    Base connection class which accepts both ``key_file`` and ``cert_file``
916    argument.
917    """
918
919    key_file = None
920
921    def __init__(self, key_file, cert_file, secure=True, host=None, port=None,
922                 url=None, proxy_url=None, timeout=None, backoff=None,
923                 retry_delay=None, ca_cert=None):
924        """
925        Initialize `cert_file`; set `secure` to an ``int`` based on
926        passed value.
927        """
928        super(KeyCertificateConnection, self).__init__(cert_file,
929                                                       secure=secure,
930                                                       host=host,
931                                                       port=port, url=url,
932                                                       timeout=timeout,
933                                                       backoff=backoff,
934                                                       retry_delay=retry_delay,
935                                                       proxy_url=proxy_url)
936
937        self.key_file = key_file
938
939
940class ConnectionUserAndKey(ConnectionKey):
941    """
942    Base connection class which accepts a ``user_id`` and ``key`` argument.
943    """
944
945    user_id = None  # type: int
946
947    def __init__(self, user_id, key, secure=True, host=None, port=None,
948                 url=None, timeout=None, proxy_url=None,
949                 backoff=None, retry_delay=None):
950        super(ConnectionUserAndKey, self).__init__(key, secure=secure,
951                                                   host=host, port=port,
952                                                   url=url, timeout=timeout,
953                                                   backoff=backoff,
954                                                   retry_delay=retry_delay,
955                                                   proxy_url=proxy_url)
956        self.user_id = user_id
957
958
959class BaseDriver(object):
960    """
961    Base driver class from which other classes can inherit from.
962    """
963
964    connectionCls = ConnectionKey  # type: Type[Connection]
965
966    def __init__(self, key, secret=None, secure=True, host=None, port=None,
967                 api_version=None, region=None, **kwargs):
968        """
969        :param    key:    API key or username to be used (required)
970        :type     key:    ``str``
971
972        :param    secret: Secret password to be used (required)
973        :type     secret: ``str``
974
975        :param    secure: Whether to use HTTPS or HTTP. Note: Some providers
976                            only support HTTPS, and it is on by default.
977        :type     secure: ``bool``
978
979        :param    host: Override hostname used for connections.
980        :type     host: ``str``
981
982        :param    port: Override port used for connections.
983        :type     port: ``int``
984
985        :param    api_version: Optional API version. Only used by drivers
986                                 which support multiple API versions.
987        :type     api_version: ``str``
988
989        :param region: Optional driver region. Only used by drivers which
990                       support multiple regions.
991        :type region: ``str``
992
993        :rtype: ``None``
994        """
995
996        self.key = key
997        self.secret = secret
998        self.secure = secure
999        self.api_version = api_version
1000        self.region = region
1001
1002        conn_kwargs = self._ex_connection_class_kwargs()
1003        conn_kwargs.update({'timeout': kwargs.pop('timeout', None),
1004                            'retry_delay': kwargs.pop('retry_delay', None),
1005                            'backoff': kwargs.pop('backoff', None),
1006                            'proxy_url': kwargs.pop('proxy_url', None)})
1007
1008        args = [self.key]
1009
1010        if self.secret is not None:
1011            args.append(self.secret)
1012
1013        args.append(secure)
1014
1015        host = conn_kwargs.pop('host', None) or host
1016
1017        if host is not None:
1018            args.append(host)
1019
1020        if port is not None:
1021            args.append(port)
1022
1023        self.connection = self.connectionCls(*args, **conn_kwargs)
1024        self.connection.driver = self
1025        self.connection.connect()
1026
1027    def _ex_connection_class_kwargs(self):
1028        """
1029        Return extra connection keyword arguments which are passed to the
1030        Connection class constructor.
1031        """
1032        return {}
1033