google_auth_httplib2.py 10.0 KB

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