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