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