urllib3.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439
  1. # Copyright 2016 Google LLC
  2. #
  3. # Licensed under the Apache License, Version 2.0 (the "License");
  4. # you may not use this file except in compliance with the License.
  5. # You may obtain a copy of the License at
  6. #
  7. # http://www.apache.org/licenses/LICENSE-2.0
  8. #
  9. # Unless required by applicable law or agreed to in writing, software
  10. # distributed under the License is distributed on an "AS IS" BASIS,
  11. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. # See the License for the specific language governing permissions and
  13. # limitations under the License.
  14. """Transport adapter for urllib3."""
  15. from __future__ import absolute_import
  16. import logging
  17. import os
  18. import warnings
  19. # Certifi is Mozilla's certificate bundle. Urllib3 needs a certificate bundle
  20. # to verify HTTPS requests, and certifi is the recommended and most reliable
  21. # way to get a root certificate bundle. See
  22. # http://urllib3.readthedocs.io/en/latest/user-guide.html\
  23. # #certificate-verification
  24. # For more details.
  25. try:
  26. import certifi
  27. except ImportError: # pragma: NO COVER
  28. certifi = None
  29. try:
  30. import urllib3
  31. except ImportError as caught_exc: # pragma: NO COVER
  32. import six
  33. six.raise_from(
  34. ImportError(
  35. "The urllib3 library is not installed, please install the "
  36. "urllib3 package to use the urllib3 transport."
  37. ),
  38. caught_exc,
  39. )
  40. import six
  41. import urllib3.exceptions # pylint: disable=ungrouped-imports
  42. from google.auth import environment_vars
  43. from google.auth import exceptions
  44. from google.auth import transport
  45. from google.oauth2 import service_account
  46. _LOGGER = logging.getLogger(__name__)
  47. class _Response(transport.Response):
  48. """urllib3 transport response adapter.
  49. Args:
  50. response (urllib3.response.HTTPResponse): The raw urllib3 response.
  51. """
  52. def __init__(self, response):
  53. self._response = response
  54. @property
  55. def status(self):
  56. return self._response.status
  57. @property
  58. def headers(self):
  59. return self._response.headers
  60. @property
  61. def data(self):
  62. return self._response.data
  63. class Request(transport.Request):
  64. """urllib3 request adapter.
  65. This class is used internally for making requests using various transports
  66. in a consistent way. If you use :class:`AuthorizedHttp` you do not need
  67. to construct or use this class directly.
  68. This class can be useful if you want to manually refresh a
  69. :class:`~google.auth.credentials.Credentials` instance::
  70. import google.auth.transport.urllib3
  71. import urllib3
  72. http = urllib3.PoolManager()
  73. request = google.auth.transport.urllib3.Request(http)
  74. credentials.refresh(request)
  75. Args:
  76. http (urllib3.request.RequestMethods): An instance of any urllib3
  77. class that implements :class:`~urllib3.request.RequestMethods`,
  78. usually :class:`urllib3.PoolManager`.
  79. .. automethod:: __call__
  80. """
  81. def __init__(self, http):
  82. self.http = http
  83. def __call__(
  84. self, url, method="GET", body=None, headers=None, timeout=None, **kwargs
  85. ):
  86. """Make an HTTP request using urllib3.
  87. Args:
  88. url (str): The URI to be requested.
  89. method (str): The HTTP method to use for the request. Defaults
  90. to 'GET'.
  91. body (bytes): The payload / body in HTTP request.
  92. headers (Mapping[str, str]): Request headers.
  93. timeout (Optional[int]): The number of seconds to wait for a
  94. response from the server. If not specified or if None, the
  95. urllib3 default timeout will be used.
  96. kwargs: Additional arguments passed throught to the underlying
  97. urllib3 :meth:`urlopen` method.
  98. Returns:
  99. google.auth.transport.Response: The HTTP response.
  100. Raises:
  101. google.auth.exceptions.TransportError: If any exception occurred.
  102. """
  103. # urllib3 uses a sentinel default value for timeout, so only set it if
  104. # specified.
  105. if timeout is not None:
  106. kwargs["timeout"] = timeout
  107. try:
  108. _LOGGER.debug("Making request: %s %s", method, url)
  109. response = self.http.request(
  110. method, url, body=body, headers=headers, **kwargs
  111. )
  112. return _Response(response)
  113. except urllib3.exceptions.HTTPError as caught_exc:
  114. new_exc = exceptions.TransportError(caught_exc)
  115. six.raise_from(new_exc, caught_exc)
  116. def _make_default_http():
  117. if certifi is not None:
  118. return urllib3.PoolManager(cert_reqs="CERT_REQUIRED", ca_certs=certifi.where())
  119. else:
  120. return urllib3.PoolManager()
  121. def _make_mutual_tls_http(cert, key):
  122. """Create a mutual TLS HTTP connection with the given client cert and key.
  123. See https://github.com/urllib3/urllib3/issues/474#issuecomment-253168415
  124. Args:
  125. cert (bytes): client certificate in PEM format
  126. key (bytes): client private key in PEM format
  127. Returns:
  128. urllib3.PoolManager: Mutual TLS HTTP connection.
  129. Raises:
  130. ImportError: If certifi or pyOpenSSL is not installed.
  131. OpenSSL.crypto.Error: If the cert or key is invalid.
  132. """
  133. import certifi
  134. from OpenSSL import crypto
  135. import urllib3.contrib.pyopenssl
  136. urllib3.contrib.pyopenssl.inject_into_urllib3()
  137. ctx = urllib3.util.ssl_.create_urllib3_context()
  138. ctx.load_verify_locations(cafile=certifi.where())
  139. pkey = crypto.load_privatekey(crypto.FILETYPE_PEM, key)
  140. x509 = crypto.load_certificate(crypto.FILETYPE_PEM, cert)
  141. ctx._ctx.use_certificate(x509)
  142. ctx._ctx.use_privatekey(pkey)
  143. http = urllib3.PoolManager(ssl_context=ctx)
  144. return http
  145. class AuthorizedHttp(urllib3.request.RequestMethods):
  146. """A urllib3 HTTP class with credentials.
  147. This class is used to perform requests to API endpoints that require
  148. authorization::
  149. from google.auth.transport.urllib3 import AuthorizedHttp
  150. authed_http = AuthorizedHttp(credentials)
  151. response = authed_http.request(
  152. 'GET', 'https://www.googleapis.com/storage/v1/b')
  153. This class implements :class:`urllib3.request.RequestMethods` and can be
  154. used just like any other :class:`urllib3.PoolManager`.
  155. The underlying :meth:`urlopen` implementation handles adding the
  156. credentials' headers to the request and refreshing credentials as needed.
  157. This class also supports mutual TLS via :meth:`configure_mtls_channel`
  158. method. In order to use this method, the `GOOGLE_API_USE_CLIENT_CERTIFICATE`
  159. environment variable must be explicitly set to `true`, otherwise it does
  160. nothing. Assume the environment is set to `true`, the method behaves in the
  161. following manner:
  162. If client_cert_callback is provided, client certificate and private
  163. key are loaded using the callback; if client_cert_callback is None,
  164. application default SSL credentials will be used. Exceptions are raised if
  165. there are problems with the certificate, private key, or the loading process,
  166. so it should be called within a try/except block.
  167. First we set the environment variable to `true`, then create an :class:`AuthorizedHttp`
  168. instance and specify the endpoints::
  169. regular_endpoint = 'https://pubsub.googleapis.com/v1/projects/{my_project_id}/topics'
  170. mtls_endpoint = 'https://pubsub.mtls.googleapis.com/v1/projects/{my_project_id}/topics'
  171. authed_http = AuthorizedHttp(credentials)
  172. Now we can pass a callback to :meth:`configure_mtls_channel`::
  173. def my_cert_callback():
  174. # some code to load client cert bytes and private key bytes, both in
  175. # PEM format.
  176. some_code_to_load_client_cert_and_key()
  177. if loaded:
  178. return cert, key
  179. raise MyClientCertFailureException()
  180. # Always call configure_mtls_channel within a try/except block.
  181. try:
  182. is_mtls = authed_http.configure_mtls_channel(my_cert_callback)
  183. except:
  184. # handle exceptions.
  185. if is_mtls:
  186. response = authed_http.request('GET', mtls_endpoint)
  187. else:
  188. response = authed_http.request('GET', regular_endpoint)
  189. You can alternatively use application default SSL credentials like this::
  190. try:
  191. is_mtls = authed_http.configure_mtls_channel()
  192. except:
  193. # handle exceptions.
  194. Args:
  195. credentials (google.auth.credentials.Credentials): The credentials to
  196. add to the request.
  197. http (urllib3.PoolManager): The underlying HTTP object to
  198. use to make requests. If not specified, a
  199. :class:`urllib3.PoolManager` instance will be constructed with
  200. sane defaults.
  201. refresh_status_codes (Sequence[int]): Which HTTP status codes indicate
  202. that credentials should be refreshed and the request should be
  203. retried.
  204. max_refresh_attempts (int): The maximum number of times to attempt to
  205. refresh the credentials and retry the request.
  206. default_host (Optional[str]): A host like "pubsub.googleapis.com".
  207. This is used when a self-signed JWT is created from service
  208. account credentials.
  209. """
  210. def __init__(
  211. self,
  212. credentials,
  213. http=None,
  214. refresh_status_codes=transport.DEFAULT_REFRESH_STATUS_CODES,
  215. max_refresh_attempts=transport.DEFAULT_MAX_REFRESH_ATTEMPTS,
  216. default_host=None,
  217. ):
  218. if http is None:
  219. self.http = _make_default_http()
  220. self._has_user_provided_http = False
  221. else:
  222. self.http = http
  223. self._has_user_provided_http = True
  224. self.credentials = credentials
  225. self._refresh_status_codes = refresh_status_codes
  226. self._max_refresh_attempts = max_refresh_attempts
  227. self._default_host = default_host
  228. # Request instance used by internal methods (for example,
  229. # credentials.refresh).
  230. self._request = Request(self.http)
  231. # https://google.aip.dev/auth/4111
  232. # Attempt to use self-signed JWTs when a service account is used.
  233. if isinstance(self.credentials, service_account.Credentials):
  234. self.credentials._create_self_signed_jwt(
  235. "https://{}/".format(self._default_host) if self._default_host else None
  236. )
  237. super(AuthorizedHttp, self).__init__()
  238. def configure_mtls_channel(self, client_cert_callback=None):
  239. """Configures mutual TLS channel using the given client_cert_callback or
  240. application default SSL credentials. The behavior is controlled by
  241. `GOOGLE_API_USE_CLIENT_CERTIFICATE` environment variable.
  242. (1) If the environment variable value is `true`, the function returns True
  243. if the channel is mutual TLS and False otherwise. The `http` provided
  244. in the constructor will be overwritten.
  245. (2) If the environment variable is not set or `false`, the function does
  246. nothing and it always return False.
  247. Args:
  248. client_cert_callback (Optional[Callable[[], (bytes, bytes)]]):
  249. The optional callback returns the client certificate and private
  250. key bytes both in PEM format.
  251. If the callback is None, application default SSL credentials
  252. will be used.
  253. Returns:
  254. True if the channel is mutual TLS and False otherwise.
  255. Raises:
  256. google.auth.exceptions.MutualTLSChannelError: If mutual TLS channel
  257. creation failed for any reason.
  258. """
  259. use_client_cert = os.getenv(
  260. environment_vars.GOOGLE_API_USE_CLIENT_CERTIFICATE, "false"
  261. )
  262. if use_client_cert != "true":
  263. return False
  264. try:
  265. import OpenSSL
  266. except ImportError as caught_exc:
  267. new_exc = exceptions.MutualTLSChannelError(caught_exc)
  268. six.raise_from(new_exc, caught_exc)
  269. try:
  270. found_cert_key, cert, key = transport._mtls_helper.get_client_cert_and_key(
  271. client_cert_callback
  272. )
  273. if found_cert_key:
  274. self.http = _make_mutual_tls_http(cert, key)
  275. else:
  276. self.http = _make_default_http()
  277. except (
  278. exceptions.ClientCertError,
  279. ImportError,
  280. OpenSSL.crypto.Error,
  281. ) as caught_exc:
  282. new_exc = exceptions.MutualTLSChannelError(caught_exc)
  283. six.raise_from(new_exc, caught_exc)
  284. if self._has_user_provided_http:
  285. self._has_user_provided_http = False
  286. warnings.warn(
  287. "`http` provided in the constructor is overwritten", UserWarning
  288. )
  289. return found_cert_key
  290. def urlopen(self, method, url, body=None, headers=None, **kwargs):
  291. """Implementation of urllib3's urlopen."""
  292. # pylint: disable=arguments-differ
  293. # We use kwargs to collect additional args that we don't need to
  294. # introspect here. However, we do explicitly collect the two
  295. # positional arguments.
  296. # Use a kwarg for this instead of an attribute to maintain
  297. # thread-safety.
  298. _credential_refresh_attempt = kwargs.pop("_credential_refresh_attempt", 0)
  299. if headers is None:
  300. headers = self.headers
  301. # Make a copy of the headers. They will be modified by the credentials
  302. # and we want to pass the original headers if we recurse.
  303. request_headers = headers.copy()
  304. self.credentials.before_request(self._request, method, url, request_headers)
  305. response = self.http.urlopen(
  306. method, url, body=body, headers=request_headers, **kwargs
  307. )
  308. # If the response indicated that the credentials needed to be
  309. # refreshed, then refresh the credentials and re-attempt the
  310. # request.
  311. # A stored token may expire between the time it is retrieved and
  312. # the time the request is made, so we may need to try twice.
  313. # The reason urllib3's retries aren't used is because they
  314. # don't allow you to modify the request headers. :/
  315. if (
  316. response.status in self._refresh_status_codes
  317. and _credential_refresh_attempt < self._max_refresh_attempts
  318. ):
  319. _LOGGER.info(
  320. "Refreshing credentials due to a %s response. Attempt %s/%s.",
  321. response.status,
  322. _credential_refresh_attempt + 1,
  323. self._max_refresh_attempts,
  324. )
  325. self.credentials.refresh(self._request)
  326. # Recurse. Pass in the original headers, not our modified set.
  327. return self.urlopen(
  328. method,
  329. url,
  330. body=body,
  331. headers=headers,
  332. _credential_refresh_attempt=_credential_refresh_attempt + 1,
  333. **kwargs
  334. )
  335. return response
  336. # Proxy methods for compliance with the urllib3.PoolManager interface
  337. def __enter__(self):
  338. """Proxy to ``self.http``."""
  339. return self.http.__enter__()
  340. def __exit__(self, exc_type, exc_val, exc_tb):
  341. """Proxy to ``self.http``."""
  342. return self.http.__exit__(exc_type, exc_val, exc_tb)
  343. @property
  344. def headers(self):
  345. """Proxy to ``self.http``."""
  346. return self.http.headers
  347. @headers.setter
  348. def headers(self, value):
  349. """Proxy to ``self.http``."""
  350. self.http.headers = value