1# Copyright 2016 Google Inc.
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 httplib2."""
16
17from __future__ import absolute_import
18
19import http.client
20import logging
21
22from google.auth import exceptions
23from google.auth import transport
24import httplib2
25
26
27_LOGGER = logging.getLogger(__name__)
28# Properties present in file-like streams / buffers.
29_STREAM_PROPERTIES = ("read", "seek", "tell")
30
31
32class _Response(transport.Response):
33 """httplib2 transport response adapter.
34
35 Args:
36 response (httplib2.Response): The raw httplib2 response.
37 data (bytes): The response body.
38 """
39
40 def __init__(self, response, data):
41 self._response = response
42 self._data = data
43
44 @property
45 def status(self):
46 """int: The HTTP status code."""
47 return self._response.status
48
49 @property
50 def headers(self):
51 """Mapping[str, str]: The HTTP response headers."""
52 return dict(self._response)
53
54 @property
55 def data(self):
56 """bytes: The response body."""
57 return self._data
58
59
60class Request(transport.Request):
61 """httplib2 request adapter.
62
63 This class is used internally for making requests using various transports
64 in a consistent way. If you use :class:`AuthorizedHttp` you do not need
65 to construct or use this class directly.
66
67 This class can be useful if you want to manually refresh a
68 :class:`~google.auth.credentials.Credentials` instance::
69
70 import google_auth_httplib2
71 import httplib2
72
73 http = httplib2.Http()
74 request = google_auth_httplib2.Request(http)
75
76 credentials.refresh(request)
77
78 Args:
79 http (httplib2.Http): The underlying http object to use to make
80 requests.
81 """
82
83 def __init__(self, http):
84 self.http = http
85
86 def __call__(
87 self, url, method="GET", body=None, headers=None, timeout=None, **kwargs
88 ):
89 """Make an HTTP request using httplib2.
90
91 Args:
92 url (str): The URI to be requested.
93 method (str): The HTTP method to use for the request. Defaults
94 to 'GET'.
95 body (bytes): The payload / body in HTTP request.
96 headers (Mapping[str, str]): Request headers.
97 timeout (Optional[int]): The number of seconds to wait for a
98 response from the server. This is ignored by httplib2 and will
99 issue a warning.
100 kwargs: Additional arguments passed throught to the underlying
101 :meth:`httplib2.Http.request` method.
102
103 Returns:
104 google.auth.transport.Response: The HTTP response.
105
106 Raises:
107 google.auth.exceptions.TransportError: If any exception occurred.
108 """
109 if timeout is not None:
110 _LOGGER.warning(
111 "httplib2 transport does not support per-request timeout. "
112 "Set the timeout when constructing the httplib2.Http instance."
113 )
114
115 try:
116 _LOGGER.debug("Making request: %s %s", method, url)
117 response, data = self.http.request(
118 url, method=method, body=body, headers=headers, **kwargs
119 )
120 return _Response(response, data)
121 # httplib2 should catch the lower http error, this is a bug and
122 # needs to be fixed there. Catch the error for the meanwhile.
123 except (httplib2.HttpLib2Error, http.client.HTTPException) as exc:
124 raise exceptions.TransportError(exc)
125
126
127def _make_default_http():
128 """Returns a default httplib2.Http instance."""
129 return httplib2.Http()
130
131
132class AuthorizedHttp(object):
133 """A httplib2 HTTP class with credentials.
134
135 This class is used to perform requests to API endpoints that require
136 authorization::
137
138 from google.auth.transport._httplib2 import AuthorizedHttp
139
140 authed_http = AuthorizedHttp(credentials)
141
142 response = authed_http.request(
143 'https://www.googleapis.com/storage/v1/b')
144
145 This class implements :meth:`request` in the same way as
146 :class:`httplib2.Http` and can usually be used just like any other
147 instance of :class:`httplib2.Http`.
148
149 The underlying :meth:`request` implementation handles adding the
150 credentials' headers to the request and refreshing credentials as needed.
151 """
152
153 def __init__(
154 self,
155 credentials,
156 http=None,
157 refresh_status_codes=transport.DEFAULT_REFRESH_STATUS_CODES,
158 max_refresh_attempts=transport.DEFAULT_MAX_REFRESH_ATTEMPTS,
159 ):
160 """
161 Args:
162 credentials (google.auth.credentials.Credentials): The credentials
163 to add to the request.
164 http (httplib2.Http): The underlying HTTP object to
165 use to make requests. If not specified, a
166 :class:`httplib2.Http` instance will be constructed.
167 refresh_status_codes (Sequence[int]): Which HTTP status codes
168 indicate that credentials should be refreshed and the request
169 should be retried.
170 max_refresh_attempts (int): The maximum number of times to attempt
171 to refresh the credentials and retry the request.
172 """
173
174 if http is None:
175 http = _make_default_http()
176
177 self.http = http
178 self.credentials = credentials
179 self._refresh_status_codes = refresh_status_codes
180 self._max_refresh_attempts = max_refresh_attempts
181 # Request instance used by internal methods (for example,
182 # credentials.refresh).
183 self._request = Request(self.http)
184
185 def close(self):
186 """Calls httplib2's Http.close"""
187 self.http.close()
188
189 def request(
190 self,
191 uri,
192 method="GET",
193 body=None,
194 headers=None,
195 redirections=httplib2.DEFAULT_MAX_REDIRECTS,
196 connection_type=None,
197 **kwargs
198 ):
199 """Implementation of httplib2's Http.request."""
200
201 _credential_refresh_attempt = kwargs.pop("_credential_refresh_attempt", 0)
202
203 # Make a copy of the headers. They will be modified by the credentials
204 # and we want to pass the original headers if we recurse.
205 request_headers = headers.copy() if headers is not None else {}
206
207 self.credentials.before_request(self._request, method, uri, request_headers)
208
209 # Check if the body is a file-like stream, and if so, save the body
210 # stream position so that it can be restored in case of refresh.
211 body_stream_position = None
212 if all(getattr(body, stream_prop, None) for stream_prop in _STREAM_PROPERTIES):
213 body_stream_position = body.tell()
214
215 # Make the request.
216 response, content = self.http.request(
217 uri,
218 method,
219 body=body,
220 headers=request_headers,
221 redirections=redirections,
222 connection_type=connection_type,
223 **kwargs
224 )
225
226 # If the response indicated that the credentials needed to be
227 # refreshed, then refresh the credentials and re-attempt the
228 # request.
229 # A stored token may expire between the time it is retrieved and
230 # the time the request is made, so we may need to try twice.
231 if (
232 response.status in self._refresh_status_codes
233 and _credential_refresh_attempt < self._max_refresh_attempts
234 ):
235 _LOGGER.info(
236 "Refreshing credentials due to a %s response. Attempt %s/%s.",
237 response.status,
238 _credential_refresh_attempt + 1,
239 self._max_refresh_attempts,
240 )
241
242 self.credentials.refresh(self._request)
243
244 # Restore the body's stream position if needed.
245 if body_stream_position is not None:
246 body.seek(body_stream_position)
247
248 # Recurse. Pass in the original headers, not our modified set.
249 return self.request(
250 uri,
251 method,
252 body=body,
253 headers=headers,
254 redirections=redirections,
255 connection_type=connection_type,
256 _credential_refresh_attempt=_credential_refresh_attempt + 1,
257 **kwargs
258 )
259
260 return response, content
261
262 def add_certificate(self, key, cert, domain, password=None):
263 """Proxy to httplib2.Http.add_certificate."""
264 self.http.add_certificate(key, cert, domain, password=password)
265
266 @property
267 def connections(self):
268 """Proxy to httplib2.Http.connections."""
269 return self.http.connections
270
271 @connections.setter
272 def connections(self, value):
273 """Proxy to httplib2.Http.connections."""
274 self.http.connections = value
275
276 @property
277 def follow_redirects(self):
278 """Proxy to httplib2.Http.follow_redirects."""
279 return self.http.follow_redirects
280
281 @follow_redirects.setter
282 def follow_redirects(self, value):
283 """Proxy to httplib2.Http.follow_redirects."""
284 self.http.follow_redirects = value
285
286 @property
287 def timeout(self):
288 """Proxy to httplib2.Http.timeout."""
289 return self.http.timeout
290
291 @timeout.setter
292 def timeout(self, value):
293 """Proxy to httplib2.Http.timeout."""
294 self.http.timeout = value
295
296 @property
297 def redirect_codes(self):
298 """Proxy to httplib2.Http.redirect_codes."""
299 return self.http.redirect_codes
300
301 @redirect_codes.setter
302 def redirect_codes(self, value):
303 """Proxy to httplib2.Http.redirect_codes."""
304 self.http.redirect_codes = value