App Engine Python SDK version 1.9.12
[gae.git] / python / google / appengine / tools / devappserver2 / http_proxy.py
blob8bdbaf0a7db73f229dd9380e90aed28dd5acd6cf
1 #!/usr/bin/env python
3 # Copyright 2007 Google Inc.
5 # Licensed under the Apache License, Version 2.0 (the "License");
6 # you may not use this file except in compliance with the License.
7 # You may obtain a copy of the License at
9 # http://www.apache.org/licenses/LICENSE-2.0
11 # Unless required by applicable law or agreed to in writing, software
12 # distributed under the License is distributed on an "AS IS" BASIS,
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 # See the License for the specific language governing permissions and
15 # limitations under the License.
17 """Forwards HTTP requests to an application instance on a given host and port.
19 An instance (also referred to as a runtime process) handles dynamic content
20 only. Static files are handled separately.
21 """
23 import contextlib
24 import httplib
25 import logging
26 import socket
27 import urllib
28 import wsgiref.headers
30 from google.appengine.tools.devappserver2 import http_runtime_constants
31 from google.appengine.tools.devappserver2 import instance
32 from google.appengine.tools.devappserver2 import login
33 from google.appengine.tools.devappserver2 import util
36 class Error(Exception):
37 """Base class for errors in this module."""
40 class HostNotReachable(Error):
41 """Raised if host can't be reached at given port."""
44 class HttpProxy:
45 """Forwards HTTP requests to an application instance."""
46 def __init__(self, host, port, instance_died_unexpectedly,
47 instance_logs_getter, error_handler_file, prior_error=None):
48 """Initializer for HttpProxy.
50 Args:
51 host: A hostname or an IP address of where application instance is
52 running.
53 port: Port that application instance is listening on.
54 instance_died_unexpectedly: Function returning True if instance has
55 unexpectedly died.
56 instance_logs_getter: Function returning logs from the instance.
57 error_handler_file: Application specific error handler for default error
58 code if specified (only default error code is supported by
59 Dev AppServer).
60 prior_error: Errors occurred before (for example during creation of an
61 instance). In case prior_error is not None handle will always return
62 corresponding error message without even trying to connect to the
63 instance.
64 """
65 self._host = host
66 self._port = port
67 self._instance_died_unexpectedly = instance_died_unexpectedly
68 self._instance_logs_getter = instance_logs_getter
69 self._error_handler_file = error_handler_file
70 self._prior_error = prior_error
72 def _respond_with_error(self, message, start_response):
73 instance_logs = self._instance_logs_getter()
74 if instance_logs:
75 message += '\n\n' + instance_logs
76 # TODO: change 'text/plain' to 'text/plain; charset=utf-8'
77 # throughout devappserver2.
78 start_response('500 Internal Server Error',
79 [('Content-Type', 'text/plain'),
80 ('Content-Length', str(len(message)))])
81 return message
83 def wait_for_connection(self, retries=100000):
84 """Waits while instance is booting.
86 Args:
87 retries: int, Number of connection retries.
89 Raises:
90 HostNotReachable: if host:port can't be reached after given number of
91 retries.
92 """
93 # If there was a prior error, we don't need to wait for a connection.
94 if self._prior_error:
95 return
97 def ping():
98 connection = httplib.HTTPConnection(self._host, self._port)
99 with contextlib.closing(connection):
100 try:
101 connection.connect()
102 except (socket.error, httplib.HTTPException):
103 return False
104 else:
105 return True
107 while not ping() and retries > 0:
108 retries -= 1
109 if not retries:
110 raise HostNotReachable(
111 'Cannot connect to the instance on {host}:{port}'.format(
112 host=self._host, port=self._port))
114 def handle(self, environ, start_response, url_map, match, request_id,
115 request_type):
116 """Serves this request by forwarding it to the runtime process.
118 Args:
119 environ: An environ dict for the request as defined in PEP-333.
120 start_response: A function with semantics defined in PEP-333.
121 url_map: An appinfo.URLMap instance containing the configuration for the
122 handler matching this request.
123 match: A re.MatchObject containing the result of the matched URL pattern.
124 request_id: A unique string id associated with the request.
125 request_type: The type of the request. See instance.*_REQUEST module
126 constants.
128 Yields:
129 A sequence of strings containing the body of the HTTP response.
132 if self._prior_error:
133 logging.error(self._prior_error)
134 yield self._respond_with_error(self._prior_error, start_response)
135 return
137 environ[http_runtime_constants.SCRIPT_HEADER] = match.expand(url_map.script)
138 if request_type == instance.BACKGROUND_REQUEST:
139 environ[http_runtime_constants.REQUEST_TYPE_HEADER] = 'background'
140 elif request_type == instance.SHUTDOWN_REQUEST:
141 environ[http_runtime_constants.REQUEST_TYPE_HEADER] = 'shutdown'
142 elif request_type == instance.INTERACTIVE_REQUEST:
143 environ[http_runtime_constants.REQUEST_TYPE_HEADER] = 'interactive'
145 for name in http_runtime_constants.ENVIRONS_TO_PROPAGATE:
146 if http_runtime_constants.APPENGINE_ENVIRON_PREFIX + name not in environ:
147 value = environ.get(name, None)
148 if value is not None:
149 environ[
150 http_runtime_constants.APPENGINE_ENVIRON_PREFIX + name] = value
151 headers = util.get_headers_from_environ(environ)
152 if environ.get('QUERY_STRING'):
153 url = '%s?%s' % (urllib.quote(environ['PATH_INFO']),
154 environ['QUERY_STRING'])
155 else:
156 url = urllib.quote(environ['PATH_INFO'])
157 if 'CONTENT_LENGTH' in environ:
158 headers['CONTENT-LENGTH'] = environ['CONTENT_LENGTH']
159 data = environ['wsgi.input'].read(int(environ['CONTENT_LENGTH']))
160 else:
161 data = ''
163 cookies = environ.get('HTTP_COOKIE')
164 user_email, admin, user_id = login.get_user_info(cookies)
165 if user_email:
166 nickname, organization = user_email.split('@', 1)
167 else:
168 nickname = ''
169 organization = ''
170 headers[http_runtime_constants.REQUEST_ID_HEADER] = request_id
171 headers[http_runtime_constants.APPENGINE_HEADER_PREFIX + 'User-Id'] = (
172 user_id)
173 headers[http_runtime_constants.APPENGINE_HEADER_PREFIX + 'User-Email'] = (
174 user_email)
175 headers[
176 http_runtime_constants.APPENGINE_HEADER_PREFIX + 'User-Is-Admin'] = (
177 str(int(admin)))
178 headers[
179 http_runtime_constants.APPENGINE_HEADER_PREFIX + 'User-Nickname'] = (
180 nickname)
181 headers[http_runtime_constants.APPENGINE_HEADER_PREFIX +
182 'User-Organization'] = organization
183 headers['X-AppEngine-Country'] = 'ZZ'
184 connection = httplib.HTTPConnection(self._host, self._port)
185 with contextlib.closing(connection):
186 try:
187 connection.connect()
188 connection.request(environ.get('REQUEST_METHOD', 'GET'),
189 url,
190 data,
191 dict(headers.items()))
193 try:
194 response = connection.getresponse()
195 except httplib.HTTPException as e:
196 # The runtime process has written a bad HTTP response. For example,
197 # a Go runtime process may have crashed in app-specific code.
198 yield self._respond_with_error(
199 'the runtime process gave a bad HTTP response: %s' % e,
200 start_response)
201 return
203 # Ensures that we avoid merging repeat headers into a single header,
204 # allowing use of multiple Set-Cookie headers.
205 headers = []
206 for name in response.msg:
207 for value in response.msg.getheaders(name):
208 headers.append((name, value))
210 response_headers = wsgiref.headers.Headers(headers)
212 if self._error_handler_file and (
213 http_runtime_constants.ERROR_CODE_HEADER in response_headers):
214 try:
215 with open(self._error_handler_file) as f:
216 content = f.read()
217 except IOError:
218 content = 'Failed to load error handler'
219 logging.exception('failed to load error file: %s',
220 self._error_handler_file)
221 start_response('500 Internal Server Error',
222 [('Content-Type', 'text/html'),
223 ('Content-Length', str(len(content)))])
224 yield content
225 return
226 del response_headers[http_runtime_constants.ERROR_CODE_HEADER]
227 start_response('%s %s' % (response.status, response.reason),
228 response_headers.items())
230 # Yield the response body in small blocks.
231 while True:
232 try:
233 block = response.read(512)
234 if not block:
235 break
236 yield block
237 except httplib.HTTPException:
238 # The runtime process has encountered a problem, but has not
239 # necessarily crashed. For example, a Go runtime process' HTTP
240 # handler may have panicked in app-specific code (which the http
241 # package will recover from, so the process as a whole doesn't
242 # crash). At this point, we have already proxied onwards the HTTP
243 # header, so we cannot retroactively serve a 500 Internal Server
244 # Error. We silently break here; the runtime process has presumably
245 # already written to stderr (via the Tee).
246 break
247 except Exception:
248 if self._instance_died_unexpectedly():
249 yield self._respond_with_error(
250 'the runtime process for the instance running on port %d has '
251 'unexpectedly quit' % self._port, start_response)
252 else:
253 raise