blob: 3dcb0c50ecb73c0135a3cc8bd6fe3c87d22dc4c0 [file] [log] [blame]
#!/usr/bin/env python
#
# Copyright 2007 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
"""Forwards HTTP requests to an application instance on a given host and port.
An instance (also referred to as a runtime process) handles dynamic content
only. Static files are handled separately.
"""
import contextlib
import httplib
import logging
import socket
import urllib
import wsgiref.headers
from google.appengine.tools.devappserver2 import http_runtime_constants
from google.appengine.tools.devappserver2 import instance
from google.appengine.tools.devappserver2 import login
from google.appengine.tools.devappserver2 import util
class Error(Exception):
"""Base class for errors in this module."""
class HostNotReachable(Error):
"""Raised if host can't be reached at given port."""
class HttpProxy:
"""Forwards HTTP requests to an application instance."""
def __init__(self, host, port, instance_died_unexpectedly,
instance_logs_getter, error_handler_file, prior_error=None):
"""Initializer for HttpProxy.
Args:
host: A hostname or an IP address of where application instance is
running.
port: Port that application instance is listening on.
instance_died_unexpectedly: Function returning True if instance has
unexpectedly died.
instance_logs_getter: Function returning logs from the instance.
error_handler_file: Application specific error handler for default error
code if specified (only default error code is supported by
Dev AppServer).
prior_error: Errors occurred before (for example during creation of an
instance). In case prior_error is not None handle will always return
corresponding error message without even trying to connect to the
instance.
"""
self._host = host
self._port = port
self._instance_died_unexpectedly = instance_died_unexpectedly
self._instance_logs_getter = instance_logs_getter
self._error_handler_file = error_handler_file
self._prior_error = prior_error
def _respond_with_error(self, message, start_response):
instance_logs = self._instance_logs_getter()
if instance_logs:
message += '\n\n' + instance_logs
# TODO: change 'text/plain' to 'text/plain; charset=utf-8'
# throughout devappserver2.
start_response('500 Internal Server Error',
[('Content-Type', 'text/plain'),
('Content-Length', str(len(message)))])
return message
def wait_for_connection(self, retries=100000):
"""Waits while instance is booting.
Args:
retries: int, Number of connection retries.
Raises:
HostNotReachable: if host:port can't be reached after given number of
retries.
"""
def ping():
connection = httplib.HTTPConnection(self._host, self._port)
with contextlib.closing(connection):
try:
connection.connect()
except (socket.error, httplib.HTTPException):
return False
else:
return True
while not ping() and retries > 0:
retries -= 1
if not retries:
raise HostNotReachable(
'Cannot connect to the instance on {host}:{port}'.format(
host=self._host, port=self._port))
def handle(self, environ, start_response, url_map, match, request_id,
request_type):
"""Serves this request by forwarding it to the runtime process.
Args:
environ: An environ dict for the request as defined in PEP-333.
start_response: A function with semantics defined in PEP-333.
url_map: An appinfo.URLMap instance containing the configuration for the
handler matching this request.
match: A re.MatchObject containing the result of the matched URL pattern.
request_id: A unique string id associated with the request.
request_type: The type of the request. See instance.*_REQUEST module
constants.
Yields:
A sequence of strings containing the body of the HTTP response.
"""
if self._prior_error:
logging.error(self._prior_error)
yield self._respond_with_error(self._prior_error, start_response)
return
environ[http_runtime_constants.SCRIPT_HEADER] = match.expand(url_map.script)
if request_type == instance.BACKGROUND_REQUEST:
environ[http_runtime_constants.REQUEST_TYPE_HEADER] = 'background'
elif request_type == instance.SHUTDOWN_REQUEST:
environ[http_runtime_constants.REQUEST_TYPE_HEADER] = 'shutdown'
elif request_type == instance.INTERACTIVE_REQUEST:
environ[http_runtime_constants.REQUEST_TYPE_HEADER] = 'interactive'
for name in http_runtime_constants.ENVIRONS_TO_PROPAGATE:
if http_runtime_constants.APPENGINE_ENVIRON_PREFIX + name not in environ:
value = environ.get(name, None)
if value is not None:
environ[
http_runtime_constants.APPENGINE_ENVIRON_PREFIX + name] = value
headers = util.get_headers_from_environ(environ)
if environ.get('QUERY_STRING'):
url = '%s?%s' % (urllib.quote(environ['PATH_INFO']),
environ['QUERY_STRING'])
else:
url = urllib.quote(environ['PATH_INFO'])
if 'CONTENT_LENGTH' in environ:
headers['CONTENT-LENGTH'] = environ['CONTENT_LENGTH']
data = environ['wsgi.input'].read(int(environ['CONTENT_LENGTH']))
else:
data = ''
cookies = environ.get('HTTP_COOKIE')
user_email, admin, user_id = login.get_user_info(cookies)
if user_email:
nickname, organization = user_email.split('@', 1)
else:
nickname = ''
organization = ''
headers[http_runtime_constants.REQUEST_ID_HEADER] = request_id
headers[http_runtime_constants.APPENGINE_HEADER_PREFIX + 'User-Id'] = (
user_id)
headers[http_runtime_constants.APPENGINE_HEADER_PREFIX + 'User-Email'] = (
user_email)
headers[
http_runtime_constants.APPENGINE_HEADER_PREFIX + 'User-Is-Admin'] = (
str(int(admin)))
headers[
http_runtime_constants.APPENGINE_HEADER_PREFIX + 'User-Nickname'] = (
nickname)
headers[http_runtime_constants.APPENGINE_HEADER_PREFIX +
'User-Organization'] = organization
headers['X-AppEngine-Country'] = 'ZZ'
connection = httplib.HTTPConnection(self._host, self._port)
with contextlib.closing(connection):
try:
connection.connect()
connection.request(environ.get('REQUEST_METHOD', 'GET'),
url,
data,
dict(headers.items()))
try:
response = connection.getresponse()
except httplib.HTTPException as e:
# The runtime process has written a bad HTTP response. For example,
# a Go runtime process may have crashed in app-specific code.
yield self._respond_with_error(
'the runtime process gave a bad HTTP response: %s' % e,
start_response)
return
# Ensures that we avoid merging repeat headers into a single header,
# allowing use of multiple Set-Cookie headers.
headers = []
for name in response.msg:
for value in response.msg.getheaders(name):
headers.append((name, value))
response_headers = wsgiref.headers.Headers(headers)
if self._error_handler_file and (
http_runtime_constants.ERROR_CODE_HEADER in response_headers):
try:
with open(self._error_handler_file) as f:
content = f.read()
except IOError:
content = 'Failed to load error handler'
logging.exception('failed to load error file: %s',
self._error_handler_file)
start_response('500 Internal Server Error',
[('Content-Type', 'text/html'),
('Content-Length', str(len(content)))])
yield content
return
del response_headers[http_runtime_constants.ERROR_CODE_HEADER]
start_response('%s %s' % (response.status, response.reason),
response_headers.items())
# Yield the response body in small blocks.
while True:
try:
block = response.read(512)
if not block:
break
yield block
except httplib.HTTPException:
# The runtime process has encountered a problem, but has not
# necessarily crashed. For example, a Go runtime process' HTTP
# handler may have panicked in app-specific code (which the http
# package will recover from, so the process as a whole doesn't
# crash). At this point, we have already proxied onwards the HTTP
# header, so we cannot retroactively serve a 500 Internal Server
# Error. We silently break here; the runtime process has presumably
# already written to stderr (via the Tee).
break
except Exception:
if self._instance_died_unexpectedly():
yield self._respond_with_error(
'the runtime process for the instance running on port %d has '
'unexpectedly quit' % self._port, start_response)
else:
raise