f4289c0ff8d54dae052b8ca076fdfedd48c540f1
[sdc/sdc-distribution-client.git] /
1 from __future__ import absolute_import
2 import logging
3 import os
4 import warnings
5
6 from ..exceptions import (
7     HTTPError,
8     HTTPWarning,
9     MaxRetryError,
10     ProtocolError,
11     TimeoutError,
12     SSLError
13 )
14
15 from ..packages.six import BytesIO
16 from ..request import RequestMethods
17 from ..response import HTTPResponse
18 from ..util.timeout import Timeout
19 from ..util.retry import Retry
20
21 try:
22     from google.appengine.api import urlfetch
23 except ImportError:
24     urlfetch = None
25
26
27 log = logging.getLogger(__name__)
28
29
30 class AppEnginePlatformWarning(HTTPWarning):
31     pass
32
33
34 class AppEnginePlatformError(HTTPError):
35     pass
36
37
38 class AppEngineManager(RequestMethods):
39     """
40     Connection manager for Google App Engine sandbox applications.
41
42     This manager uses the URLFetch service directly instead of using the
43     emulated httplib, and is subject to URLFetch limitations as described in
44     the App Engine documentation here:
45
46         https://cloud.google.com/appengine/docs/python/urlfetch
47
48     Notably it will raise an AppEnginePlatformError if:
49         * URLFetch is not available.
50         * If you attempt to use this on GAEv2 (Managed VMs), as full socket
51           support is available.
52         * If a request size is more than 10 megabytes.
53         * If a response size is more than 32 megabtyes.
54         * If you use an unsupported request method such as OPTIONS.
55
56     Beyond those cases, it will raise normal urllib3 errors.
57     """
58
59     def __init__(self, headers=None, retries=None, validate_certificate=True):
60         if not urlfetch:
61             raise AppEnginePlatformError(
62                 "URLFetch is not available in this environment.")
63
64         if is_prod_appengine_mvms():
65             raise AppEnginePlatformError(
66                 "Use normal urllib3.PoolManager instead of AppEngineManager"
67                 "on Managed VMs, as using URLFetch is not necessary in "
68                 "this environment.")
69
70         warnings.warn(
71             "urllib3 is using URLFetch on Google App Engine sandbox instead "
72             "of sockets. To use sockets directly instead of URLFetch see "
73             "https://urllib3.readthedocs.org/en/latest/contrib.html.",
74             AppEnginePlatformWarning)
75
76         RequestMethods.__init__(self, headers)
77         self.validate_certificate = validate_certificate
78
79         self.retries = retries or Retry.DEFAULT
80
81     def __enter__(self):
82         return self
83
84     def __exit__(self, exc_type, exc_val, exc_tb):
85         # Return False to re-raise any potential exceptions
86         return False
87
88     def urlopen(self, method, url, body=None, headers=None,
89                 retries=None, redirect=True, timeout=Timeout.DEFAULT_TIMEOUT,
90                 **response_kw):
91
92         retries = self._get_retries(retries, redirect)
93
94         try:
95             response = urlfetch.fetch(
96                 url,
97                 payload=body,
98                 method=method,
99                 headers=headers or {},
100                 allow_truncated=False,
101                 follow_redirects=(
102                     redirect and
103                     retries.redirect != 0 and
104                     retries.total),
105                 deadline=self._get_absolute_timeout(timeout),
106                 validate_certificate=self.validate_certificate,
107             )
108         except urlfetch.DeadlineExceededError as e:
109             raise TimeoutError(self, e)
110
111         except urlfetch.InvalidURLError as e:
112             if 'too large' in str(e):
113                 raise AppEnginePlatformError(
114                     "URLFetch request too large, URLFetch only "
115                     "supports requests up to 10mb in size.", e)
116             raise ProtocolError(e)
117
118         except urlfetch.DownloadError as e:
119             if 'Too many redirects' in str(e):
120                 raise MaxRetryError(self, url, reason=e)
121             raise ProtocolError(e)
122
123         except urlfetch.ResponseTooLargeError as e:
124             raise AppEnginePlatformError(
125                 "URLFetch response too large, URLFetch only supports"
126                 "responses up to 32mb in size.", e)
127
128         except urlfetch.SSLCertificateError as e:
129             raise SSLError(e)
130
131         except urlfetch.InvalidMethodError as e:
132             raise AppEnginePlatformError(
133                 "URLFetch does not support method: %s" % method, e)
134
135         http_response = self._urlfetch_response_to_http_response(
136             response, **response_kw)
137
138         # Check for redirect response
139         if (http_response.get_redirect_location() and
140                 retries.raise_on_redirect and redirect):
141             raise MaxRetryError(self, url, "too many redirects")
142
143         # Check if we should retry the HTTP response.
144         if retries.is_forced_retry(method, status_code=http_response.status):
145             retries = retries.increment(
146                 method, url, response=http_response, _pool=self)
147             log.info("Forced retry: %s", url)
148             retries.sleep()
149             return self.urlopen(
150                 method, url,
151                 body=body, headers=headers,
152                 retries=retries, redirect=redirect,
153                 timeout=timeout, **response_kw)
154
155         return http_response
156
157     def _urlfetch_response_to_http_response(self, urlfetch_resp, **response_kw):
158
159         if is_prod_appengine():
160             # Production GAE handles deflate encoding automatically, but does
161             # not remove the encoding header.
162             content_encoding = urlfetch_resp.headers.get('content-encoding')
163
164             if content_encoding == 'deflate':
165                 del urlfetch_resp.headers['content-encoding']
166
167         transfer_encoding = urlfetch_resp.headers.get('transfer-encoding')
168         # We have a full response's content,
169         # so let's make sure we don't report ourselves as chunked data.
170         if transfer_encoding == 'chunked':
171             encodings = transfer_encoding.split(",")
172             encodings.remove('chunked')
173             urlfetch_resp.headers['transfer-encoding'] = ','.join(encodings)
174
175         return HTTPResponse(
176             # In order for decoding to work, we must present the content as
177             # a file-like object.
178             body=BytesIO(urlfetch_resp.content),
179             headers=urlfetch_resp.headers,
180             status=urlfetch_resp.status_code,
181             **response_kw
182         )
183
184     def _get_absolute_timeout(self, timeout):
185         if timeout is Timeout.DEFAULT_TIMEOUT:
186             return 5  # 5s is the default timeout for URLFetch.
187         if isinstance(timeout, Timeout):
188             if timeout._read is not timeout._connect:
189                 warnings.warn(
190                     "URLFetch does not support granular timeout settings, "
191                     "reverting to total timeout.", AppEnginePlatformWarning)
192             return timeout.total
193         return timeout
194
195     def _get_retries(self, retries, redirect):
196         if not isinstance(retries, Retry):
197             retries = Retry.from_int(
198                 retries, redirect=redirect, default=self.retries)
199
200         if retries.connect or retries.read or retries.redirect:
201             warnings.warn(
202                 "URLFetch only supports total retries and does not "
203                 "recognize connect, read, or redirect retry parameters.",
204                 AppEnginePlatformWarning)
205
206         return retries
207
208
209 def is_appengine():
210     return (is_local_appengine() or
211             is_prod_appengine() or
212             is_prod_appengine_mvms())
213
214
215 def is_appengine_sandbox():
216     return is_appengine() and not is_prod_appengine_mvms()
217
218
219 def is_local_appengine():
220     return ('APPENGINE_RUNTIME' in os.environ and
221             'Development/' in os.environ['SERVER_SOFTWARE'])
222
223
224 def is_prod_appengine():
225     return ('APPENGINE_RUNTIME' in os.environ and
226             'Google App Engine/' in os.environ['SERVER_SOFTWARE'] and
227             not is_prod_appengine_mvms())
228
229
230 def is_prod_appengine_mvms():
231     return os.environ.get('GAE_VM', False) == 'true'