1#!/usr/bin/env python
2# Copyright 2016 Google Inc. All rights reserved.
3#
4# Licensed under the Apache License, Version 2.0 (the "License");
5# you may not use this file except in compliance with the License.
6# You may obtain a copy of the License at
7#
8#      http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS,
12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13# See the License for the specific language governing permissions and
14# limitations under the License.
15
16import logging
17
18import httplib2
19import six
20from six.moves import http_client
21
22from oauth2client_4_0 import _helpers
23
24
25_LOGGER = logging.getLogger(__name__)
26# Properties present in file-like streams / buffers.
27_STREAM_PROPERTIES = ('read', 'seek', 'tell')
28
29# Google Data client libraries may need to set this to [401, 403].
30REFRESH_STATUS_CODES = (http_client.UNAUTHORIZED,)
31
32
33class MemoryCache(object):
34    """httplib2 Cache implementation which only caches locally."""
35
36    def __init__(self):
37        self.cache = {}
38
39    def get(self, key):
40        return self.cache.get(key)
41
42    def set(self, key, value):
43        self.cache[key] = value
44
45    def delete(self, key):
46        self.cache.pop(key, None)
47
48
49def get_cached_http():
50    """Return an HTTP object which caches results returned.
51
52    This is intended to be used in methods like
53    oauth2client_4_0.client.verify_id_token(), which calls to the same URI
54    to retrieve certs.
55
56    Returns:
57        httplib2.Http, an HTTP object with a MemoryCache
58    """
59    return _CACHED_HTTP
60
61
62def get_http_object(*args, **kwargs):
63    """Return a new HTTP object.
64
65    Args:
66        *args: tuple, The positional arguments to be passed when
67               contructing a new HTTP object.
68        **kwargs: dict, The keyword arguments to be passed when
69                  contructing a new HTTP object.
70
71    Returns:
72        httplib2.Http, an HTTP object.
73    """
74    return httplib2.Http(*args, **kwargs)
75
76
77def _initialize_headers(headers):
78    """Creates a copy of the headers.
79
80    Args:
81        headers: dict, request headers to copy.
82
83    Returns:
84        dict, the copied headers or a new dictionary if the headers
85        were None.
86    """
87    return {} if headers is None else dict(headers)
88
89
90def _apply_user_agent(headers, user_agent):
91    """Adds a user-agent to the headers.
92
93    Args:
94        headers: dict, request headers to add / modify user
95                 agent within.
96        user_agent: str, the user agent to add.
97
98    Returns:
99        dict, the original headers passed in, but modified if the
100        user agent is not None.
101    """
102    if user_agent is not None:
103        if 'user-agent' in headers:
104            headers['user-agent'] = (user_agent + ' ' + headers['user-agent'])
105        else:
106            headers['user-agent'] = user_agent
107
108    return headers
109
110
111def clean_headers(headers):
112    """Forces header keys and values to be strings, i.e not unicode.
113
114    The httplib module just concats the header keys and values in a way that
115    may make the message header a unicode string, which, if it then tries to
116    contatenate to a binary request body may result in a unicode decode error.
117
118    Args:
119        headers: dict, A dictionary of headers.
120
121    Returns:
122        The same dictionary but with all the keys converted to strings.
123    """
124    clean = {}
125    try:
126        for k, v in six.iteritems(headers):
127            if not isinstance(k, six.binary_type):
128                k = str(k)
129            if not isinstance(v, six.binary_type):
130                v = str(v)
131            clean[_helpers._to_bytes(k)] = _helpers._to_bytes(v)
132    except UnicodeEncodeError:
133        from oauth2client_4_0.client import NonAsciiHeaderError
134        raise NonAsciiHeaderError(k, ': ', v)
135    return clean
136
137
138def wrap_http_for_auth(credentials, http):
139    """Prepares an HTTP object's request method for auth.
140
141    Wraps HTTP requests with logic to catch auth failures (typically
142    identified via a 401 status code). In the event of failure, tries
143    to refresh the token used and then retry the original request.
144
145    Args:
146        credentials: Credentials, the credentials used to identify
147                     the authenticated user.
148        http: httplib2.Http, an http object to be used to make
149              auth requests.
150    """
151    orig_request_method = http.request
152
153    # The closure that will replace 'httplib2.Http.request'.
154    def new_request(uri, method='GET', body=None, headers=None,
155                    redirections=httplib2.DEFAULT_MAX_REDIRECTS,
156                    connection_type=None):
157        if not credentials.access_token:
158            _LOGGER.info('Attempting refresh to obtain '
159                         'initial access_token')
160            credentials._refresh(orig_request_method)
161
162        # Clone and modify the request headers to add the appropriate
163        # Authorization header.
164        headers = _initialize_headers(headers)
165        credentials.apply(headers)
166        _apply_user_agent(headers, credentials.user_agent)
167
168        body_stream_position = None
169        # Check if the body is a file-like stream.
170        if all(getattr(body, stream_prop, None) for stream_prop in
171               _STREAM_PROPERTIES):
172            body_stream_position = body.tell()
173
174        resp, content = request(orig_request_method, uri, method, body,
175                                clean_headers(headers),
176                                redirections, connection_type)
177
178        # A stored token may expire between the time it is retrieved and
179        # the time the request is made, so we may need to try twice.
180        max_refresh_attempts = 2
181        for refresh_attempt in range(max_refresh_attempts):
182            if resp.status not in REFRESH_STATUS_CODES:
183                break
184            _LOGGER.info('Refreshing due to a %s (attempt %s/%s)',
185                         resp.status, refresh_attempt + 1,
186                         max_refresh_attempts)
187            credentials._refresh(orig_request_method)
188            credentials.apply(headers)
189            if body_stream_position is not None:
190                body.seek(body_stream_position)
191
192            resp, content = request(orig_request_method, uri, method, body,
193                                    clean_headers(headers),
194                                    redirections, connection_type)
195
196        return resp, content
197
198    # Replace the request method with our own closure.
199    http.request = new_request
200
201    # Set credentials as a property of the request method.
202    http.request.credentials = credentials
203
204
205def wrap_http_for_jwt_access(credentials, http):
206    """Prepares an HTTP object's request method for JWT access.
207
208    Wraps HTTP requests with logic to catch auth failures (typically
209    identified via a 401 status code). In the event of failure, tries
210    to refresh the token used and then retry the original request.
211
212    Args:
213        credentials: _JWTAccessCredentials, the credentials used to identify
214                     a service account that uses JWT access tokens.
215        http: httplib2.Http, an http object to be used to make
216              auth requests.
217    """
218    orig_request_method = http.request
219    wrap_http_for_auth(credentials, http)
220    # The new value of ``http.request`` set by ``wrap_http_for_auth``.
221    authenticated_request_method = http.request
222
223    # The closure that will replace 'httplib2.Http.request'.
224    def new_request(uri, method='GET', body=None, headers=None,
225                    redirections=httplib2.DEFAULT_MAX_REDIRECTS,
226                    connection_type=None):
227        if 'aud' in credentials._kwargs:
228            # Preemptively refresh token, this is not done for OAuth2
229            if (credentials.access_token is None or
230                    credentials.access_token_expired):
231                credentials.refresh(None)
232            return request(authenticated_request_method, uri,
233                           method, body, headers, redirections,
234                           connection_type)
235        else:
236            # If we don't have an 'aud' (audience) claim,
237            # create a 1-time token with the uri root as the audience
238            headers = _initialize_headers(headers)
239            _apply_user_agent(headers, credentials.user_agent)
240            uri_root = uri.split('?', 1)[0]
241            token, unused_expiry = credentials._create_token({'aud': uri_root})
242
243            headers['Authorization'] = 'Bearer ' + token
244            return request(orig_request_method, uri, method, body,
245                           clean_headers(headers),
246                           redirections, connection_type)
247
248    # Replace the request method with our own closure.
249    http.request = new_request
250
251    # Set credentials as a property of the request method.
252    http.request.credentials = credentials
253
254
255def request(http, uri, method='GET', body=None, headers=None,
256            redirections=httplib2.DEFAULT_MAX_REDIRECTS,
257            connection_type=None):
258    """Make an HTTP request with an HTTP object and arguments.
259
260    Args:
261        http: httplib2.Http, an http object to be used to make requests.
262        uri: string, The URI to be requested.
263        method: string, The HTTP method to use for the request. Defaults
264                to 'GET'.
265        body: string, The payload / body in HTTP request. By default
266              there is no payload.
267        headers: dict, Key-value pairs of request headers. By default
268                 there are no headers.
269        redirections: int, The number of allowed 203 redirects for
270                      the request. Defaults to 5.
271        connection_type: httplib.HTTPConnection, a subclass to be used for
272                         establishing connection. If not set, the type
273                         will be determined from the ``uri``.
274
275    Returns:
276        tuple, a pair of a httplib2.Response with the status code and other
277        headers and the bytes of the content returned.
278    """
279    # NOTE: Allowing http or http.request is temporary (See Issue 601).
280    http_callable = getattr(http, 'request', http)
281    return http_callable(uri, method=method, body=body, headers=headers,
282                         redirections=redirections,
283                         connection_type=connection_type)
284
285
286_CACHED_HTTP = httplib2.Http(MemoryCache())
287