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