--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/thirdparty/google_appengine/google/appengine/tools/dev_appserver.py Tue Aug 26 21:49:54 2008 +0000
@@ -0,0 +1,2649 @@
+#!/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.
+#
+"""Pure-Python application server for testing applications locally.
+
+Given a port and the paths to a valid application directory (with an 'app.yaml'
+file), the external library directory, and a relative URL to use for logins,
+creates an HTTP server that can be used to test an application locally. Uses
+stubs instead of actual APIs when SetupStubs() is called first.
+
+Example:
+ root_path = '/path/to/application/directory'
+ login_url = '/login'
+ port = 8080
+ template_dir = '/path/to/appserver/templates'
+ server = dev_appserver.CreateServer(root_path, login_url, port, template_dir)
+ server.serve_forever()
+"""
+
+
+import os
+os.environ['TZ'] = 'UTC'
+import time
+if hasattr(time, 'tzset'):
+ time.tzset()
+
+import __builtin__
+import BaseHTTPServer
+import Cookie
+import cStringIO
+import cgi
+import cgitb
+import dummy_thread
+import errno
+import httplib
+import imp
+import inspect
+import itertools
+import logging
+import mimetools
+import mimetypes
+import pickle
+import pprint
+import random
+
+import re
+import sre_compile
+import sre_constants
+import sre_parse
+
+import mimetypes
+import socket
+import sys
+import urlparse
+import urllib
+import traceback
+import types
+
+import google
+from google.pyglib import gexcept
+
+from google.appengine.api import apiproxy_stub_map
+from google.appengine.api import appinfo
+from google.appengine.api import datastore_admin
+from google.appengine.api import datastore_file_stub
+from google.appengine.api import urlfetch_stub
+from google.appengine.api import mail_stub
+from google.appengine.api import user_service_stub
+from google.appengine.api import yaml_errors
+from google.appengine.api.memcache import memcache_stub
+
+from google.appengine.tools import dev_appserver_index
+from google.appengine.tools import dev_appserver_login
+
+
+PYTHON_LIB_VAR = '$PYTHON_LIB'
+DEVEL_CONSOLE_PATH = PYTHON_LIB_VAR + '/google/appengine/ext/admin'
+
+FILE_MISSING_EXCEPTIONS = frozenset([errno.ENOENT, errno.ENOTDIR])
+
+MAX_URL_LENGTH = 2047
+
+HEADER_TEMPLATE = 'logging_console_header.html'
+SCRIPT_TEMPLATE = 'logging_console.js'
+MIDDLE_TEMPLATE = 'logging_console_middle.html'
+FOOTER_TEMPLATE = 'logging_console_footer.html'
+
+DEFAULT_ENV = {
+ 'GATEWAY_INTERFACE': 'CGI/1.1',
+ 'AUTH_DOMAIN': 'gmail.com',
+ 'TZ': 'UTC',
+}
+
+for ext, mime_type in (('.asc', 'text/plain'),
+ ('.diff', 'text/plain'),
+ ('.csv', 'text/comma-separated-values'),
+ ('.rss', 'application/rss+xml'),
+ ('.text', 'text/plain'),
+ ('.wbmp', 'image/vnd.wap.wbmp')):
+ mimetypes.add_type(mime_type, ext)
+
+
+class Error(Exception):
+ """Base-class for exceptions in this module."""
+
+class InvalidAppConfigError(Error):
+ """The supplied application configuration file is invalid."""
+
+class AppConfigNotFoundError(Error):
+ """Application configuration file not found."""
+
+class TemplatesNotLoadedError(Error):
+ """Templates for the debugging console were not loaded."""
+
+
+def SplitURL(relative_url):
+ """Splits a relative URL into its path and query-string components.
+
+ Args:
+ relative_url: String containing the relative URL (often starting with '/')
+ to split. Should be properly escaped as www-form-urlencoded data.
+
+ Returns:
+ Tuple (script_name, query_string) where:
+ script_name: Relative URL of the script that was accessed.
+ query_string: String containing everything after the '?' character.
+ """
+ scheme, netloc, path, query, fragment = urlparse.urlsplit(relative_url)
+ return path, query
+
+
+def GetFullURL(server_name, server_port, relative_url):
+ """Returns the full, original URL used to access the relative URL.
+
+ Args:
+ server_name: Name of the local host, or the value of the 'host' header
+ from the request.
+ server_port: Port on which the request was served (string or int).
+ relative_url: Relative URL that was accessed, including query string.
+
+ Returns:
+ String containing the original URL.
+ """
+ if str(server_port) != '80':
+ netloc = '%s:%s' % (server_name, server_port)
+ else:
+ netloc = server_name
+ return 'http://%s%s' % (netloc, relative_url)
+
+
+class URLDispatcher(object):
+ """Base-class for handling HTTP requests."""
+
+ def Dispatch(self,
+ relative_url,
+ path,
+ headers,
+ infile,
+ outfile,
+ base_env_dict=None):
+ """Dispatch and handle an HTTP request.
+
+ base_env_dict should contain at least these CGI variables:
+ REQUEST_METHOD, REMOTE_ADDR, SERVER_SOFTWARE, SERVER_NAME,
+ SERVER_PROTOCOL, SERVER_PORT
+
+ Args:
+ relative_url: String containing the URL accessed.
+ path: Local path of the resource that was matched; back-references will be
+ replaced by values matched in the relative_url. Path may be relative
+ or absolute, depending on the resource being served (e.g., static files
+ will have an absolute path; scripts will be relative).
+ headers: Instance of mimetools.Message with headers from the request.
+ infile: File-like object with input data from the request.
+ outfile: File-like object where output data should be written.
+ base_env_dict: Dictionary of CGI environment parameters if available.
+ Defaults to None.
+ """
+ raise NotImplementedError
+
+
+class URLMatcher(object):
+ """Matches an arbitrary URL using a list of URL patterns from an application.
+
+ Each URL pattern has an associated URLDispatcher instance and path to the
+ resource's location on disk. See AddURL for more details. The first pattern
+ that matches an inputted URL will have its associated values returned by
+ Match().
+ """
+
+ def __init__(self):
+ """Initializer."""
+ self._url_patterns = []
+
+ def AddURL(self, regex, dispatcher, path, requires_login, admin_only):
+ """Adds a URL pattern to the list of patterns.
+
+ If the supplied regex starts with a '^' or ends with a '$' an
+ InvalidAppConfigError exception will be raised. Start and end symbols
+ and implicitly added to all regexes, meaning we assume that all regexes
+ consume all input from a URL.
+
+ Args:
+ regex: String containing the regular expression pattern.
+ dispatcher: Instance of URLDispatcher that should handle requests that
+ match this regex.
+ path: Path on disk for the resource. May contain back-references like
+ r'\1', r'\2', etc, which will be replaced by the corresponding groups
+ matched by the regex if present.
+ requires_login: True if the user must be logged-in before accessing this
+ URL; False if anyone can access this URL.
+ admin_only: True if the user must be a logged-in administrator to
+ access the URL; False if anyone can access the URL.
+ """
+ if not isinstance(dispatcher, URLDispatcher):
+ raise TypeError, 'dispatcher must be a URLDispatcher sub-class'
+
+ if regex.startswith('^') or regex.endswith('$'):
+ raise InvalidAppConfigError, 'regex starts with "^" or ends with "$"'
+
+ adjusted_regex = '^%s$' % regex
+
+ try:
+ url_re = re.compile(adjusted_regex)
+ except re.error, e:
+ raise InvalidAppConfigError, 'regex invalid: %s' % e
+
+ match_tuple = (url_re, dispatcher, path, requires_login, admin_only)
+ self._url_patterns.append(match_tuple)
+
+ def Match(self,
+ relative_url,
+ split_url=SplitURL):
+ """Matches a URL from a request against the list of URL patterns.
+
+ The supplied relative_url may include the query string (i.e., the '?'
+ character and everything following).
+
+ Args:
+ relative_url: Relative URL being accessed in a request.
+
+ Returns:
+ Tuple (dispatcher, matched_path, requires_login, admin_only), which are
+ the corresponding values passed to AddURL when the matching URL pattern
+ was added to this matcher. The matched_path will have back-references
+ replaced using values matched by the URL pattern. If no match was found,
+ dispatcher will be None.
+ """
+ adjusted_url, query_string = split_url(relative_url)
+
+ for url_tuple in self._url_patterns:
+ url_re, dispatcher, path, requires_login, admin_only = url_tuple
+ the_match = url_re.match(adjusted_url)
+
+ if the_match:
+ adjusted_path = the_match.expand(path)
+ return dispatcher, adjusted_path, requires_login, admin_only
+
+ return None, None, None, None
+
+ def GetDispatchers(self):
+ """Retrieves the URLDispatcher objects that could be matched.
+
+ Should only be used in tests.
+
+ Returns:
+ A set of URLDispatcher objects.
+ """
+ return set([url_tuple[1] for url_tuple in self._url_patterns])
+
+
+class MatcherDispatcher(URLDispatcher):
+ """Dispatcher across multiple URLMatcher instances."""
+
+ def __init__(self,
+ login_url,
+ url_matchers,
+ get_user_info=dev_appserver_login.GetUserInfo,
+ login_redirect=dev_appserver_login.LoginRedirect):
+ """Initializer.
+
+ Args:
+ login_url: Relative URL which should be used for handling user logins.
+ url_matchers: Sequence of URLMatcher objects.
+ get_user_info, login_redirect: Used for dependency injection.
+ """
+ self._login_url = login_url
+ self._url_matchers = tuple(url_matchers)
+ self._get_user_info = get_user_info
+ self._login_redirect = login_redirect
+
+ def Dispatch(self,
+ relative_url,
+ path,
+ headers,
+ infile,
+ outfile,
+ base_env_dict=None):
+ """Dispatches a request to the first matching dispatcher.
+
+ Matchers are checked in the order they were supplied to the constructor.
+ If no matcher matches, a 404 error will be written to the outfile. The
+ path variable supplied to this method is ignored.
+ """
+ cookies = ', '.join(headers.getheaders('cookie'))
+ email, admin = self._get_user_info(cookies)
+
+ for matcher in self._url_matchers:
+ dispatcher, matched_path, requires_login, admin_only = matcher.Match(relative_url)
+ if dispatcher is None:
+ continue
+
+ logging.debug('Matched "%s" to %s with path %s',
+ relative_url, dispatcher, matched_path)
+
+ if (requires_login or admin_only) and not email:
+ logging.debug('Login required, redirecting user')
+ self._login_redirect(
+ self._login_url,
+ base_env_dict['SERVER_NAME'],
+ base_env_dict['SERVER_PORT'],
+ relative_url,
+ outfile)
+ elif admin_only and not admin:
+ outfile.write('Status: %d Not authorized\r\n'
+ '\r\n'
+ 'Current logged in user %s is not '
+ 'authorized to view this page.'
+ % (httplib.FORBIDDEN, email))
+ else:
+ dispatcher.Dispatch(relative_url,
+ matched_path,
+ headers,
+ infile,
+ outfile,
+ base_env_dict=base_env_dict)
+
+ return
+
+ outfile.write('Status: %d URL did not match\r\n'
+ '\r\n'
+ 'Not found error: %s did not match any patterns '
+ 'in application configuration.'
+ % (httplib.NOT_FOUND, relative_url))
+
+
+class ApplicationLoggingHandler(logging.Handler):
+ """Python Logging handler that displays the debugging console to users."""
+
+ _COOKIE_NAME = '_ah_severity'
+
+ _TEMPLATES_INITIALIZED = False
+ _HEADER = None
+ _SCRIPT = None
+ _MIDDLE = None
+ _FOOTER = None
+
+ @staticmethod
+ def InitializeTemplates(header, script, middle, footer):
+ """Initializes the templates used to render the debugging console.
+
+ This method must be called before any ApplicationLoggingHandler instances
+ are created.
+
+ Args:
+ header: The header template that is printed first.
+ script: The script template that is printed after the logging messages.
+ middle: The middle element that's printed before the footer.
+ footer; The last element that's printed at the end of the document.
+ """
+ ApplicationLoggingHandler._HEADER = header
+ ApplicationLoggingHandler._SCRIPT = script
+ ApplicationLoggingHandler._MIDDLE = middle
+ ApplicationLoggingHandler._FOOTER = footer
+ ApplicationLoggingHandler._TEMPLATES_INITIALIZED = True
+
+ @staticmethod
+ def AreTemplatesInitialized():
+ """Returns True if InitializeTemplates has been called, False otherwise."""
+ return ApplicationLoggingHandler._TEMPLATES_INITIALIZED
+
+ def __init__(self, *args, **kwargs):
+ """Initializer.
+
+ Args:
+ args, kwargs: See logging.Handler.
+
+ Raises:
+ TemplatesNotLoadedError exception if the InitializeTemplates method was
+ not called before creating this instance.
+ """
+ if not self._TEMPLATES_INITIALIZED:
+ raise TemplatesNotLoadedError
+
+ logging.Handler.__init__(self, *args, **kwargs)
+ self._record_list = []
+ self._start_time = time.time()
+
+ def emit(self, record):
+ """Called by the logging module each time the application logs a message.
+
+ Args:
+ record: logging.LogRecord instance corresponding to the newly logged
+ message.
+ """
+ self._record_list.append(record)
+
+ def AddDebuggingConsole(self, relative_url, env, outfile):
+ """Prints an HTML debugging console to an output stream, if requested.
+
+ Args:
+ relative_url: Relative URL that was accessed, including the query string.
+ Used to determine if the parameter 'debug' was supplied, in which case
+ the console will be shown.
+ env: Dictionary containing CGI environment variables. Checks for the
+ HTTP_COOKIE entry to see if the accessing user has any logging-related
+ cookies set.
+ outfile: Output stream to which the console should be written if either
+ a debug parameter was supplied or a logging cookie is present.
+ """
+ script_name, query_string = SplitURL(relative_url)
+ param_dict = cgi.parse_qs(query_string, True)
+ cookie_dict = Cookie.SimpleCookie(env.get('HTTP_COOKIE', ''))
+ if 'debug' not in param_dict and self._COOKIE_NAME not in cookie_dict:
+ return
+
+ outfile.write(self._HEADER)
+ for record in self._record_list:
+ self._PrintRecord(record, outfile)
+
+ outfile.write(self._MIDDLE)
+ outfile.write(self._SCRIPT)
+ outfile.write(self._FOOTER)
+
+ def _PrintRecord(self, record, outfile):
+ """Prints a single logging record to an output stream.
+
+ Args:
+ record: logging.LogRecord instance to print.
+ outfile: Output stream to which the LogRecord should be printed.
+ """
+ message = cgi.escape(record.getMessage())
+ level_name = logging.getLevelName(record.levelno).lower()
+ level_letter = level_name[:1].upper()
+ time_diff = record.created - self._start_time
+ outfile.write('<span class="_ah_logline_%s">\n' % level_name)
+ outfile.write('<span class="_ah_logline_%s_prefix">%2.5f %s ></span>\n'
+ % (level_name, time_diff, level_letter))
+ outfile.write('%s\n' % message)
+ outfile.write('</span>\n')
+
+
+_IGNORE_HEADERS = frozenset(['content-type', 'content-length'])
+
+def SetupEnvironment(cgi_path,
+ relative_url,
+ headers,
+ split_url=SplitURL,
+ get_user_info=dev_appserver_login.GetUserInfo):
+ """Sets up environment variables for a CGI.
+
+ Args:
+ cgi_path: Full file-system path to the CGI being executed.
+ relative_url: Relative URL used to access the CGI.
+ headers: Instance of mimetools.Message containing request headers.
+ split_url, get_user_info: Used for dependency injection.
+
+ Returns:
+ Dictionary containing CGI environment variables.
+ """
+ env = DEFAULT_ENV.copy()
+
+ script_name, query_string = split_url(relative_url)
+
+ env['SCRIPT_NAME'] = ''
+ env['QUERY_STRING'] = query_string
+ env['PATH_INFO'] = urllib.unquote(script_name)
+ env['PATH_TRANSLATED'] = cgi_path
+ env['CONTENT_TYPE'] = headers.getheader('content-type',
+ 'application/x-www-form-urlencoded')
+ env['CONTENT_LENGTH'] = headers.getheader('content-length', '')
+
+ cookies = ', '.join(headers.getheaders('cookie'))
+ email, admin = get_user_info(cookies)
+ env['USER_EMAIL'] = email
+ if admin:
+ env['USER_IS_ADMIN'] = '1'
+
+ for key in headers:
+ if key in _IGNORE_HEADERS:
+ continue
+ adjusted_name = key.replace('-', '_').upper()
+ env['HTTP_' + adjusted_name] = ', '.join(headers.getheaders(key))
+
+ return env
+
+
+def FakeTemporaryFile(*args, **kwargs):
+ """Fake for tempfile.TemporaryFile that just uses StringIO."""
+ return cStringIO.StringIO()
+
+
+def NotImplementedFake(*args, **kwargs):
+ """Fake for methods/classes that are not implemented in the production
+ environment.
+ """
+ raise NotImplementedError("This class/method is not available.")
+
+
+def IsEncodingsModule(module_name):
+ """Determines if the supplied module is related to encodings in any way.
+
+ Encodings-related modules cannot be reloaded, so they need to be treated
+ specially when sys.modules is modified in any way.
+
+ Args:
+ module_name: Absolute name of the module regardless of how it is imported
+ into the local namespace (e.g., foo.bar.baz).
+
+ Returns:
+ True if it's an encodings-related module; False otherwise.
+ """
+ if (module_name in ('codecs', 'encodings') or
+ module_name.startswith('encodings.')):
+ return True
+ return False
+
+
+def ClearAllButEncodingsModules(module_dict):
+ """Clear all modules in a module dictionary except for those modules that
+ are in any way related to encodings.
+
+ Args:
+ module_dict: Dictionary in the form used by sys.modules.
+ """
+ for module_name in module_dict.keys():
+ if not IsEncodingsModule(module_name):
+ del module_dict[module_name]
+
+
+def FakeURandom(n):
+ """Fake version of os.urandom."""
+ bytes = ''
+ for i in xrange(n):
+ bytes += chr(random.randint(0, 255))
+ return bytes
+
+
+def FakeUname():
+ """Fake version of os.uname."""
+ return ('Linux', '', '', '', '')
+
+
+def IsPathInSubdirectories(filename,
+ subdirectories,
+ normcase=os.path.normcase):
+ """Determines if a filename is contained within one of a set of directories.
+
+ Args:
+ filename: Path of the file (relative or absolute).
+ subdirectories: Iterable collection of paths to subdirectories which the
+ given filename may be under.
+ normcase: Used for dependency injection.
+
+ Returns:
+ True if the supplied filename is in one of the given sub-directories or
+ its hierarchy of children. False otherwise.
+ """
+ file_dir = normcase(os.path.dirname(os.path.abspath(filename)))
+ for parent in subdirectories:
+ fixed_parent = normcase(os.path.abspath(parent))
+ if os.path.commonprefix([file_dir, fixed_parent]) == fixed_parent:
+ return True
+ return False
+
+SHARED_MODULE_PREFIXES = set([
+ 'google',
+ 'logging',
+ 'sys',
+ 'warnings',
+
+
+
+
+ 're',
+ 'sre_compile',
+ 'sre_constants',
+ 'sre_parse',
+
+
+
+
+ 'wsgiref',
+])
+
+NOT_SHARED_MODULE_PREFIXES = set([
+ 'google.appengine.ext',
+])
+
+
+def ModuleNameHasPrefix(module_name, prefix_set):
+ """Determines if a module's name belongs to a set of prefix strings.
+
+ Args:
+ module_name: String containing the fully qualified module name.
+ prefix_set: Iterable set of module name prefixes to check against.
+
+ Returns:
+ True if the module_name belongs to the prefix set or is a submodule of
+ any of the modules specified in the prefix_set. Otherwise False.
+ """
+ for prefix in prefix_set:
+ if prefix == module_name:
+ return True
+
+ if module_name.startswith(prefix + '.'):
+ return True
+
+ return False
+
+
+def SetupSharedModules(module_dict):
+ """Creates a module dictionary for the hardened part of the process.
+
+ Module dictionary will contain modules that should be shared between the
+ hardened and unhardened parts of the process.
+
+ Args:
+ module_dict: Module dictionary from which existing modules should be
+ pulled (usually sys.modules).
+
+ Returns:
+ A new module dictionary.
+ """
+ output_dict = {}
+ for module_name, module in module_dict.iteritems():
+ if module is None:
+ continue
+
+ if IsEncodingsModule(module_name):
+ output_dict[module_name] = module
+ continue
+
+ shared_prefix = ModuleNameHasPrefix(module_name, SHARED_MODULE_PREFIXES)
+ banned_prefix = ModuleNameHasPrefix(module_name, NOT_SHARED_MODULE_PREFIXES)
+
+ if shared_prefix and not banned_prefix:
+ output_dict[module_name] = module
+
+ return output_dict
+
+
+class FakeFile(file):
+ """File sub-class that enforces the security restrictions of the production
+ environment.
+ """
+
+ ALLOWED_MODES = frozenset(['r', 'rb', 'U', 'rU'])
+
+ ALLOWED_FILES = set(os.path.normcase(filename)
+ for filename in mimetypes.knownfiles
+ if os.path.isfile(filename))
+
+ ALLOWED_DIRS = set([
+ os.path.normcase(os.path.abspath(os.path.dirname(os.__file__)))
+ ])
+
+ NOT_ALLOWED_DIRS = set([
+
+
+
+
+ os.path.normcase(os.path.join(os.path.dirname(os.__file__),
+ 'site-packages'))
+ ])
+
+ ALLOWED_SITE_PACKAGE_DIRS = set(
+ os.path.normcase(os.path.abspath(os.path.join(
+ os.path.dirname(os.__file__), 'site-packages', path)))
+ for path in [
+
+ ])
+
+ _application_paths = None
+ _original_file = file
+
+ @staticmethod
+ def SetAllowedPaths(application_paths):
+ """Sets the root path of the application that is currently running.
+
+ Must be called at least once before any file objects are created in the
+ hardened environment.
+
+ Args:
+ root_path: Path to the root of the application.
+ """
+ FakeFile._application_paths = set(os.path.abspath(path)
+ for path in application_paths)
+
+ @staticmethod
+ def IsFileAccessible(filename, normcase=os.path.normcase):
+ """Determines if a file's path is accessible.
+
+ SetAllowedPaths() must be called before this method or else all file
+ accesses will raise an error.
+
+ Args:
+ filename: Path of the file to check (relative or absolute). May be a
+ directory, in which case access for files inside that directory will
+ be checked.
+ normcase: Used for dependency injection.
+
+ Returns:
+ True if the file is accessible, False otherwise.
+ """
+ logical_filename = normcase(os.path.abspath(filename))
+
+ if os.path.isdir(logical_filename):
+ logical_filename = os.path.join(logical_filename, 'foo')
+
+ if logical_filename in FakeFile.ALLOWED_FILES:
+ return True
+
+ if IsPathInSubdirectories(logical_filename,
+ FakeFile.ALLOWED_SITE_PACKAGE_DIRS,
+ normcase=normcase):
+ return True
+
+ allowed_dirs = FakeFile._application_paths | FakeFile.ALLOWED_DIRS
+ if (IsPathInSubdirectories(logical_filename,
+ allowed_dirs,
+ normcase=normcase) and
+ not IsPathInSubdirectories(logical_filename,
+ FakeFile.NOT_ALLOWED_DIRS,
+ normcase=normcase)):
+ return True
+
+ return False
+
+ def __init__(self, filename, mode='r', **kwargs):
+ """Initializer. See file built-in documentation."""
+ if mode not in FakeFile.ALLOWED_MODES:
+ raise IOError('invalid mode: %s' % mode)
+
+ if not FakeFile.IsFileAccessible(filename):
+ raise IOError(errno.EACCES, 'file not accessible')
+
+ super(FakeFile, self).__init__(filename, mode, **kwargs)
+
+
+class RestrictedPathFunction(object):
+ """Enforces access restrictions for functions that have a file or
+ directory path as their first argument."""
+
+ _original_os = os
+
+ def __init__(self, original_func):
+ """Initializer.
+
+ Args:
+ original_func: Callable that takes as its first argument the path to a
+ file or directory on disk; all subsequent arguments may be variable.
+ """
+ self._original_func = original_func
+
+ def __call__(self, path, *args, **kwargs):
+ """Enforces access permissions for the function passed to the constructor.
+ """
+ if not FakeFile.IsFileAccessible(path):
+ raise OSError(errno.EACCES, 'path not accessible')
+
+ return self._original_func(path, *args, **kwargs)
+
+
+def GetSubmoduleName(fullname):
+ """Determines the leaf submodule name of a full module name.
+
+ Args:
+ fullname: Fully qualified module name, e.g. 'foo.bar.baz'
+
+ Returns:
+ Submodule name, e.g. 'baz'. If the supplied module has no submodule (e.g.,
+ 'stuff'), the returned value will just be that module name ('stuff').
+ """
+ return fullname.rsplit('.', 1)[-1]
+
+
+class CouldNotFindModuleError(ImportError):
+ """Raised when a module could not be found.
+
+ In contrast to when a module has been found, but cannot be loaded because of
+ hardening restrictions.
+ """
+
+
+def Trace(func):
+ """Decorator that logs the call stack of the HardenedModulesHook class as
+ it executes, indenting logging messages based on the current stack depth.
+ """
+ def decorate(self, *args, **kwargs):
+ args_to_show = []
+ if args is not None:
+ args_to_show.extend(str(argument) for argument in args)
+ if kwargs is not None:
+ args_to_show.extend('%s=%s' % (key, value)
+ for key, value in kwargs.iteritems())
+
+ args_string = ', '.join(args_to_show)
+
+ self.log('Entering %s(%s)', func.func_name, args_string)
+ self._indent_level += 1
+ try:
+ return func(self, *args, **kwargs)
+ finally:
+ self._indent_level -= 1
+ self.log('Exiting %s(%s)', func.func_name, args_string)
+
+ return decorate
+
+
+class HardenedModulesHook(object):
+ """Meta import hook that restricts the modules used by applications to match
+ the production environment.
+
+ Module controls supported:
+ - Disallow native/extension modules from being loaded
+ - Disallow built-in and/or Python-distributed modules from being loaded
+ - Replace modules with completely empty modules
+ - Override specific module attributes
+ - Replace one module with another
+
+ After creation, this object should be added to the front of the sys.meta_path
+ list (which may need to be created). The sys.path_importer_cache dictionary
+ should also be cleared, to prevent loading any non-restricted modules.
+
+ See PEP302 for more info on how this works:
+ http://www.python.org/dev/peps/pep-0302/
+ """
+
+ ENABLE_LOGGING = False
+
+ def log(self, message, *args):
+ """Logs an import-related message to stderr, with indentation based on
+ current call-stack depth.
+
+ Args:
+ message: Logging format string.
+ args: Positional format parameters for the logging message.
+ """
+ if HardenedModulesHook.ENABLE_LOGGING:
+ indent = self._indent_level * ' '
+ print >>sys.stderr, indent + (message % args)
+
+ EMPTY_MODULE_FILE = '<empty module>'
+
+ _WHITE_LIST_C_MODULES = [
+ 'array',
+ 'binascii',
+ 'bz2',
+ 'cmath',
+ 'collections',
+ 'crypt',
+ 'cStringIO',
+ 'datetime',
+ 'errno',
+ 'exceptions',
+ 'gc',
+ 'itertools',
+ 'math',
+ 'md5',
+ 'operator',
+ 'posix',
+ 'posixpath',
+ 'pyexpat',
+ 'sha',
+ 'struct',
+ 'sys',
+ 'time',
+ 'timing',
+ 'unicodedata',
+ 'zlib',
+ '_bisect',
+ '_codecs',
+ '_codecs_cn',
+ '_codecs_hk',
+ '_codecs_iso2022',
+ '_codecs_jp',
+ '_codecs_kr',
+ '_codecs_tw',
+ '_csv',
+ '_elementtree',
+ '_functools',
+ '_hashlib',
+ '_heapq',
+ '_locale',
+ '_lsprof',
+ '_md5',
+ '_multibytecodec',
+ '_random',
+ '_sha',
+ '_sha256',
+ '_sha512',
+ '_sre',
+ '_struct',
+ '_types',
+ '_weakref',
+ '__main__',
+ ]
+
+ _WHITE_LIST_PARTIAL_MODULES = {
+ 'gc': [
+ 'enable',
+ 'disable',
+ 'isenabled',
+ 'collect',
+ 'get_debug',
+ 'set_threshold',
+ 'get_threshold',
+ 'get_count'
+ ],
+
+
+
+ 'os': [
+ 'altsep',
+ 'curdir',
+ 'defpath',
+ 'devnull',
+ 'environ',
+ 'error',
+ 'extsep',
+ 'EX_NOHOST',
+ 'EX_NOINPUT',
+ 'EX_NOPERM',
+ 'EX_NOUSER',
+ 'EX_OK',
+ 'EX_OSERR',
+ 'EX_OSFILE',
+ 'EX_PROTOCOL',
+ 'EX_SOFTWARE',
+ 'EX_TEMPFAIL',
+ 'EX_UNAVAILABLE',
+ 'EX_USAGE',
+ 'F_OK',
+ 'getcwd',
+ 'getcwdu',
+ 'getenv',
+ 'listdir',
+ 'lstat',
+ 'name',
+ 'NGROUPS_MAX',
+ 'O_APPEND',
+ 'O_CREAT',
+ 'O_DIRECT',
+ 'O_DIRECTORY',
+ 'O_DSYNC',
+ 'O_EXCL',
+ 'O_LARGEFILE',
+ 'O_NDELAY',
+ 'O_NOCTTY',
+ 'O_NOFOLLOW',
+ 'O_NONBLOCK',
+ 'O_RDONLY',
+ 'O_RDWR',
+ 'O_RSYNC',
+ 'O_SYNC',
+ 'O_TRUNC',
+ 'O_WRONLY',
+ 'pardir',
+ 'path',
+ 'pathsep',
+ 'R_OK',
+ 'SEEK_CUR',
+ 'SEEK_END',
+ 'SEEK_SET',
+ 'sep',
+ 'stat',
+ 'stat_float_times',
+ 'stat_result',
+ 'strerror',
+ 'TMP_MAX',
+ 'urandom',
+ 'walk',
+ 'WCOREDUMP',
+ 'WEXITSTATUS',
+ 'WIFEXITED',
+ 'WIFSIGNALED',
+ 'WIFSTOPPED',
+ 'WNOHANG',
+ 'WSTOPSIG',
+ 'WTERMSIG',
+ 'WUNTRACED',
+ 'W_OK',
+ 'X_OK',
+ ],
+ }
+
+ _EMPTY_MODULES = [
+ 'imp',
+ 'ftplib',
+ 'select',
+ 'socket',
+ 'tempfile',
+ ]
+
+ _MODULE_OVERRIDES = {
+ 'os': {
+ 'listdir': RestrictedPathFunction(os.listdir),
+ 'lstat': RestrictedPathFunction(os.lstat),
+ 'stat': RestrictedPathFunction(os.stat),
+ 'uname': FakeUname,
+ 'urandom': FakeURandom,
+ },
+
+ 'socket': {
+ 'AF_INET': None,
+ 'SOCK_STREAM': None,
+ 'SOCK_DGRAM': None,
+ },
+
+ 'tempfile': {
+ 'TemporaryFile': FakeTemporaryFile,
+ 'gettempdir': NotImplementedFake,
+ 'gettempprefix': NotImplementedFake,
+ 'mkdtemp': NotImplementedFake,
+ 'mkstemp': NotImplementedFake,
+ 'mktemp': NotImplementedFake,
+ 'NamedTemporaryFile': NotImplementedFake,
+ 'tempdir': NotImplementedFake,
+ },
+ }
+
+ _ENABLED_FILE_TYPES = (
+ imp.PKG_DIRECTORY,
+ imp.PY_SOURCE,
+ imp.PY_COMPILED,
+ imp.C_BUILTIN,
+ )
+
+ def __init__(self,
+ module_dict,
+ imp_module=imp,
+ os_module=os,
+ dummy_thread_module=dummy_thread,
+ pickle_module=pickle):
+ """Initializer.
+
+ Args:
+ module_dict: Module dictionary to use for managing system modules.
+ Should be sys.modules.
+ imp_module, os_module, dummy_thread_module, pickle_module: References to
+ modules that exist in the dev_appserver that must be used by this class
+ in order to function, even if these modules have been unloaded from
+ sys.modules.
+ """
+ self._module_dict = module_dict
+ self._imp = imp_module
+ self._os = os_module
+ self._dummy_thread = dummy_thread_module
+ self._pickle = pickle
+ self._indent_level = 0
+
+ @Trace
+ def find_module(self, fullname, path=None):
+ """See PEP 302."""
+ if (fullname in ('cPickle', 'thread') or
+ fullname in HardenedModulesHook._EMPTY_MODULES):
+ return self
+
+ search_path = path
+ all_modules = fullname.split('.')
+ try:
+ for index, current_module in enumerate(all_modules):
+ current_module_fullname = '.'.join(all_modules[:index + 1])
+ if current_module_fullname == fullname:
+ self.FindModuleRestricted(current_module,
+ current_module_fullname,
+ search_path)
+ else:
+ if current_module_fullname in self._module_dict:
+ module = self._module_dict[current_module_fullname]
+ else:
+ module = self.FindAndLoadModule(current_module,
+ current_module_fullname,
+ search_path)
+
+ if hasattr(module, '__path__'):
+ search_path = module.__path__
+ except CouldNotFindModuleError:
+ return None
+
+ return self
+
+ @Trace
+ def FixModule(self, module):
+ """Prunes and overrides restricted module attributes.
+
+ Args:
+ module: The module to prune. This should be a new module whose attributes
+ reference back to the real module's __dict__ members.
+ """
+ if module.__name__ in self._WHITE_LIST_PARTIAL_MODULES:
+ allowed_symbols = self._WHITE_LIST_PARTIAL_MODULES[module.__name__]
+ for symbol in set(module.__dict__) - set(allowed_symbols):
+ if not (symbol.startswith('__') and symbol.endswith('__')):
+ del module.__dict__[symbol]
+
+ if module.__name__ in self._MODULE_OVERRIDES:
+ module.__dict__.update(self._MODULE_OVERRIDES[module.__name__])
+
+ @Trace
+ def FindModuleRestricted(self,
+ submodule,
+ submodule_fullname,
+ search_path):
+ """Locates a module while enforcing module import restrictions.
+
+ Args:
+ submodule: The short name of the submodule (i.e., the last section of
+ the fullname; for 'foo.bar' this would be 'bar').
+ submodule_fullname: The fully qualified name of the module to find (e.g.,
+ 'foo.bar').
+ search_path: List of paths to search for to find this module. Should be
+ None if the current sys.path should be used.
+
+ Returns:
+ Tuple (source_file, pathname, description) where:
+ source_file: File-like object that contains the module; in the case
+ of packages, this will be None, which implies to look at __init__.py.
+ pathname: String containing the full path of the module on disk.
+ description: Tuple returned by imp.find_module().
+
+ Raises:
+ ImportError exception if the requested module was found, but importing
+ it is disallowed.
+
+ CouldNotFindModuleError exception if the request module could not even
+ be found for import.
+ """
+ try:
+ source_file, pathname, description = self._imp.find_module(submodule, search_path)
+ except ImportError:
+ self.log('Could not find module "%s"', submodule_fullname)
+ raise CouldNotFindModuleError()
+
+ suffix, mode, file_type = description
+
+ if (file_type not in (self._imp.C_BUILTIN, self._imp.C_EXTENSION) and
+ not FakeFile.IsFileAccessible(pathname)):
+ error_message = 'Access to module file denied: %s' % pathname
+ logging.debug(error_message)
+ raise ImportError(error_message)
+
+ if (file_type not in self._ENABLED_FILE_TYPES and
+ submodule not in self._WHITE_LIST_C_MODULES):
+ error_message = ('Could not import "%s": Disallowed C-extension '
+ 'or built-in module' % submodule_fullname)
+ logging.debug(error_message)
+ raise ImportError(error_message)
+
+ return source_file, pathname, description
+
+ @Trace
+ def LoadModuleRestricted(self,
+ submodule_fullname,
+ source_file,
+ pathname,
+ description):
+ """Loads a module while enforcing module import restrictions.
+
+ As a byproduct, the new module will be added to the module dictionary.
+
+ Args:
+ submodule_fullname: The fully qualified name of the module to find (e.g.,
+ 'foo.bar').
+ source_file: File-like object that contains the module's source code.
+ pathname: String containing the full path of the module on disk.
+ description: Tuple returned by imp.find_module().
+
+ Returns:
+ The new module.
+
+ Raises:
+ ImportError exception of the specified module could not be loaded for
+ whatever reason.
+ """
+ try:
+ try:
+ return self._imp.load_module(submodule_fullname,
+ source_file,
+ pathname,
+ description)
+ except:
+ if submodule_fullname in self._module_dict:
+ del self._module_dict[submodule_fullname]
+ raise
+
+ finally:
+ if source_file is not None:
+ source_file.close()
+
+ @Trace
+ def FindAndLoadModule(self,
+ submodule,
+ submodule_fullname,
+ search_path):
+ """Finds and loads a module, loads it, and adds it to the module dictionary.
+
+ Args:
+ submodule: Name of the module to import (e.g., baz).
+ submodule_fullname: Full name of the module to import (e.g., foo.bar.baz).
+ search_path: Path to use for searching for this submodule. For top-level
+ modules this should be None; otherwise it should be the __path__
+ attribute from the parent package.
+
+ Returns:
+ A new module instance that has been inserted into the module dictionary
+ supplied to __init__.
+
+ Raises:
+ ImportError exception if the module could not be loaded for whatever
+ reason (e.g., missing, not allowed).
+ """
+ module = self._imp.new_module(submodule_fullname)
+
+ if submodule_fullname in self._EMPTY_MODULES:
+ module.__file__ = self.EMPTY_MODULE_FILE
+ elif submodule_fullname == 'thread':
+ module.__dict__.update(self._dummy_thread.__dict__)
+ module.__name__ = 'thread'
+ elif submodule_fullname == 'cPickle':
+ module.__dict__.update(self._pickle.__dict__)
+ module.__name__ = 'cPickle'
+ elif submodule_fullname == 'os':
+ module.__dict__.update(self._os.__dict__)
+ self._module_dict['os.path'] = module.path
+ else:
+ source_file, pathname, description = self.FindModuleRestricted(submodule, submodule_fullname, search_path)
+ module = self.LoadModuleRestricted(submodule_fullname,
+ source_file,
+ pathname,
+ description)
+
+ module.__loader__ = self
+ self.FixModule(module)
+ if submodule_fullname not in self._module_dict:
+ self._module_dict[submodule_fullname] = module
+
+ return module
+
+ @Trace
+ def GetParentPackage(self, fullname):
+ """Retrieves the parent package of a fully qualified module name.
+
+ Args:
+ fullname: Full name of the module whose parent should be retrieved (e.g.,
+ foo.bar).
+
+ Returns:
+ Module instance for the parent or None if there is no parent module.
+
+ Raise:
+ ImportError exception if the module's parent could not be found.
+ """
+ all_modules = fullname.split('.')
+ parent_module_fullname = '.'.join(all_modules[:-1])
+ if parent_module_fullname:
+ if self.find_module(fullname) is None:
+ raise ImportError('Could not find module %s' % fullname)
+
+ return self._module_dict[parent_module_fullname]
+ return None
+
+ @Trace
+ def GetParentSearchPath(self, fullname):
+ """Determines the search path of a module's parent package.
+
+ Args:
+ fullname: Full name of the module to look up (e.g., foo.bar).
+
+ Returns:
+ Tuple (submodule, search_path) where:
+ submodule: The last portion of the module name from fullname (e.g.,
+ if fullname is foo.bar, then this is bar).
+ search_path: List of paths that belong to the parent package's search
+ path or None if there is no parent package.
+
+ Raises:
+ ImportError exception if the module or its parent could not be found.
+ """
+ submodule = GetSubmoduleName(fullname)
+ parent_package = self.GetParentPackage(fullname)
+ search_path = None
+ if parent_package is not None and hasattr(parent_package, '__path__'):
+ search_path = parent_package.__path__
+ return submodule, search_path
+
+ @Trace
+ def GetModuleInfo(self, fullname):
+ """Determines the path on disk and the search path of a module or package.
+
+ Args:
+ fullname: Full name of the module to look up (e.g., foo.bar).
+
+ Returns:
+ Tuple (pathname, search_path, submodule) where:
+ pathname: String containing the full path of the module on disk.
+ search_path: List of paths that belong to the found package's search
+ path or None if found module is not a package.
+ submodule: The relative name of the submodule that's being imported.
+ """
+ submodule, search_path = self.GetParentSearchPath(fullname)
+ source_file, pathname, description = self.FindModuleRestricted(submodule, fullname, search_path)
+ suffix, mode, file_type = description
+ module_search_path = None
+ if file_type == self._imp.PKG_DIRECTORY:
+ module_search_path = [pathname]
+ pathname = os.path.join(pathname, '__init__%spy' % os.extsep)
+ return pathname, module_search_path, submodule
+
+ @Trace
+ def load_module(self, fullname):
+ """See PEP 302."""
+ all_modules = fullname.split('.')
+ submodule = all_modules[-1]
+ parent_module_fullname = '.'.join(all_modules[:-1])
+ search_path = None
+ if parent_module_fullname and parent_module_fullname in self._module_dict:
+ parent_module = self._module_dict[parent_module_fullname]
+ if hasattr(parent_module, '__path__'):
+ search_path = parent_module.__path__
+
+ return self.FindAndLoadModule(submodule, fullname, search_path)
+
+ @Trace
+ def is_package(self, fullname):
+ """See PEP 302 extensions."""
+ submodule, search_path = self.GetParentSearchPath(fullname)
+ source_file, pathname, description = self.FindModuleRestricted(submodule, fullname, search_path)
+ suffix, mode, file_type = description
+ if file_type == self._imp.PKG_DIRECTORY:
+ return True
+ return False
+
+ @Trace
+ def get_source(self, fullname):
+ """See PEP 302 extensions."""
+ full_path, search_path, submodule = self.GetModuleInfo(fullname)
+ source_file = open(full_path)
+ try:
+ return source_file.read()
+ finally:
+ source_file.close()
+
+ @Trace
+ def get_code(self, fullname):
+ """See PEP 302 extensions."""
+ full_path, search_path, submodule = self.GetModuleInfo(fullname)
+ source_file = open(full_path)
+ try:
+ source_code = source_file.read()
+ finally:
+ source_file.close()
+
+ source_code = source_code.replace('\r\n', '\n')
+ if not source_code.endswith('\n'):
+ source_code += '\n'
+
+ return compile(source_code, full_path, 'exec')
+
+
+def ModuleHasValidMainFunction(module):
+ """Determines if a module has a main function that takes no arguments.
+
+ This includes functions that have arguments with defaults that are all
+ assigned, thus requiring no additional arguments in order to be called.
+
+ Args:
+ module: A types.ModuleType instance.
+
+ Returns:
+ True if the module has a valid, reusable main function; False otherwise.
+ """
+ if hasattr(module, 'main') and type(module.main) is types.FunctionType:
+ arg_names, var_args, var_kwargs, default_values = inspect.getargspec(module.main)
+ if len(arg_names) == 0:
+ return True
+ if default_values is not None and len(arg_names) == len(default_values):
+ return True
+ return False
+
+
+def GetScriptModuleName(handler_path):
+ """Determines the fully-qualified Python module name of a script on disk.
+
+ Args:
+ handler_path: CGI path stored in the application configuration (as a path
+ like 'foo/bar/baz.py'). May contain $PYTHON_LIB references.
+
+ Returns:
+ String containing the corresponding module name (e.g., 'foo.bar.baz').
+ """
+ if handler_path.startswith(PYTHON_LIB_VAR + '/'):
+ handler_path = handler_path[len(PYTHON_LIB_VAR):]
+ handler_path = os.path.normpath(handler_path)
+
+ extension_index = handler_path.rfind('.py')
+ if extension_index != -1:
+ handler_path = handler_path[:extension_index]
+ module_fullname = handler_path.replace(os.sep, '.')
+ module_fullname = module_fullname.strip('.')
+ module_fullname = re.sub('\.+', '.', module_fullname)
+
+ if module_fullname.endswith('.__init__'):
+ module_fullname = module_fullname[:-len('.__init__')]
+
+ return module_fullname
+
+
+def FindMissingInitFiles(cgi_path, module_fullname, isfile=os.path.isfile):
+ """Determines which __init__.py files are missing from a module's parent
+ packages.
+
+ Args:
+ cgi_path: Absolute path of the CGI module file on disk.
+ module_fullname: Fully qualified Python module name used to import the
+ cgi_path module.
+
+ Returns:
+ List containing the paths to the missing __init__.py files.
+ """
+ missing_init_files = []
+
+ if cgi_path.endswith('.py'):
+ module_base = os.path.dirname(cgi_path)
+ else:
+ module_base = cgi_path
+
+ depth_count = module_fullname.count('.')
+ if cgi_path.endswith('__init__.py') or not cgi_path.endswith('.py'):
+ depth_count += 1
+
+ for index in xrange(depth_count):
+ current_init_file = os.path.join(module_base, '__init__.py')
+
+ if not isfile(current_init_file):
+ missing_init_files.append(current_init_file)
+
+ module_base = os.path.abspath(os.path.join(module_base, os.pardir))
+
+ return missing_init_files
+
+
+def LoadTargetModule(handler_path,
+ cgi_path,
+ import_hook,
+ module_dict=sys.modules):
+ """Loads a target CGI script by importing it as a Python module.
+
+ If the module for the target CGI script has already been loaded before,
+ the new module will be loaded in its place using the same module object,
+ possibly overwriting existing module attributes.
+
+ Args:
+ handler_path: CGI path stored in the application configuration (as a path
+ like 'foo/bar/baz.py'). Should not have $PYTHON_LIB references.
+ cgi_path: Absolute path to the CGI script file on disk.
+ import_hook: Instance of HardenedModulesHook to use for module loading.
+ module_dict: Used for dependency injection.
+
+ Returns:
+ Tuple (module_fullname, script_module, module_code) where:
+ module_fullname: Fully qualified module name used to import the script.
+ script_module: The ModuleType object corresponding to the module_fullname.
+ If the module has not already been loaded, this will be an empty
+ shell of a module.
+ module_code: Code object (returned by compile built-in) corresponding
+ to the cgi_path to run. If the script_module was previously loaded
+ and has a main() function that can be reused, this will be None.
+ """
+ module_fullname = GetScriptModuleName(handler_path)
+ script_module = module_dict.get(module_fullname)
+ module_code = None
+ if script_module != None and ModuleHasValidMainFunction(script_module):
+ logging.debug('Reusing main() function of module "%s"', module_fullname)
+ else:
+ if script_module is None:
+ script_module = imp.new_module(module_fullname)
+ script_module.__loader__ = import_hook
+
+ try:
+ module_code = import_hook.get_code(module_fullname)
+ full_path, search_path, submodule = import_hook.GetModuleInfo(module_fullname)
+ script_module.__file__ = full_path
+ if search_path is not None:
+ script_module.__path__ = search_path
+ except:
+ exc_type, exc_value, exc_tb = sys.exc_info()
+ import_error_message = str(exc_type)
+ if exc_value:
+ import_error_message += ': ' + str(exc_value)
+
+ logging.error('Encountered error loading module "%s": %s',
+ module_fullname, import_error_message)
+ missing_inits = FindMissingInitFiles(cgi_path, module_fullname)
+ if missing_inits:
+ logging.warning('Missing package initialization files: %s',
+ ', '.join(missing_inits))
+ else:
+ logging.error('Parent package initialization files are present, '
+ 'but must be broken')
+
+ independent_load_successful = True
+
+ if not os.path.isfile(cgi_path):
+ independent_load_successful = False
+ else:
+ try:
+ source_file = open(cgi_path)
+ try:
+ module_code = compile(source_file.read(), cgi_path, 'exec')
+ script_module.__file__ = cgi_path
+ finally:
+ source_file.close()
+
+ except OSError:
+ independent_load_successful = False
+
+ if not independent_load_successful:
+ raise exc_type, exc_value, exc_tb
+
+ module_dict[module_fullname] = script_module
+
+ return module_fullname, script_module, module_code
+
+
+def ExecuteOrImportScript(handler_path, cgi_path, import_hook):
+ """Executes a CGI script by importing it as a new module; possibly reuses
+ the module's main() function if it is defined and takes no arguments.
+
+ Basic technique lifted from PEP 338 and Python2.5's runpy module. See:
+ http://www.python.org/dev/peps/pep-0338/
+
+ See the section entitled "Import Statements and the Main Module" to understand
+ why a module named '__main__' cannot do relative imports. To get around this,
+ the requested module's path could be added to sys.path on each request.
+
+ Args:
+ handler_path: CGI path stored in the application configuration (as a path
+ like 'foo/bar/baz.py'). Should not have $PYTHON_LIB references.
+ cgi_path: Absolute path to the CGI script file on disk.
+ import_hook: Instance of HardenedModulesHook to use for module loading.
+
+ Returns:
+ True if the response code had an error status (e.g., 404), or False if it
+ did not.
+
+ Raises:
+ Any kind of exception that could have been raised when loading the target
+ module, running a target script, or executing the application code itself.
+ """
+ module_fullname, script_module, module_code = LoadTargetModule(
+ handler_path, cgi_path, import_hook)
+ script_module.__name__ = '__main__'
+ sys.modules['__main__'] = script_module
+ try:
+ if module_code:
+ exec module_code in script_module.__dict__
+ else:
+ script_module.main()
+
+ sys.stdout.flush()
+ sys.stdout.seek(0)
+ try:
+ headers = mimetools.Message(sys.stdout)
+ finally:
+ sys.stdout.seek(0, 2)
+ status_header = headers.get('status')
+ error_response = False
+ if status_header:
+ try:
+ status_code = int(status_header.split(' ', 1)[0])
+ error_response = status_code >= 400
+ except ValueError:
+ error_response = True
+
+ if not error_response:
+ try:
+ parent_package = import_hook.GetParentPackage(module_fullname)
+ except Exception:
+ parent_package = None
+
+ if parent_package is not None:
+ submodule = GetSubmoduleName(module_fullname)
+ setattr(parent_package, submodule, script_module)
+
+ return error_response
+ finally:
+ script_module.__name__ = module_fullname
+
+
+def ExecuteCGI(root_path,
+ handler_path,
+ cgi_path,
+ env,
+ infile,
+ outfile,
+ module_dict,
+ exec_script=ExecuteOrImportScript):
+ """Executes Python file in this process as if it were a CGI.
+
+ Does not return an HTTP response line. CGIs should output headers followed by
+ the body content.
+
+ The modules in sys.modules should be the same before and after the CGI is
+ executed, with the specific exception of encodings-related modules, which
+ cannot be reloaded and thus must always stay in sys.modules.
+
+ Args:
+ root_path: Path to the root of the application.
+ handler_path: CGI path stored in the application configuration (as a path
+ like 'foo/bar/baz.py'). May contain $PYTHON_LIB references.
+ cgi_path: Absolute path to the CGI script file on disk.
+ env: Dictionary of environment variables to use for the execution.
+ infile: File-like object to read HTTP request input data from.
+ outfile: FIle-like object to write HTTP response data to.
+ module_dict: Dictionary in which application-loaded modules should be
+ preserved between requests. This removes the need to reload modules that
+ are reused between requests, significantly increasing load performance.
+ This dictionary must be separate from the sys.modules dictionary.
+ exec_script: Used for dependency injection.
+ """
+ old_module_dict = sys.modules.copy()
+ old_builtin = __builtin__.__dict__.copy()
+ old_argv = sys.argv
+ old_stdin = sys.stdin
+ old_stdout = sys.stdout
+ old_env = os.environ.copy()
+ old_cwd = os.getcwd()
+ old_file_type = types.FileType
+ reset_modules = False
+
+ try:
+ ClearAllButEncodingsModules(sys.modules)
+ sys.modules.update(module_dict)
+ sys.argv = [cgi_path]
+ sys.stdin = infile
+ sys.stdout = outfile
+ os.environ.clear()
+ os.environ.update(env)
+ before_path = sys.path[:]
+ os.chdir(os.path.dirname(cgi_path))
+
+ hook = HardenedModulesHook(sys.modules)
+ sys.meta_path = [hook]
+ if hasattr(sys, 'path_importer_cache'):
+ sys.path_importer_cache.clear()
+
+ __builtin__.file = FakeFile
+ __builtin__.open = FakeFile
+ types.FileType = FakeFile
+
+ __builtin__.buffer = NotImplementedFake
+
+ logging.debug('Executing CGI with env:\n%s', pprint.pformat(env))
+ try:
+ reset_modules = exec_script(handler_path, cgi_path, hook)
+ except SystemExit, e:
+ logging.debug('CGI exited with status: %s', e)
+ except:
+ reset_modules = True
+ raise
+
+ finally:
+ sys.meta_path = []
+ sys.path_importer_cache.clear()
+
+ _ClearTemplateCache(sys.modules)
+
+ module_dict.update(sys.modules)
+ ClearAllButEncodingsModules(sys.modules)
+ sys.modules.update(old_module_dict)
+
+ __builtin__.__dict__.update(old_builtin)
+ sys.argv = old_argv
+ sys.stdin = old_stdin
+ sys.stdout = old_stdout
+
+ sys.path[:] = before_path
+
+ os.environ.clear()
+ os.environ.update(old_env)
+ os.chdir(old_cwd)
+
+ types.FileType = old_file_type
+
+
+class CGIDispatcher(URLDispatcher):
+ """Dispatcher that executes Python CGI scripts."""
+
+ def __init__(self,
+ module_dict,
+ root_path,
+ path_adjuster,
+ setup_env=SetupEnvironment,
+ exec_cgi=ExecuteCGI,
+ create_logging_handler=ApplicationLoggingHandler):
+ """Initializer.
+
+ Args:
+ module_dict: Dictionary in which application-loaded modules should be
+ preserved between requests. This dictionary must be separate from the
+ sys.modules dictionary.
+ path_adjuster: Instance of PathAdjuster to use for finding absolute
+ paths of CGI files on disk.
+ setup_env, exec_cgi, create_logging_handler: Used for dependency
+ injection.
+ """
+ self._module_dict = module_dict
+ self._root_path = root_path
+ self._path_adjuster = path_adjuster
+ self._setup_env = setup_env
+ self._exec_cgi = exec_cgi
+ self._create_logging_handler = create_logging_handler
+
+ def Dispatch(self,
+ relative_url,
+ path,
+ headers,
+ infile,
+ outfile,
+ base_env_dict=None):
+ """Dispatches the Python CGI."""
+ handler = self._create_logging_handler()
+ logging.getLogger().addHandler(handler)
+ before_level = logging.root.level
+ try:
+ env = {}
+ if base_env_dict:
+ env.update(base_env_dict)
+ cgi_path = self._path_adjuster.AdjustPath(path)
+ env.update(self._setup_env(cgi_path, relative_url, headers))
+ self._exec_cgi(self._root_path,
+ path,
+ cgi_path,
+ env,
+ infile,
+ outfile,
+ self._module_dict)
+ handler.AddDebuggingConsole(relative_url, env, outfile)
+ finally:
+ logging.root.level = before_level
+ logging.getLogger().removeHandler(handler)
+
+ def __str__(self):
+ """Returns a string representation of this dispatcher."""
+ return 'CGI dispatcher'
+
+
+class LocalCGIDispatcher(CGIDispatcher):
+ """Dispatcher that executes local functions like they're CGIs.
+
+ The contents of sys.modules will be preserved for local CGIs running this
+ dispatcher, but module hardening will still occur for any new imports. Thus,
+ be sure that any local CGIs have loaded all of their dependent modules
+ _before_ they are executed.
+ """
+
+ def __init__(self, module_dict, path_adjuster, cgi_func):
+ """Initializer.
+
+ Args:
+ module_dict: Passed to CGIDispatcher.
+ path_adjuster: Passed to CGIDispatcher.
+ cgi_func: Callable function taking no parameters that should be
+ executed in a CGI environment in the current process.
+ """
+ self._cgi_func = cgi_func
+
+ def curried_exec_script(*args, **kwargs):
+ cgi_func()
+ return False
+
+ def curried_exec_cgi(*args, **kwargs):
+ kwargs['exec_script'] = curried_exec_script
+ return ExecuteCGI(*args, **kwargs)
+
+ CGIDispatcher.__init__(self,
+ module_dict,
+ '',
+ path_adjuster,
+ exec_cgi=curried_exec_cgi)
+
+ def Dispatch(self, *args, **kwargs):
+ """Preserves sys.modules for CGIDispatcher.Dispatch."""
+ self._module_dict.update(sys.modules)
+ CGIDispatcher.Dispatch(self, *args, **kwargs)
+
+ def __str__(self):
+ """Returns a string representation of this dispatcher."""
+ return 'Local CGI dispatcher for %s' % self._cgi_func
+
+
+class PathAdjuster(object):
+ """Adjusts application file paths to paths relative to the application or
+ external library directories."""
+
+ def __init__(self, root_path):
+ """Initializer.
+
+ Args:
+ root_path: Path to the root of the application running on the server.
+ """
+ self._root_path = os.path.abspath(root_path)
+
+ def AdjustPath(self, path):
+ """Adjusts application file path to paths relative to the application or
+ external library directories.
+
+ Handler paths that start with $PYTHON_LIB will be converted to paths
+ relative to the google directory.
+
+ Args:
+ path: File path that should be adjusted.
+
+ Returns:
+ The adjusted path.
+ """
+ if path.startswith(PYTHON_LIB_VAR):
+ path = os.path.join(os.path.dirname(os.path.dirname(google.__file__)),
+ path[len(PYTHON_LIB_VAR) + 1:])
+ else:
+ path = os.path.join(self._root_path, path)
+
+ return path
+
+
+class StaticFileMimeTypeMatcher(object):
+ """Computes mime type based on URLMap and file extension.
+
+ To determine the mime type, we first see if there is any mime-type property
+ on each URLMap entry. If non is specified, we use the mimetypes module to
+ guess the mime type from the file path extension, and use
+ application/octet-stream if we can't find the mimetype.
+ """
+
+ def __init__(self,
+ url_map_list,
+ path_adjuster):
+ """Initializer.
+
+ Args:
+ url_map_list: List of appinfo.URLMap objects.
+ If empty or None, then we always use the mime type chosen by the
+ mimetypes module.
+ path_adjuster: PathAdjuster object used to adjust application file paths.
+ """
+ self._patterns = []
+
+ if url_map_list:
+ for entry in url_map_list:
+ if entry.mime_type is None:
+ continue
+ handler_type = entry.GetHandlerType()
+ if handler_type not in (appinfo.STATIC_FILES, appinfo.STATIC_DIR):
+ continue
+
+ if handler_type == appinfo.STATIC_FILES:
+ regex = entry.upload
+ else:
+ static_dir = entry.static_dir
+ if static_dir[-1] == '/':
+ static_dir = static_dir[:-1]
+ regex = '/'.join((entry.static_dir, r'(.*)'))
+
+ adjusted_regex = r'^%s$' % path_adjuster.AdjustPath(regex)
+ try:
+ path_re = re.compile(adjusted_regex)
+ except re.error, e:
+ raise InvalidAppConfigError('regex does not compile: %s' % e)
+
+ self._patterns.append((path_re, entry.mime_type))
+
+ def GetMimeType(self, path):
+ """Returns the mime type that we should use when serving the specified file.
+
+ Args:
+ path: String containing the file's path on disk.
+
+ Returns:
+ String containing the mime type to use. Will be 'application/octet-stream'
+ if we have no idea what it should be.
+ """
+ for (path_re, mime_type) in self._patterns:
+ the_match = path_re.match(path)
+ if the_match:
+ return mime_type
+
+ filename, extension = os.path.splitext(path)
+ return mimetypes.types_map.get(extension, 'application/octet-stream')
+
+
+def ReadDataFile(data_path, openfile=file):
+ """Reads a file on disk, returning a corresponding HTTP status and data.
+
+ Args:
+ data_path: Path to the file on disk to read.
+ openfile: Used for dependency injection.
+
+ Returns:
+ Tuple (status, data) where status is an HTTP response code, and data is
+ the data read; will be an empty string if an error occurred or the
+ file was empty.
+ """
+ status = httplib.INTERNAL_SERVER_ERROR
+ data = ""
+
+ try:
+ data_file = openfile(data_path, 'rb')
+ try:
+ data = data_file.read()
+ finally:
+ data_file.close()
+ status = httplib.OK
+ except (OSError, IOError), e:
+ logging.error('Error encountered reading file "%s":\n%s', data_path, e)
+ if e.errno in FILE_MISSING_EXCEPTIONS:
+ status = httplib.NOT_FOUND
+ else:
+ status = httplib.FORBIDDEN
+
+ return status, data
+
+
+class FileDispatcher(URLDispatcher):
+ """Dispatcher that reads data files from disk."""
+
+ def __init__(self,
+ path_adjuster,
+ static_file_mime_type_matcher,
+ read_data_file=ReadDataFile):
+ """Initializer.
+
+ Args:
+ path_adjuster: Instance of PathAdjuster to use for finding absolute
+ paths of data files on disk.
+ static_file_mime_type_matcher: StaticFileMimeTypeMatcher object.
+ read_data_file: Used for dependency injection.
+ """
+ self._path_adjuster = path_adjuster
+ self._static_file_mime_type_matcher = static_file_mime_type_matcher
+ self._read_data_file = read_data_file
+
+ def Dispatch(self,
+ relative_url,
+ path,
+ headers,
+ infile,
+ outfile,
+ base_env_dict=None):
+ """Reads the file and returns the response status and data."""
+ full_path = self._path_adjuster.AdjustPath(path)
+ status, data = self._read_data_file(full_path)
+ content_type = self._static_file_mime_type_matcher.GetMimeType(full_path)
+
+ outfile.write('Status: %d\r\n' % status)
+ outfile.write('Content-type: %s\r\n' % content_type)
+ outfile.write('\r\n')
+ outfile.write(data)
+
+ def __str__(self):
+ """Returns a string representation of this dispatcher."""
+ return 'File dispatcher'
+
+
+def RewriteResponse(response_file):
+ """Interprets server-side headers and adjusts the HTTP response accordingly.
+
+ Handles the server-side 'status' header, which instructs the server to change
+ the HTTP response code accordingly. Handles the 'location' header, which
+ issues an HTTP 302 redirect to the client. Also corrects the 'content-length'
+ header to reflect actual content length in case extra information has been
+ appended to the response body.
+
+ If the 'status' header supplied by the client is invalid, this method will
+ set the response to a 500 with an error message as content.
+
+ Args:
+ response_file: File-like object containing the full HTTP response including
+ the response code, all headers, and the request body.
+
+ Returns:
+ Tuple (status_code, status_message, header, body) where:
+ status_code: Integer HTTP response status (e.g., 200, 302, 404, 500)
+ status_message: String containing an informational message about the
+ response code, possibly derived from the 'status' header, if supplied.
+ header: String containing the HTTP headers of the response, without
+ a trailing new-line (CRLF).
+ body: String containing the body of the response.
+ """
+ headers = mimetools.Message(response_file)
+
+ response_status = '%d Good to go' % httplib.OK
+
+ location_value = headers.getheader('location')
+ status_value = headers.getheader('status')
+ if status_value:
+ response_status = status_value
+ del headers['status']
+ elif location_value:
+ response_status = '%d Redirecting' % httplib.FOUND
+
+ if not 'Cache-Control' in headers:
+ headers['Cache-Control'] = 'no-cache'
+
+ status_parts = response_status.split(' ', 1)
+ status_code, status_message = (status_parts + [''])[:2]
+ try:
+ status_code = int(status_code)
+ except ValueError:
+ status_code = 500
+ body = 'Error: Invalid "status" header value returned.'
+ else:
+ body = response_file.read()
+
+ headers['content-length'] = str(len(body))
+
+ header_list = []
+ for header in headers.headers:
+ header = header.rstrip('\n')
+ header = header.rstrip('\r')
+ header_list.append(header)
+
+ header_data = '\r\n'.join(header_list) + '\r\n'
+ return status_code, status_message, header_data, body
+
+
+class ModuleManager(object):
+ """Manages loaded modules in the runtime.
+
+ Responsible for monitoring and reporting about file modification times.
+ Modules can be loaded from source or precompiled byte-code files. When a
+ file has source code, the ModuleManager monitors the modification time of
+ the source file even if the module itself is loaded from byte-code.
+ """
+
+ def __init__(self, modules):
+ """Initializer.
+
+ Args:
+ modules: Dictionary containing monitored modules.
+ """
+ self._modules = modules
+ self._default_modules = self._modules.copy()
+
+ self._modification_times = {}
+
+ @staticmethod
+ def GetModuleFile(module, is_file=os.path.isfile):
+ """Helper method to try to determine modules source file.
+
+ Args:
+ module: Module object to get file for.
+ is_file: Function used to determine if a given path is a file.
+
+ Returns:
+ Path of the module's corresponding Python source file if it exists, or
+ just the module's compiled Python file. If the module has an invalid
+ __file__ attribute, None will be returned.
+ """
+ module_file = getattr(module, '__file__', None)
+ if not module_file or module_file == HardenedModulesHook.EMPTY_MODULE_FILE:
+ return None
+
+ source_file = module_file[:module_file.rfind('py') + 2]
+
+ if is_file(source_file):
+ return source_file
+ return module.__file__
+
+ def AreModuleFilesModified(self):
+ """Determines if any monitored files have been modified.
+
+ Returns:
+ True if one or more files have been modified, False otherwise.
+ """
+ for name, (mtime, fname) in self._modification_times.iteritems():
+ if name not in self._modules:
+ continue
+
+ module = self._modules[name]
+
+ if not os.path.isfile(fname):
+ return True
+
+ if mtime != os.path.getmtime(fname):
+ return True
+
+ return False
+
+ def UpdateModuleFileModificationTimes(self):
+ """Records the current modification times of all monitored modules.
+ """
+ self._modification_times.clear()
+ for name, module in self._modules.items():
+ if not isinstance(module, types.ModuleType):
+ continue
+ module_file = self.GetModuleFile(module)
+ if not module_file:
+ continue
+ try:
+ self._modification_times[name] = (os.path.getmtime(module_file),
+ module_file)
+ except OSError, e:
+ if e.errno not in FILE_MISSING_EXCEPTIONS:
+ raise e
+
+ def ResetModules(self):
+ """Clear modules so that when request is run they are reloaded."""
+ self._modules.clear()
+ self._modules.update(self._default_modules)
+
+
+def _ClearTemplateCache(module_dict=sys.modules):
+ """Clear template cache in webapp.template module.
+
+ Attempts to load template module. Ignores failure. If module loads, the
+ template cache is cleared.
+ """
+ template_module = module_dict.get('google.appengine.ext.webapp.template')
+ if template_module is not None:
+ template_module.template_cache.clear()
+
+
+def CreateRequestHandler(root_path, login_url, require_indexes=False):
+ """Creates a new BaseHTTPRequestHandler sub-class for use with the Python
+ BaseHTTPServer module's HTTP server.
+
+ Python's built-in HTTP server does not support passing context information
+ along to instances of its request handlers. This function gets around that
+ by creating a sub-class of the handler in a closure that has access to
+ this context information.
+
+ Args:
+ root_path: Path to the root of the application running on the server.
+ login_url: Relative URL which should be used for handling user logins.
+ require_indexes: True if index.yaml is read-only gospel; default False.
+
+ Returns:
+ Sub-class of BaseHTTPRequestHandler.
+ """
+ application_module_dict = SetupSharedModules(sys.modules)
+
+ if require_indexes:
+ index_yaml_updater = None
+ else:
+ index_yaml_updater = dev_appserver_index.IndexYamlUpdater(root_path)
+
+ class DevAppServerRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
+ """Dispatches URLs using patterns from a URLMatcher, which is created by
+ loading an application's configuration file. Executes CGI scripts in the
+ local process so the scripts can use mock versions of APIs.
+
+ HTTP requests that correctly specify a user info cookie
+ (dev_appserver_login.COOKIE_NAME) will have the 'USER_EMAIL' environment
+ variable set accordingly. If the user is also an admin, the
+ 'USER_IS_ADMIN' variable will exist and be set to '1'. If the user is not
+ logged in, 'USER_EMAIL' will be set to the empty string.
+
+ On each request, raises an InvalidAppConfigError exception if the
+ application configuration file in the directory specified by the root_path
+ argument is invalid.
+ """
+ server_version = 'Development/1.0'
+
+ module_dict = application_module_dict
+ module_manager = ModuleManager(application_module_dict)
+
+ def __init__(self, *args, **kwargs):
+ """Initializer.
+
+ Args:
+ args, kwargs: Positional and keyword arguments passed to the constructor
+ of the super class.
+ """
+ BaseHTTPServer.BaseHTTPRequestHandler.__init__(self, *args, **kwargs)
+
+ def do_GET(self):
+ """Handle GET requests."""
+ self._HandleRequest()
+
+ def do_POST(self):
+ """Handles POST requests."""
+ self._HandleRequest()
+
+ def do_PUT(self):
+ """Handle PUT requests."""
+ self._HandleRequest()
+
+ def do_HEAD(self):
+ """Handle HEAD requests."""
+ self._HandleRequest()
+
+ def do_OPTIONS(self):
+ """Handles OPTIONS requests."""
+ self._HandleRequest()
+
+ def do_DELETE(self):
+ """Handle DELETE requests."""
+ self._HandleRequest()
+
+ def do_TRACE(self):
+ """Handles TRACE requests."""
+ self._HandleRequest()
+
+ def _HandleRequest(self):
+ """Handles any type of request and prints exceptions if they occur."""
+ server_name = self.headers.get('host') or self.server.server_name
+ server_name = server_name.split(':', 1)[0]
+
+ env_dict = {
+ 'REQUEST_METHOD': self.command,
+ 'REMOTE_ADDR': self.client_address[0],
+ 'SERVER_SOFTWARE': self.server_version,
+ 'SERVER_NAME': server_name,
+ 'SERVER_PROTOCOL': self.protocol_version,
+ 'SERVER_PORT': str(self.server.server_port),
+ }
+
+ full_url = GetFullURL(server_name, self.server.server_port, self.path)
+ if len(full_url) > MAX_URL_LENGTH:
+ msg = 'Requested URI too long: %s' % full_url
+ logging.error(msg)
+ self.send_response(httplib.REQUEST_URI_TOO_LONG, msg)
+ return
+
+ tbhandler = cgitb.Hook(file=self.wfile).handle
+ try:
+ if self.module_manager.AreModuleFilesModified():
+ self.module_manager.ResetModules()
+
+ implicit_matcher = CreateImplicitMatcher(self.module_dict,
+ root_path,
+ login_url)
+ config, explicit_matcher = LoadAppConfig(root_path, self.module_dict)
+ env_dict['CURRENT_VERSION_ID'] = config.version + ".1"
+ env_dict['APPLICATION_ID'] = config.application
+ dispatcher = MatcherDispatcher(login_url,
+ [implicit_matcher, explicit_matcher])
+
+ if require_indexes:
+ dev_appserver_index.SetupIndexes(config.application, root_path)
+
+ infile = cStringIO.StringIO(self.rfile.read(
+ int(self.headers.get('content-length', 0))))
+ outfile = cStringIO.StringIO()
+ try:
+ dispatcher.Dispatch(self.path,
+ None,
+ self.headers,
+ infile,
+ outfile,
+ base_env_dict=env_dict)
+ finally:
+ self.module_manager.UpdateModuleFileModificationTimes()
+
+ outfile.flush()
+ outfile.seek(0)
+ status_code, status_message, header_data, body = RewriteResponse(outfile)
+
+ except yaml_errors.EventListenerError, e:
+ title = 'Fatal error when loading application configuration'
+ msg = '%s:\n%s' % (title, str(e))
+ logging.error(msg)
+ self.send_response(httplib.INTERNAL_SERVER_ERROR, title)
+ self.wfile.write('Content-Type: text/html\n\n')
+ self.wfile.write('<pre>%s</pre>' % cgi.escape(msg))
+ except:
+ msg = 'Exception encountered handling request'
+ logging.exception(msg)
+ self.send_response(httplib.INTERNAL_SERVER_ERROR, msg)
+ tbhandler()
+ else:
+ try:
+ self.send_response(status_code, status_message)
+ self.wfile.write(header_data)
+ self.wfile.write('\r\n')
+ if self.command != 'HEAD':
+ self.wfile.write(body)
+ elif body:
+ logging.warning('Dropping unexpected body in response '
+ 'to HEAD request')
+ except (IOError, OSError), e:
+ if e.errno != errno.EPIPE:
+ raise e
+ except socket.error, e:
+ if len(e.args) >= 1 and e.args[0] != errno.EPIPE:
+ raise e
+ else:
+ if index_yaml_updater is not None:
+ index_yaml_updater.UpdateIndexYaml()
+
+ def log_error(self, format, *args):
+ """Redirect error messages through the logging module."""
+ logging.error(format, *args)
+
+ def log_message(self, format, *args):
+ """Redirect log messages through the logging module."""
+ logging.info(format, *args)
+
+ return DevAppServerRequestHandler
+
+
+def ReadAppConfig(appinfo_path, parse_app_config=appinfo.LoadSingleAppInfo):
+ """Reads app.yaml file and returns its app id and list of URLMap instances.
+
+ Args:
+ appinfo_path: String containing the path to the app.yaml file.
+ parse_app_config: Used for dependency injection.
+
+ Returns:
+ AppInfoExternal instance.
+
+ Raises:
+ If the config file could not be read or the config does not contain any
+ URLMap instances, this function will raise an InvalidAppConfigError
+ exception.
+ """
+ try:
+ appinfo_file = file(appinfo_path, 'r')
+ try:
+ return parse_app_config(appinfo_file)
+ finally:
+ appinfo_file.close()
+ except IOError, e:
+ raise InvalidAppConfigError(
+ 'Application configuration could not be read from "%s"' % appinfo_path)
+
+
+def CreateURLMatcherFromMaps(root_path,
+ url_map_list,
+ module_dict,
+ create_url_matcher=URLMatcher,
+ create_cgi_dispatcher=CGIDispatcher,
+ create_file_dispatcher=FileDispatcher,
+ create_path_adjuster=PathAdjuster):
+ """Creates a URLMatcher instance from URLMap.
+
+ Creates all of the correct URLDispatcher instances to handle the various
+ content types in the application configuration.
+
+ Args:
+ root_path: Path to the root of the application running on the server.
+ url_map_list: List of appinfo.URLMap objects to initialize this
+ matcher with. Can be an empty list if you would like to add patterns
+ manually.
+ module_dict: Dictionary in which application-loaded modules should be
+ preserved between requests. This dictionary must be separate from the
+ sys.modules dictionary.
+ create_url_matcher, create_cgi_dispatcher, create_file_dispatcher,
+ create_path_adjuster: Used for dependency injection.
+
+ Returns:
+ Instance of URLMatcher with the supplied URLMap objects properly loaded.
+ """
+ url_matcher = create_url_matcher()
+ path_adjuster = create_path_adjuster(root_path)
+ cgi_dispatcher = create_cgi_dispatcher(module_dict, root_path, path_adjuster)
+ file_dispatcher = create_file_dispatcher(path_adjuster,
+ StaticFileMimeTypeMatcher(url_map_list, path_adjuster))
+
+ for url_map in url_map_list:
+ admin_only = url_map.login == appinfo.LOGIN_ADMIN
+ requires_login = url_map.login == appinfo.LOGIN_REQUIRED or admin_only
+
+ handler_type = url_map.GetHandlerType()
+ if handler_type == appinfo.HANDLER_SCRIPT:
+ dispatcher = cgi_dispatcher
+ elif handler_type in (appinfo.STATIC_FILES, appinfo.STATIC_DIR):
+ dispatcher = file_dispatcher
+ else:
+ raise InvalidAppConfigError('Unknown handler type "%s"' % handler_type)
+
+ regex = url_map.url
+ path = url_map.GetHandler()
+ if handler_type == appinfo.STATIC_DIR:
+ if regex[-1] == r'/':
+ regex = regex[:-1]
+ if path[-1] == os.path.sep:
+ path = path[:-1]
+ regex = '/'.join((re.escape(regex), '(.*)'))
+ if os.path.sep == '\\':
+ backref = r'\\1'
+ else:
+ backref = r'\1'
+ path = os.path.normpath(path) + os.path.sep + backref
+
+ url_matcher.AddURL(regex,
+ dispatcher,
+ path,
+ requires_login, admin_only)
+
+ return url_matcher
+
+
+def LoadAppConfig(root_path,
+ module_dict,
+ read_app_config=ReadAppConfig,
+ create_matcher=CreateURLMatcherFromMaps):
+ """Creates a Matcher instance for an application configuration file.
+
+ Raises an InvalidAppConfigError exception if there is anything wrong with
+ the application configuration file.
+
+ Args:
+ root_path: Path to the root of the application to load.
+ module_dict: Dictionary in which application-loaded modules should be
+ preserved between requests. This dictionary must be separate from the
+ sys.modules dictionary.
+ read_url_map, create_matcher: Used for dependency injection.
+
+ Returns:
+ tuple: (AppInfoExternal, URLMatcher)
+ """
+
+ for appinfo_path in [os.path.join(root_path, 'app.yaml'),
+ os.path.join(root_path, 'app.yml')]:
+
+ if os.path.isfile(appinfo_path):
+ try:
+ config = read_app_config(appinfo_path, appinfo.LoadSingleAppInfo)
+
+ matcher = create_matcher(root_path,
+ config.handlers,
+ module_dict)
+
+ return (config, matcher)
+ except gexcept.AbstractMethod:
+ pass
+
+ raise AppConfigNotFoundError
+
+
+def SetupStubs(app_id, **config):
+ """Sets up testing stubs of APIs.
+
+ Args:
+ app_id: Application ID being served.
+
+ Keywords:
+ login_url: Relative URL which should be used for handling user login/logout.
+ datastore_path: Path to the file to store Datastore file stub data in.
+ history_path: Path to the file to store Datastore history in.
+ clear_datastore: If the datastore and history should be cleared on startup.
+ smtp_host: SMTP host used for sending test mail.
+ smtp_port: SMTP port.
+ smtp_user: SMTP user.
+ smtp_password: SMTP password.
+ enable_sendmail: Whether to use sendmail as an alternative to SMTP.
+ show_mail_body: Whether to log the body of emails.
+ remove: Used for dependency injection.
+ """
+ login_url = config['login_url']
+ datastore_path = config['datastore_path']
+ history_path = config['history_path']
+ clear_datastore = config['clear_datastore']
+ require_indexes = config.get('require_indexes', False)
+ smtp_host = config.get('smtp_host', None)
+ smtp_port = config.get('smtp_port', 25)
+ smtp_user = config.get('smtp_user', '')
+ smtp_password = config.get('smtp_password', '')
+ enable_sendmail = config.get('enable_sendmail', False)
+ show_mail_body = config.get('show_mail_body', False)
+ remove = config.get('remove', os.remove)
+
+ if clear_datastore:
+ for path in (datastore_path, history_path):
+ if os.path.lexists(path):
+ logging.info('Attempting to remove file at %s', path)
+ try:
+ remove(path)
+ except OSError, e:
+ logging.warning('Removing file failed: %s', e)
+
+ apiproxy_stub_map.apiproxy = apiproxy_stub_map.APIProxyStubMap()
+
+ datastore = datastore_file_stub.DatastoreFileStub(
+ app_id, datastore_path, history_path, require_indexes=require_indexes)
+ apiproxy_stub_map.apiproxy.RegisterStub('datastore_v3', datastore)
+
+ fixed_login_url = '%s?%s=%%s' % (login_url,
+ dev_appserver_login.CONTINUE_PARAM)
+ fixed_logout_url = '%s&%s' % (fixed_login_url,
+ dev_appserver_login.LOGOUT_PARAM)
+
+ apiproxy_stub_map.apiproxy.RegisterStub(
+ 'user',
+ user_service_stub.UserServiceStub(login_url=fixed_login_url,
+ logout_url=fixed_logout_url))
+
+ apiproxy_stub_map.apiproxy.RegisterStub(
+ 'urlfetch',
+ urlfetch_stub.URLFetchServiceStub())
+
+ apiproxy_stub_map.apiproxy.RegisterStub(
+ 'mail',
+ mail_stub.MailServiceStub(smtp_host,
+ smtp_port,
+ smtp_user,
+ smtp_password,
+ enable_sendmail=enable_sendmail,
+ show_mail_body=show_mail_body))
+
+ apiproxy_stub_map.apiproxy.RegisterStub(
+ 'memcache',
+ memcache_stub.MemcacheServiceStub())
+
+ try:
+ from google.appengine.api.images import images_stub
+ apiproxy_stub_map.apiproxy.RegisterStub(
+ 'images',
+ images_stub.ImagesServiceStub())
+ except ImportError, e:
+ logging.warning('Could not initialize images API; you are likely missing '
+ 'the Python "PIL" module. ImportError: %s', e)
+ from google.appengine.api.images import images_not_implemented_stub
+ apiproxy_stub_map.apiproxy.RegisterStub('images',
+ images_not_implemented_stub.ImagesNotImplementedServiceStub())
+
+
+def CreateImplicitMatcher(module_dict,
+ root_path,
+ login_url,
+ create_path_adjuster=PathAdjuster,
+ create_local_dispatcher=LocalCGIDispatcher,
+ create_cgi_dispatcher=CGIDispatcher):
+ """Creates a URLMatcher instance that handles internal URLs.
+
+ Used to facilitate handling user login/logout, debugging, info about the
+ currently running app, etc.
+
+ Args:
+ module_dict: Dictionary in the form used by sys.modules.
+ root_path: Path to the root of the application.
+ login_url: Relative URL which should be used for handling user login/logout.
+ create_local_dispatcher: Used for dependency injection.
+
+ Returns:
+ Instance of URLMatcher with appropriate dispatchers.
+ """
+ url_matcher = URLMatcher()
+ path_adjuster = create_path_adjuster(root_path)
+
+ login_dispatcher = create_local_dispatcher(sys.modules, path_adjuster,
+ dev_appserver_login.main)
+ url_matcher.AddURL(login_url,
+ login_dispatcher,
+ '',
+ False,
+ False)
+
+
+ admin_dispatcher = create_cgi_dispatcher(module_dict, root_path,
+ path_adjuster)
+ url_matcher.AddURL('/_ah/admin(?:/.*)?',
+ admin_dispatcher,
+ DEVEL_CONSOLE_PATH,
+ False,
+ False)
+
+ return url_matcher
+
+
+def SetupTemplates(template_dir):
+ """Reads debugging console template files and initializes the console.
+
+ Does nothing if templates have already been initialized.
+
+ Args:
+ template_dir: Path to the directory containing the templates files.
+
+ Raises:
+ OSError or IOError if any of the template files could not be read.
+ """
+ if ApplicationLoggingHandler.AreTemplatesInitialized():
+ return
+
+ try:
+ header = open(os.path.join(template_dir, HEADER_TEMPLATE)).read()
+ script = open(os.path.join(template_dir, SCRIPT_TEMPLATE)).read()
+ middle = open(os.path.join(template_dir, MIDDLE_TEMPLATE)).read()
+ footer = open(os.path.join(template_dir, FOOTER_TEMPLATE)).read()
+ except (OSError, IOError):
+ logging.error('Could not read template files from %s', template_dir)
+ raise
+
+ ApplicationLoggingHandler.InitializeTemplates(header, script, middle, footer)
+
+
+def CreateServer(root_path,
+ login_url,
+ port,
+ template_dir,
+ serve_address='',
+ require_indexes=False,
+ python_path_list=sys.path):
+ """Creates an new HTTPServer for an application.
+
+ Args:
+ root_path: String containing the path to the root directory of the
+ application where the app.yaml file is.
+ login_url: Relative URL which should be used for handling user login/logout.
+ port: Port to start the application server on.
+ template_dir: Path to the directory in which the debug console templates
+ are stored.
+ serve_address: Address on which the server should serve.
+ require_indexes: True if index.yaml is read-only gospel; default False.
+ python_path_list: Used for dependency injection.
+
+ Returns:
+ Instance of BaseHTTPServer.HTTPServer that's ready to start accepting.
+ """
+ absolute_root_path = os.path.abspath(root_path)
+
+ SetupTemplates(template_dir)
+ FakeFile.SetAllowedPaths([absolute_root_path,
+ os.path.dirname(os.path.dirname(google.__file__)),
+ template_dir])
+
+ handler_class = CreateRequestHandler(absolute_root_path, login_url,
+ require_indexes)
+
+ if absolute_root_path not in python_path_list:
+ python_path_list.insert(0, absolute_root_path)
+
+ return BaseHTTPServer.HTTPServer((serve_address, port), handler_class)