blob: af99bf970639e604f578cb5c043828304eba5c49 [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.
#
"""CGI server interface to Python runtime.
CGI-compliant interface between the Python runtime and user-provided Python
code.
"""
from __future__ import with_statement
import cStringIO
from email import feedparser
import imp
import logging
import marshal
import os
import sys
import traceback
import types
def HandleRequest(unused_environ, handler_name, unused_url, post_data,
unused_error, application_root, python_lib,
import_hook=None):
"""Handle a single CGI request.
Handles a request for handler_name in the form 'path/to/handler.py' with the
environment contained in environ.
Args:
handler_name: A str containing the user-specified handler file to use for
this request as specified in the script field of a handler in app.yaml.
post_data: A stream containing the post data for this request.
application_root: A str containing the root path of the application.
python_lib: A str containing the root the Python App Engine library.
import_hook: Optional import hook (PEP 302 style loader).
Returns:
A dict containing zero or more of the following:
error: App Engine error code. 0 for OK, 1 for error. Defaults to OK if not
set. If set, then the other fields may be missing.
response_code: HTTP response code.
headers: A list of tuples (key, value) of HTTP headers.
body: A str of the body of the response.
"""
body = cStringIO.StringIO()
module_name = _FileToModuleName(handler_name)
parent_module, _, submodule_name = module_name.rpartition('.')
parent_module = _GetModuleOrNone(parent_module)
main = None
if module_name in sys.modules:
module = sys.modules[module_name]
main = _GetValidMain(module)
if not main:
module = imp.new_module('__main__')
if import_hook is not None:
module.__loader__ = import_hook
saved_streams = sys.stdin, sys.stdout
try:
sys.modules['__main__'] = module
module.__dict__['__name__'] = '__main__'
sys.stdin = post_data
sys.stdout = body
if main:
os.environ['PATH_TRANSLATED'] = module.__file__
main()
else:
filename = _AbsolutePath(handler_name, application_root, python_lib)
if filename.endswith(os.sep + '__init__.py'):
module.__path__ = [os.path.dirname(filename)]
if import_hook is None:
code, filename = _LoadModuleCode(filename)
else:
code = import_hook.get_code(module_name)
if not code:
return {'error': 2}
os.environ['PATH_TRANSLATED'] = filename
module.__file__ = filename
try:
sys.modules[module_name] = module
eval(code, module.__dict__)
except:
del sys.modules[module_name]
if parent_module and submodule_name in parent_module.__dict__:
del parent_module.__dict__[submodule_name]
raise
else:
if parent_module:
parent_module.__dict__[submodule_name] = module
return _ParseResponse(body)
except:
exception = sys.exc_info()
message = ''.join(traceback.format_exception(exception[0], exception[1],
exception[2].tb_next))
logging.error(message)
return {'error': 1}
finally:
sys.stdin, sys.stdout = saved_streams
module.__name__ = module_name
if '__main__' in sys.modules:
del sys.modules['__main__']
def _ParseResponse(response):
"""Parses an HTTP response into a dict.
Args:
response: A cStringIO.StringIO (StringO) containing the HTTP response.
Returns:
A dict with fields:
body: A str containing the body.
headers: A list containing tuples (key, value) of key and value pairs.
response_code: An int containing the HTTP response code.
"""
response.reset()
parser = feedparser.FeedParser()
parser._set_headersonly()
while True:
line = response.readline()
if not feedparser.headerRE.match(line):
if not feedparser.NLCRE.match(line):
parser.feed(line)
break
parser.feed(line)
parsed_response = parser.close()
if 'Status' in parsed_response:
status = int(parsed_response['Status'].split(' ', 1)[0])
del parsed_response['Status']
else:
status = 200
return {'body': parsed_response.get_payload() + response.read(),
'headers': parsed_response.items(),
'response_code': status}
def _ParseHeader(header):
"""Parses a str header into a (key, value) pair."""
key, _, value = header.partition(':')
return key.strip(), value.strip()
def _GetValidMain(module):
"""Returns a main function in module if it exists and is valid or None.
A main function is valid if it can be called with no arguments, i.e. calling
module.main() would be valid.
Args:
module: The module in which to search for a main function.
Returns:
A function that takes no arguments if found or None otherwise.
"""
if not hasattr(module, 'main'):
return None
main = module.main
if not hasattr(main, '__call__'):
return None
defaults = main.__defaults__
if defaults:
default_argcount = len(defaults)
else:
default_argcount = 0
if (main.__code__.co_argcount - default_argcount) == 0:
return main
else:
return None
def _FileToModuleName(filename):
"""Returns the module name corresponding to a filename."""
_, lib, suffix = filename.partition('$PYTHON_LIB/')
if lib:
module = suffix
else:
module = filename
module = os.path.normpath(module)
if '.py' in module:
module = module.rpartition('.py')[0]
module = module.replace(os.sep, '.')
module = module.strip('.')
if module.endswith('.__init__'):
module = module.rpartition('.__init__')[0]
return module
def _AbsolutePath(filename, application_root, python_lib):
"""Returns the absolute path of a Python script file.
Args:
filename: A str containing the handler script path.
application_root: The absolute path of the root of the application.
python_lib: The absolute path of the Python library.
Returns:
The absolute path of the handler script.
"""
_, lib, suffix = filename.partition('$PYTHON_LIB/')
if lib:
filename = os.path.join(python_lib, suffix)
else:
filename = os.path.join(application_root, filename)
if filename.endswith(os.sep) or os.path.isdir(filename):
filename = os.path.join(filename, '__init__.py')
return filename
def _LoadModuleCode(filename):
"""Loads the code of a module, using compiled bytecode if available.
Args:
filename: The Python script filename.
Returns:
A 2-tuple (code, filename) where:
code: A code object contained in the file or None if it does not exist.
filename: The name of the file loaded, either the same as the arg
filename, or the corresponding .pyc file.
"""
compiled_filename = filename + 'c'
if os.path.exists(compiled_filename):
with open(compiled_filename, 'r') as f:
magic_numbers = f.read(8)
if len(magic_numbers) == 8 and magic_numbers[:4] == imp.get_magic():
try:
return _FixCodeFilename(marshal.load(f), filename), compiled_filename
except (EOFError, ValueError):
pass
if os.path.exists(filename):
with open(filename, 'r') as f:
code = compile(f.read(), filename, 'exec', 0, True)
return code, filename
else:
return None, filename
def _FixCodeFilename(code, filename):
"""Creates a CodeType with co_filename replaced with filename.
Also affects nested code objects in co_consts.
Args:
code: The code object to be replaced.
filename: The replacement filename.
Returns:
A new code object with its co_filename set to the provided filename.
"""
if isinstance(code, types.CodeType):
code = types.CodeType(
code.co_argcount,
code.co_nlocals,
code.co_stacksize,
code.co_flags,
code.co_code,
tuple([_FixCodeFilename(c, filename) for c in code.co_consts]),
code.co_names,
code.co_varnames,
filename,
code.co_name,
code.co_firstlineno,
code.co_lnotab,
code.co_freevars,
code.co_cellvars)
return code
def _GetModuleOrNone(module_name):
"""Returns a module if it exists or None."""
module = None
if module_name:
try:
module = __import__(module_name)
except ImportError:
pass
else:
for name in module_name.split('.')[1:]:
module = getattr(module, name)
return module