1# Unless explicitly stated otherwise all files in this repository are licensed under the BSD-3-Clause License.
2# This product includes software developed at Datadog (https://www.datadoghq.com/).
3# Copyright 2015-Present Datadog, Inc
4"""
5Available HTTP Client for Datadog API client.
6
7Priority:
81. `requests` 3p module
92. `urlfetch` 3p module - Google App Engine only
10"""
11# stdlib
12import copy
13import logging
14import platform
15import urllib
16from threading import Lock
17
18# 3p
19try:
20    import requests
21    import requests.adapters
22except ImportError:
23    requests = None  # type: ignore
24
25try:
26    from google.appengine.api import urlfetch, urlfetch_errors
27except ImportError:
28    urlfetch, urlfetch_errors = None, None
29
30# datadog
31from datadog.api.exceptions import ProxyError, ClientError, HTTPError, HttpTimeout
32
33
34log = logging.getLogger("datadog.api")
35
36
37def _get_user_agent_header():
38    from datadog import version
39
40    return "datadogpy/{version} (python {pyver}; os {os}; arch {arch})".format(
41        version=version.__version__,
42        pyver=platform.python_version(),
43        os=platform.system().lower(),
44        arch=platform.machine().lower(),
45    )
46
47
48def _remove_context(exc):
49    """Python3: remove context from chained exceptions to prevent leaking API keys in tracebacks."""
50    exc.__cause__ = None
51    return exc
52
53
54class HTTPClient(object):
55    """
56    An abstract generic HTTP client. Subclasses must implement the `request` methods.
57    """
58
59    @classmethod
60    def request(cls, method, url, headers, params, data, timeout, proxies, verify, max_retries):
61        """
62        Main method to be implemented by HTTP clients.
63
64        The returned data structure has the following fields:
65        * `content`: string containing the response from the server
66        * `status_code`: HTTP status code returned by the server
67
68        Can raise the following exceptions:
69        * `ClientError`: server cannot be contacted
70        * `HttpTimeout`: connection timed out
71        * `HTTPError`: unexpected HTTP response code
72        """
73        raise NotImplementedError(u"Must be implemented by HTTPClient subclasses.")
74
75
76class RequestClient(HTTPClient):
77    """
78    HTTP client based on 3rd party `requests` module, using a single session.
79    This allows us to keep the session alive to spare some execution time.
80    """
81
82    _session = None
83    _session_lock = Lock()
84
85    @classmethod
86    def request(cls, method, url, headers, params, data, timeout, proxies, verify, max_retries):
87        try:
88
89            with cls._session_lock:
90                if cls._session is None:
91                    cls._session = requests.Session()
92                    http_adapter = requests.adapters.HTTPAdapter(max_retries=max_retries)
93                    cls._session.mount("https://", http_adapter)
94                    cls._session.headers.update({"User-Agent": _get_user_agent_header()})
95
96            result = cls._session.request(
97                method, url, headers=headers, params=params, data=data, timeout=timeout, proxies=proxies, verify=verify
98            )
99
100            result.raise_for_status()
101
102        except requests.exceptions.ProxyError as e:
103            raise _remove_context(ProxyError(method, url, e))
104        except requests.ConnectionError as e:
105            raise _remove_context(ClientError(method, url, e))
106        except requests.exceptions.Timeout:
107            raise _remove_context(HttpTimeout(method, url, timeout))
108        except requests.exceptions.HTTPError as e:
109            if e.response.status_code in (400, 401, 403, 404, 409, 429):
110                # This gets caught afterwards and raises an ApiError exception
111                pass
112            else:
113                raise _remove_context(HTTPError(e.response.status_code, result.reason))
114        except TypeError:
115            raise TypeError(
116                u"Your installed version of `requests` library seems not compatible with"
117                u"Datadog's usage. We recommand upgrading it ('pip install -U requests')."
118                u"If you need help or have any question, please contact support@datadoghq.com"
119            )
120
121        return result
122
123
124class URLFetchClient(HTTPClient):
125    """
126    HTTP client based on Google App Engine `urlfetch` module.
127    """
128
129    @classmethod
130    def request(cls, method, url, headers, params, data, timeout, proxies, verify, max_retries):
131        """
132        Wrapper around `urlfetch.fetch` method.
133
134        TO IMPLEMENT:
135        * `max_retries`
136        """
137        # No local certificate file can be used on Google App Engine
138        validate_certificate = True if verify else False
139
140        # Encode parameters in the url
141        url_with_params = "{url}?{params}".format(url=url, params=urllib.urlencode(params))
142        newheaders = copy.deepcopy(headers)
143        newheaders["User-Agent"] = _get_user_agent_header()
144
145        try:
146            result = urlfetch.fetch(
147                url=url_with_params,
148                method=method,
149                headers=newheaders,
150                validate_certificate=validate_certificate,
151                deadline=timeout,
152                payload=data,
153                # setting follow_redirects=False may be slightly faster:
154                # https://cloud.google.com/appengine/docs/python/microservice-performance#use_the_shortest_route
155                follow_redirects=False,
156            )
157
158            cls.raise_on_status(result)
159
160        except urlfetch.DownloadError as e:
161            raise ClientError(method, url, e)
162        except urlfetch_errors.DeadlineExceededError:
163            raise HttpTimeout(method, url, timeout)
164
165        return result
166
167    @classmethod
168    def raise_on_status(cls, result):
169        """
170        Raise on HTTP status code errors.
171        """
172        status_code = result.status_code
173
174        if (status_code / 100) != 2:
175            if status_code in (400, 401, 403, 404, 409, 429):
176                pass
177            else:
178                raise HTTPError(status_code)
179
180
181def resolve_http_client():
182    """
183    Resolve an appropriate HTTP client based the defined priority and user environment.
184    """
185    if requests:
186        log.debug(u"Use `requests` based HTTP client.")
187        return RequestClient
188
189    if urlfetch and urlfetch_errors:
190        log.debug(u"Use `urlfetch` based HTTP client.")
191        return URLFetchClient
192
193    raise ImportError(
194        u"Datadog API client was unable to resolve a HTTP client. " u" Please install `requests` library."
195    )
196