thirdparty/google_appengine/google/appengine/tools/dev_appserver.py
changeset 109 620f9b141567
child 149 f2e327a7c5de
equal deleted inserted replaced
108:261778de26ff 109:620f9b141567
       
     1 #!/usr/bin/env python
       
     2 #
       
     3 # Copyright 2007 Google Inc.
       
     4 #
       
     5 # Licensed under the Apache License, Version 2.0 (the "License");
       
     6 # you may not use this file except in compliance with the License.
       
     7 # You may obtain a copy of the License at
       
     8 #
       
     9 #     http://www.apache.org/licenses/LICENSE-2.0
       
    10 #
       
    11 # Unless required by applicable law or agreed to in writing, software
       
    12 # distributed under the License is distributed on an "AS IS" BASIS,
       
    13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
       
    14 # See the License for the specific language governing permissions and
       
    15 # limitations under the License.
       
    16 #
       
    17 """Pure-Python application server for testing applications locally.
       
    18 
       
    19 Given a port and the paths to a valid application directory (with an 'app.yaml'
       
    20 file), the external library directory, and a relative URL to use for logins,
       
    21 creates an HTTP server that can be used to test an application locally. Uses
       
    22 stubs instead of actual APIs when SetupStubs() is called first.
       
    23 
       
    24 Example:
       
    25   root_path = '/path/to/application/directory'
       
    26   login_url = '/login'
       
    27   port = 8080
       
    28   template_dir = '/path/to/appserver/templates'
       
    29   server = dev_appserver.CreateServer(root_path, login_url, port, template_dir)
       
    30   server.serve_forever()
       
    31 """
       
    32 
       
    33 
       
    34 import os
       
    35 os.environ['TZ'] = 'UTC'
       
    36 import time
       
    37 if hasattr(time, 'tzset'):
       
    38   time.tzset()
       
    39 
       
    40 import __builtin__
       
    41 import BaseHTTPServer
       
    42 import Cookie
       
    43 import cStringIO
       
    44 import cgi
       
    45 import cgitb
       
    46 import dummy_thread
       
    47 import errno
       
    48 import httplib
       
    49 import imp
       
    50 import inspect
       
    51 import itertools
       
    52 import logging
       
    53 import mimetools
       
    54 import mimetypes
       
    55 import pickle
       
    56 import pprint
       
    57 import random
       
    58 
       
    59 import re
       
    60 import sre_compile
       
    61 import sre_constants
       
    62 import sre_parse
       
    63 
       
    64 import mimetypes
       
    65 import socket
       
    66 import sys
       
    67 import urlparse
       
    68 import urllib
       
    69 import traceback
       
    70 import types
       
    71 
       
    72 import google
       
    73 from google.pyglib import gexcept
       
    74 
       
    75 from google.appengine.api import apiproxy_stub_map
       
    76 from google.appengine.api import appinfo
       
    77 from google.appengine.api import datastore_admin
       
    78 from google.appengine.api import datastore_file_stub
       
    79 from google.appengine.api import urlfetch_stub
       
    80 from google.appengine.api import mail_stub
       
    81 from google.appengine.api import user_service_stub
       
    82 from google.appengine.api import yaml_errors
       
    83 from google.appengine.api.memcache import memcache_stub
       
    84 
       
    85 from google.appengine.tools import dev_appserver_index
       
    86 from google.appengine.tools import dev_appserver_login
       
    87 
       
    88 
       
    89 PYTHON_LIB_VAR = '$PYTHON_LIB'
       
    90 DEVEL_CONSOLE_PATH = PYTHON_LIB_VAR + '/google/appengine/ext/admin'
       
    91 
       
    92 FILE_MISSING_EXCEPTIONS = frozenset([errno.ENOENT, errno.ENOTDIR])
       
    93 
       
    94 MAX_URL_LENGTH = 2047
       
    95 
       
    96 HEADER_TEMPLATE = 'logging_console_header.html'
       
    97 SCRIPT_TEMPLATE = 'logging_console.js'
       
    98 MIDDLE_TEMPLATE = 'logging_console_middle.html'
       
    99 FOOTER_TEMPLATE = 'logging_console_footer.html'
       
   100 
       
   101 DEFAULT_ENV = {
       
   102   'GATEWAY_INTERFACE': 'CGI/1.1',
       
   103   'AUTH_DOMAIN': 'gmail.com',
       
   104   'TZ': 'UTC',
       
   105 }
       
   106 
       
   107 for ext, mime_type in (('.asc', 'text/plain'),
       
   108                        ('.diff', 'text/plain'),
       
   109                        ('.csv', 'text/comma-separated-values'),
       
   110                        ('.rss', 'application/rss+xml'),
       
   111                        ('.text', 'text/plain'),
       
   112                        ('.wbmp', 'image/vnd.wap.wbmp')):
       
   113   mimetypes.add_type(mime_type, ext)
       
   114 
       
   115 
       
   116 class Error(Exception):
       
   117   """Base-class for exceptions in this module."""
       
   118 
       
   119 class InvalidAppConfigError(Error):
       
   120   """The supplied application configuration file is invalid."""
       
   121 
       
   122 class AppConfigNotFoundError(Error):
       
   123   """Application configuration file not found."""
       
   124 
       
   125 class TemplatesNotLoadedError(Error):
       
   126   """Templates for the debugging console were not loaded."""
       
   127 
       
   128 
       
   129 def SplitURL(relative_url):
       
   130   """Splits a relative URL into its path and query-string components.
       
   131 
       
   132   Args:
       
   133     relative_url: String containing the relative URL (often starting with '/')
       
   134       to split. Should be properly escaped as www-form-urlencoded data.
       
   135 
       
   136   Returns:
       
   137     Tuple (script_name, query_string) where:
       
   138       script_name: Relative URL of the script that was accessed.
       
   139       query_string: String containing everything after the '?' character.
       
   140   """
       
   141   scheme, netloc, path, query, fragment = urlparse.urlsplit(relative_url)
       
   142   return path, query
       
   143 
       
   144 
       
   145 def GetFullURL(server_name, server_port, relative_url):
       
   146   """Returns the full, original URL used to access the relative URL.
       
   147 
       
   148   Args:
       
   149     server_name: Name of the local host, or the value of the 'host' header
       
   150       from the request.
       
   151     server_port: Port on which the request was served (string or int).
       
   152     relative_url: Relative URL that was accessed, including query string.
       
   153 
       
   154   Returns:
       
   155     String containing the original URL.
       
   156   """
       
   157   if str(server_port) != '80':
       
   158     netloc = '%s:%s' % (server_name, server_port)
       
   159   else:
       
   160     netloc = server_name
       
   161   return 'http://%s%s' % (netloc, relative_url)
       
   162 
       
   163 
       
   164 class URLDispatcher(object):
       
   165   """Base-class for handling HTTP requests."""
       
   166 
       
   167   def Dispatch(self,
       
   168                relative_url,
       
   169                path,
       
   170                headers,
       
   171                infile,
       
   172                outfile,
       
   173                base_env_dict=None):
       
   174     """Dispatch and handle an HTTP request.
       
   175 
       
   176     base_env_dict should contain at least these CGI variables:
       
   177       REQUEST_METHOD, REMOTE_ADDR, SERVER_SOFTWARE, SERVER_NAME,
       
   178       SERVER_PROTOCOL, SERVER_PORT
       
   179 
       
   180     Args:
       
   181       relative_url: String containing the URL accessed.
       
   182       path: Local path of the resource that was matched; back-references will be
       
   183         replaced by values matched in the relative_url. Path may be relative
       
   184         or absolute, depending on the resource being served (e.g., static files
       
   185         will have an absolute path; scripts will be relative).
       
   186       headers: Instance of mimetools.Message with headers from the request.
       
   187       infile: File-like object with input data from the request.
       
   188       outfile: File-like object where output data should be written.
       
   189       base_env_dict: Dictionary of CGI environment parameters if available.
       
   190         Defaults to None.
       
   191     """
       
   192     raise NotImplementedError
       
   193 
       
   194 
       
   195 class URLMatcher(object):
       
   196   """Matches an arbitrary URL using a list of URL patterns from an application.
       
   197 
       
   198   Each URL pattern has an associated URLDispatcher instance and path to the
       
   199   resource's location on disk. See AddURL for more details. The first pattern
       
   200   that matches an inputted URL will have its associated values returned by
       
   201   Match().
       
   202   """
       
   203 
       
   204   def __init__(self):
       
   205     """Initializer."""
       
   206     self._url_patterns = []
       
   207 
       
   208   def AddURL(self, regex, dispatcher, path, requires_login, admin_only):
       
   209     """Adds a URL pattern to the list of patterns.
       
   210 
       
   211     If the supplied regex starts with a '^' or ends with a '$' an
       
   212     InvalidAppConfigError exception will be raised. Start and end symbols
       
   213     and implicitly added to all regexes, meaning we assume that all regexes
       
   214     consume all input from a URL.
       
   215 
       
   216     Args:
       
   217       regex: String containing the regular expression pattern.
       
   218       dispatcher: Instance of URLDispatcher that should handle requests that
       
   219         match this regex.
       
   220       path: Path on disk for the resource. May contain back-references like
       
   221         r'\1', r'\2', etc, which will be replaced by the corresponding groups
       
   222         matched by the regex if present.
       
   223       requires_login: True if the user must be logged-in before accessing this
       
   224         URL; False if anyone can access this URL.
       
   225       admin_only: True if the user must be a logged-in administrator to
       
   226         access the URL; False if anyone can access the URL.
       
   227     """
       
   228     if not isinstance(dispatcher, URLDispatcher):
       
   229       raise TypeError, 'dispatcher must be a URLDispatcher sub-class'
       
   230 
       
   231     if regex.startswith('^') or regex.endswith('$'):
       
   232       raise InvalidAppConfigError, 'regex starts with "^" or ends with "$"'
       
   233 
       
   234     adjusted_regex = '^%s$' % regex
       
   235 
       
   236     try:
       
   237       url_re = re.compile(adjusted_regex)
       
   238     except re.error, e:
       
   239       raise InvalidAppConfigError, 'regex invalid: %s' % e
       
   240 
       
   241     match_tuple = (url_re, dispatcher, path, requires_login, admin_only)
       
   242     self._url_patterns.append(match_tuple)
       
   243 
       
   244   def Match(self,
       
   245             relative_url,
       
   246             split_url=SplitURL):
       
   247     """Matches a URL from a request against the list of URL patterns.
       
   248 
       
   249     The supplied relative_url may include the query string (i.e., the '?'
       
   250     character and everything following).
       
   251 
       
   252     Args:
       
   253       relative_url: Relative URL being accessed in a request.
       
   254 
       
   255     Returns:
       
   256       Tuple (dispatcher, matched_path, requires_login, admin_only), which are
       
   257       the corresponding values passed to AddURL when the matching URL pattern
       
   258       was added to this matcher. The matched_path will have back-references
       
   259       replaced using values matched by the URL pattern. If no match was found,
       
   260       dispatcher will be None.
       
   261     """
       
   262     adjusted_url, query_string = split_url(relative_url)
       
   263 
       
   264     for url_tuple in self._url_patterns:
       
   265       url_re, dispatcher, path, requires_login, admin_only = url_tuple
       
   266       the_match = url_re.match(adjusted_url)
       
   267 
       
   268       if the_match:
       
   269         adjusted_path = the_match.expand(path)
       
   270         return dispatcher, adjusted_path, requires_login, admin_only
       
   271 
       
   272     return None, None, None, None
       
   273 
       
   274   def GetDispatchers(self):
       
   275     """Retrieves the URLDispatcher objects that could be matched.
       
   276 
       
   277     Should only be used in tests.
       
   278 
       
   279     Returns:
       
   280       A set of URLDispatcher objects.
       
   281     """
       
   282     return set([url_tuple[1] for url_tuple in self._url_patterns])
       
   283 
       
   284 
       
   285 class MatcherDispatcher(URLDispatcher):
       
   286   """Dispatcher across multiple URLMatcher instances."""
       
   287 
       
   288   def __init__(self,
       
   289                login_url,
       
   290                url_matchers,
       
   291                get_user_info=dev_appserver_login.GetUserInfo,
       
   292                login_redirect=dev_appserver_login.LoginRedirect):
       
   293     """Initializer.
       
   294 
       
   295     Args:
       
   296       login_url: Relative URL which should be used for handling user logins.
       
   297       url_matchers: Sequence of URLMatcher objects.
       
   298       get_user_info, login_redirect: Used for dependency injection.
       
   299     """
       
   300     self._login_url = login_url
       
   301     self._url_matchers = tuple(url_matchers)
       
   302     self._get_user_info = get_user_info
       
   303     self._login_redirect = login_redirect
       
   304 
       
   305   def Dispatch(self,
       
   306                relative_url,
       
   307                path,
       
   308                headers,
       
   309                infile,
       
   310                outfile,
       
   311                base_env_dict=None):
       
   312     """Dispatches a request to the first matching dispatcher.
       
   313 
       
   314     Matchers are checked in the order they were supplied to the constructor.
       
   315     If no matcher matches, a 404 error will be written to the outfile. The
       
   316     path variable supplied to this method is ignored.
       
   317     """
       
   318     cookies = ', '.join(headers.getheaders('cookie'))
       
   319     email, admin = self._get_user_info(cookies)
       
   320 
       
   321     for matcher in self._url_matchers:
       
   322       dispatcher, matched_path, requires_login, admin_only = matcher.Match(relative_url)
       
   323       if dispatcher is None:
       
   324         continue
       
   325 
       
   326       logging.debug('Matched "%s" to %s with path %s',
       
   327                     relative_url, dispatcher, matched_path)
       
   328 
       
   329       if (requires_login or admin_only) and not email:
       
   330         logging.debug('Login required, redirecting user')
       
   331         self._login_redirect(
       
   332           self._login_url,
       
   333           base_env_dict['SERVER_NAME'],
       
   334           base_env_dict['SERVER_PORT'],
       
   335           relative_url,
       
   336           outfile)
       
   337       elif admin_only and not admin:
       
   338         outfile.write('Status: %d Not authorized\r\n'
       
   339                       '\r\n'
       
   340                       'Current logged in user %s is not '
       
   341                       'authorized to view this page.'
       
   342                       % (httplib.FORBIDDEN, email))
       
   343       else:
       
   344         dispatcher.Dispatch(relative_url,
       
   345                             matched_path,
       
   346                             headers,
       
   347                             infile,
       
   348                             outfile,
       
   349                             base_env_dict=base_env_dict)
       
   350 
       
   351       return
       
   352 
       
   353     outfile.write('Status: %d URL did not match\r\n'
       
   354                   '\r\n'
       
   355                   'Not found error: %s did not match any patterns '
       
   356                   'in application configuration.'
       
   357                   % (httplib.NOT_FOUND, relative_url))
       
   358 
       
   359 
       
   360 class ApplicationLoggingHandler(logging.Handler):
       
   361   """Python Logging handler that displays the debugging console to users."""
       
   362 
       
   363   _COOKIE_NAME = '_ah_severity'
       
   364 
       
   365   _TEMPLATES_INITIALIZED = False
       
   366   _HEADER = None
       
   367   _SCRIPT = None
       
   368   _MIDDLE = None
       
   369   _FOOTER = None
       
   370 
       
   371   @staticmethod
       
   372   def InitializeTemplates(header, script, middle, footer):
       
   373     """Initializes the templates used to render the debugging console.
       
   374 
       
   375     This method must be called before any ApplicationLoggingHandler instances
       
   376     are created.
       
   377 
       
   378     Args:
       
   379       header: The header template that is printed first.
       
   380       script: The script template that is printed after the logging messages.
       
   381       middle: The middle element that's printed before the footer.
       
   382       footer; The last element that's printed at the end of the document.
       
   383     """
       
   384     ApplicationLoggingHandler._HEADER = header
       
   385     ApplicationLoggingHandler._SCRIPT = script
       
   386     ApplicationLoggingHandler._MIDDLE = middle
       
   387     ApplicationLoggingHandler._FOOTER = footer
       
   388     ApplicationLoggingHandler._TEMPLATES_INITIALIZED = True
       
   389 
       
   390   @staticmethod
       
   391   def AreTemplatesInitialized():
       
   392     """Returns True if InitializeTemplates has been called, False otherwise."""
       
   393     return ApplicationLoggingHandler._TEMPLATES_INITIALIZED
       
   394 
       
   395   def __init__(self, *args, **kwargs):
       
   396     """Initializer.
       
   397 
       
   398     Args:
       
   399       args, kwargs: See logging.Handler.
       
   400 
       
   401     Raises:
       
   402       TemplatesNotLoadedError exception if the InitializeTemplates method was
       
   403       not called before creating this instance.
       
   404     """
       
   405     if not self._TEMPLATES_INITIALIZED:
       
   406       raise TemplatesNotLoadedError
       
   407 
       
   408     logging.Handler.__init__(self, *args, **kwargs)
       
   409     self._record_list = []
       
   410     self._start_time = time.time()
       
   411 
       
   412   def emit(self, record):
       
   413     """Called by the logging module each time the application logs a message.
       
   414 
       
   415     Args:
       
   416       record: logging.LogRecord instance corresponding to the newly logged
       
   417         message.
       
   418     """
       
   419     self._record_list.append(record)
       
   420 
       
   421   def AddDebuggingConsole(self, relative_url, env, outfile):
       
   422     """Prints an HTML debugging console to an output stream, if requested.
       
   423 
       
   424     Args:
       
   425       relative_url: Relative URL that was accessed, including the query string.
       
   426         Used to determine if the parameter 'debug' was supplied, in which case
       
   427         the console will be shown.
       
   428       env: Dictionary containing CGI environment variables. Checks for the
       
   429         HTTP_COOKIE entry to see if the accessing user has any logging-related
       
   430         cookies set.
       
   431       outfile: Output stream to which the console should be written if either
       
   432         a debug parameter was supplied or a logging cookie is present.
       
   433     """
       
   434     script_name, query_string = SplitURL(relative_url)
       
   435     param_dict = cgi.parse_qs(query_string, True)
       
   436     cookie_dict = Cookie.SimpleCookie(env.get('HTTP_COOKIE', ''))
       
   437     if 'debug' not in param_dict and self._COOKIE_NAME not in cookie_dict:
       
   438       return
       
   439 
       
   440     outfile.write(self._HEADER)
       
   441     for record in self._record_list:
       
   442       self._PrintRecord(record, outfile)
       
   443 
       
   444     outfile.write(self._MIDDLE)
       
   445     outfile.write(self._SCRIPT)
       
   446     outfile.write(self._FOOTER)
       
   447 
       
   448   def _PrintRecord(self, record, outfile):
       
   449     """Prints a single logging record to an output stream.
       
   450 
       
   451     Args:
       
   452       record: logging.LogRecord instance to print.
       
   453       outfile: Output stream to which the LogRecord should be printed.
       
   454     """
       
   455     message = cgi.escape(record.getMessage())
       
   456     level_name = logging.getLevelName(record.levelno).lower()
       
   457     level_letter = level_name[:1].upper()
       
   458     time_diff = record.created - self._start_time
       
   459     outfile.write('<span class="_ah_logline_%s">\n' % level_name)
       
   460     outfile.write('<span class="_ah_logline_%s_prefix">%2.5f %s &gt;</span>\n'
       
   461                   % (level_name, time_diff, level_letter))
       
   462     outfile.write('%s\n' % message)
       
   463     outfile.write('</span>\n')
       
   464 
       
   465 
       
   466 _IGNORE_HEADERS = frozenset(['content-type', 'content-length'])
       
   467 
       
   468 def SetupEnvironment(cgi_path,
       
   469                      relative_url,
       
   470                      headers,
       
   471                      split_url=SplitURL,
       
   472                      get_user_info=dev_appserver_login.GetUserInfo):
       
   473   """Sets up environment variables for a CGI.
       
   474 
       
   475   Args:
       
   476     cgi_path: Full file-system path to the CGI being executed.
       
   477     relative_url: Relative URL used to access the CGI.
       
   478     headers: Instance of mimetools.Message containing request headers.
       
   479     split_url, get_user_info: Used for dependency injection.
       
   480 
       
   481   Returns:
       
   482     Dictionary containing CGI environment variables.
       
   483   """
       
   484   env = DEFAULT_ENV.copy()
       
   485 
       
   486   script_name, query_string = split_url(relative_url)
       
   487 
       
   488   env['SCRIPT_NAME'] = ''
       
   489   env['QUERY_STRING'] = query_string
       
   490   env['PATH_INFO'] = urllib.unquote(script_name)
       
   491   env['PATH_TRANSLATED'] = cgi_path
       
   492   env['CONTENT_TYPE'] = headers.getheader('content-type',
       
   493                                           'application/x-www-form-urlencoded')
       
   494   env['CONTENT_LENGTH'] = headers.getheader('content-length', '')
       
   495 
       
   496   cookies = ', '.join(headers.getheaders('cookie'))
       
   497   email, admin = get_user_info(cookies)
       
   498   env['USER_EMAIL'] = email
       
   499   if admin:
       
   500     env['USER_IS_ADMIN'] = '1'
       
   501 
       
   502   for key in headers:
       
   503     if key in _IGNORE_HEADERS:
       
   504       continue
       
   505     adjusted_name = key.replace('-', '_').upper()
       
   506     env['HTTP_' + adjusted_name] = ', '.join(headers.getheaders(key))
       
   507 
       
   508   return env
       
   509 
       
   510 
       
   511 def FakeTemporaryFile(*args, **kwargs):
       
   512   """Fake for tempfile.TemporaryFile that just uses StringIO."""
       
   513   return cStringIO.StringIO()
       
   514 
       
   515 
       
   516 def NotImplementedFake(*args, **kwargs):
       
   517   """Fake for methods/classes that are not implemented in the production
       
   518   environment.
       
   519   """
       
   520   raise NotImplementedError("This class/method is not available.")
       
   521 
       
   522 
       
   523 def IsEncodingsModule(module_name):
       
   524   """Determines if the supplied module is related to encodings in any way.
       
   525 
       
   526   Encodings-related modules cannot be reloaded, so they need to be treated
       
   527   specially when sys.modules is modified in any way.
       
   528 
       
   529   Args:
       
   530     module_name: Absolute name of the module regardless of how it is imported
       
   531       into the local namespace (e.g., foo.bar.baz).
       
   532 
       
   533   Returns:
       
   534     True if it's an encodings-related module; False otherwise.
       
   535   """
       
   536   if (module_name in ('codecs', 'encodings') or
       
   537       module_name.startswith('encodings.')):
       
   538     return True
       
   539   return False
       
   540 
       
   541 
       
   542 def ClearAllButEncodingsModules(module_dict):
       
   543   """Clear all modules in a module dictionary except for those modules that
       
   544   are in any way related to encodings.
       
   545 
       
   546   Args:
       
   547     module_dict: Dictionary in the form used by sys.modules.
       
   548   """
       
   549   for module_name in module_dict.keys():
       
   550     if not IsEncodingsModule(module_name):
       
   551       del module_dict[module_name]
       
   552 
       
   553 
       
   554 def FakeURandom(n):
       
   555   """Fake version of os.urandom."""
       
   556   bytes = ''
       
   557   for i in xrange(n):
       
   558     bytes += chr(random.randint(0, 255))
       
   559   return bytes
       
   560 
       
   561 
       
   562 def FakeUname():
       
   563   """Fake version of os.uname."""
       
   564   return ('Linux', '', '', '', '')
       
   565 
       
   566 
       
   567 def IsPathInSubdirectories(filename,
       
   568                            subdirectories,
       
   569                            normcase=os.path.normcase):
       
   570   """Determines if a filename is contained within one of a set of directories.
       
   571 
       
   572   Args:
       
   573     filename: Path of the file (relative or absolute).
       
   574     subdirectories: Iterable collection of paths to subdirectories which the
       
   575       given filename may be under.
       
   576     normcase: Used for dependency injection.
       
   577 
       
   578   Returns:
       
   579     True if the supplied filename is in one of the given sub-directories or
       
   580     its hierarchy of children. False otherwise.
       
   581   """
       
   582   file_dir = normcase(os.path.dirname(os.path.abspath(filename)))
       
   583   for parent in subdirectories:
       
   584     fixed_parent = normcase(os.path.abspath(parent))
       
   585     if os.path.commonprefix([file_dir, fixed_parent]) == fixed_parent:
       
   586       return True
       
   587   return False
       
   588 
       
   589 SHARED_MODULE_PREFIXES = set([
       
   590   'google',
       
   591   'logging',
       
   592   'sys',
       
   593   'warnings',
       
   594 
       
   595 
       
   596 
       
   597 
       
   598   're',
       
   599   'sre_compile',
       
   600   'sre_constants',
       
   601   'sre_parse',
       
   602 
       
   603 
       
   604 
       
   605 
       
   606   'wsgiref',
       
   607 ])
       
   608 
       
   609 NOT_SHARED_MODULE_PREFIXES = set([
       
   610   'google.appengine.ext',
       
   611 ])
       
   612 
       
   613 
       
   614 def ModuleNameHasPrefix(module_name, prefix_set):
       
   615   """Determines if a module's name belongs to a set of prefix strings.
       
   616 
       
   617   Args:
       
   618     module_name: String containing the fully qualified module name.
       
   619     prefix_set: Iterable set of module name prefixes to check against.
       
   620 
       
   621   Returns:
       
   622     True if the module_name belongs to the prefix set or is a submodule of
       
   623     any of the modules specified in the prefix_set. Otherwise False.
       
   624   """
       
   625   for prefix in prefix_set:
       
   626     if prefix == module_name:
       
   627       return True
       
   628 
       
   629     if module_name.startswith(prefix + '.'):
       
   630       return True
       
   631 
       
   632   return False
       
   633 
       
   634 
       
   635 def SetupSharedModules(module_dict):
       
   636   """Creates a module dictionary for the hardened part of the process.
       
   637 
       
   638   Module dictionary will contain modules that should be shared between the
       
   639   hardened and unhardened parts of the process.
       
   640 
       
   641   Args:
       
   642     module_dict: Module dictionary from which existing modules should be
       
   643       pulled (usually sys.modules).
       
   644 
       
   645   Returns:
       
   646     A new module dictionary.
       
   647   """
       
   648   output_dict = {}
       
   649   for module_name, module in module_dict.iteritems():
       
   650     if module is None:
       
   651       continue
       
   652 
       
   653     if IsEncodingsModule(module_name):
       
   654       output_dict[module_name] = module
       
   655       continue
       
   656 
       
   657     shared_prefix = ModuleNameHasPrefix(module_name, SHARED_MODULE_PREFIXES)
       
   658     banned_prefix = ModuleNameHasPrefix(module_name, NOT_SHARED_MODULE_PREFIXES)
       
   659 
       
   660     if shared_prefix and not banned_prefix:
       
   661       output_dict[module_name] = module
       
   662 
       
   663   return output_dict
       
   664 
       
   665 
       
   666 class FakeFile(file):
       
   667   """File sub-class that enforces the security restrictions of the production
       
   668   environment.
       
   669   """
       
   670 
       
   671   ALLOWED_MODES = frozenset(['r', 'rb', 'U', 'rU'])
       
   672 
       
   673   ALLOWED_FILES = set(os.path.normcase(filename)
       
   674                       for filename in mimetypes.knownfiles
       
   675                       if os.path.isfile(filename))
       
   676 
       
   677   ALLOWED_DIRS = set([
       
   678     os.path.normcase(os.path.abspath(os.path.dirname(os.__file__)))
       
   679   ])
       
   680 
       
   681   NOT_ALLOWED_DIRS = set([
       
   682 
       
   683 
       
   684 
       
   685 
       
   686     os.path.normcase(os.path.join(os.path.dirname(os.__file__),
       
   687                                   'site-packages'))
       
   688   ])
       
   689 
       
   690   ALLOWED_SITE_PACKAGE_DIRS = set(
       
   691     os.path.normcase(os.path.abspath(os.path.join(
       
   692       os.path.dirname(os.__file__), 'site-packages', path)))
       
   693     for path in [
       
   694 
       
   695   ])
       
   696 
       
   697   _application_paths = None
       
   698   _original_file = file
       
   699 
       
   700   @staticmethod
       
   701   def SetAllowedPaths(application_paths):
       
   702     """Sets the root path of the application that is currently running.
       
   703 
       
   704     Must be called at least once before any file objects are created in the
       
   705     hardened environment.
       
   706 
       
   707     Args:
       
   708       root_path: Path to the root of the application.
       
   709     """
       
   710     FakeFile._application_paths = set(os.path.abspath(path)
       
   711                                       for path in application_paths)
       
   712 
       
   713   @staticmethod
       
   714   def IsFileAccessible(filename, normcase=os.path.normcase):
       
   715     """Determines if a file's path is accessible.
       
   716 
       
   717     SetAllowedPaths() must be called before this method or else all file
       
   718     accesses will raise an error.
       
   719 
       
   720     Args:
       
   721       filename: Path of the file to check (relative or absolute). May be a
       
   722         directory, in which case access for files inside that directory will
       
   723         be checked.
       
   724       normcase: Used for dependency injection.
       
   725 
       
   726     Returns:
       
   727       True if the file is accessible, False otherwise.
       
   728     """
       
   729     logical_filename = normcase(os.path.abspath(filename))
       
   730 
       
   731     if os.path.isdir(logical_filename):
       
   732       logical_filename = os.path.join(logical_filename, 'foo')
       
   733 
       
   734     if logical_filename in FakeFile.ALLOWED_FILES:
       
   735       return True
       
   736 
       
   737     if IsPathInSubdirectories(logical_filename,
       
   738                               FakeFile.ALLOWED_SITE_PACKAGE_DIRS,
       
   739                               normcase=normcase):
       
   740       return True
       
   741 
       
   742     allowed_dirs = FakeFile._application_paths | FakeFile.ALLOWED_DIRS
       
   743     if (IsPathInSubdirectories(logical_filename,
       
   744                                allowed_dirs,
       
   745                                normcase=normcase) and
       
   746         not IsPathInSubdirectories(logical_filename,
       
   747                                    FakeFile.NOT_ALLOWED_DIRS,
       
   748                                    normcase=normcase)):
       
   749       return True
       
   750 
       
   751     return False
       
   752 
       
   753   def __init__(self, filename, mode='r', **kwargs):
       
   754     """Initializer. See file built-in documentation."""
       
   755     if mode not in FakeFile.ALLOWED_MODES:
       
   756       raise IOError('invalid mode: %s' % mode)
       
   757 
       
   758     if not FakeFile.IsFileAccessible(filename):
       
   759       raise IOError(errno.EACCES, 'file not accessible')
       
   760 
       
   761     super(FakeFile, self).__init__(filename, mode, **kwargs)
       
   762 
       
   763 
       
   764 class RestrictedPathFunction(object):
       
   765   """Enforces access restrictions for functions that have a file or
       
   766   directory path as their first argument."""
       
   767 
       
   768   _original_os = os
       
   769 
       
   770   def __init__(self, original_func):
       
   771     """Initializer.
       
   772 
       
   773     Args:
       
   774       original_func: Callable that takes as its first argument the path to a
       
   775         file or directory on disk; all subsequent arguments may be variable.
       
   776     """
       
   777     self._original_func = original_func
       
   778 
       
   779   def __call__(self, path, *args, **kwargs):
       
   780     """Enforces access permissions for the function passed to the constructor.
       
   781     """
       
   782     if not FakeFile.IsFileAccessible(path):
       
   783       raise OSError(errno.EACCES, 'path not accessible')
       
   784 
       
   785     return self._original_func(path, *args, **kwargs)
       
   786 
       
   787 
       
   788 def GetSubmoduleName(fullname):
       
   789   """Determines the leaf submodule name of a full module name.
       
   790 
       
   791   Args:
       
   792     fullname: Fully qualified module name, e.g. 'foo.bar.baz'
       
   793 
       
   794   Returns:
       
   795     Submodule name, e.g. 'baz'. If the supplied module has no submodule (e.g.,
       
   796     'stuff'), the returned value will just be that module name ('stuff').
       
   797   """
       
   798   return fullname.rsplit('.', 1)[-1]
       
   799 
       
   800 
       
   801 class CouldNotFindModuleError(ImportError):
       
   802   """Raised when a module could not be found.
       
   803 
       
   804   In contrast to when a module has been found, but cannot be loaded because of
       
   805   hardening restrictions.
       
   806   """
       
   807 
       
   808 
       
   809 def Trace(func):
       
   810   """Decorator that logs the call stack of the HardenedModulesHook class as
       
   811   it executes, indenting logging messages based on the current stack depth.
       
   812   """
       
   813   def decorate(self, *args, **kwargs):
       
   814     args_to_show = []
       
   815     if args is not None:
       
   816       args_to_show.extend(str(argument) for argument in args)
       
   817     if kwargs is not None:
       
   818       args_to_show.extend('%s=%s' % (key, value)
       
   819                           for key, value in kwargs.iteritems())
       
   820 
       
   821     args_string = ', '.join(args_to_show)
       
   822 
       
   823     self.log('Entering %s(%s)', func.func_name, args_string)
       
   824     self._indent_level += 1
       
   825     try:
       
   826       return func(self, *args, **kwargs)
       
   827     finally:
       
   828       self._indent_level -= 1
       
   829       self.log('Exiting %s(%s)', func.func_name, args_string)
       
   830 
       
   831   return decorate
       
   832 
       
   833 
       
   834 class HardenedModulesHook(object):
       
   835   """Meta import hook that restricts the modules used by applications to match
       
   836   the production environment.
       
   837 
       
   838   Module controls supported:
       
   839   - Disallow native/extension modules from being loaded
       
   840   - Disallow built-in and/or Python-distributed modules from being loaded
       
   841   - Replace modules with completely empty modules
       
   842   - Override specific module attributes
       
   843   - Replace one module with another
       
   844 
       
   845   After creation, this object should be added to the front of the sys.meta_path
       
   846   list (which may need to be created). The sys.path_importer_cache dictionary
       
   847   should also be cleared, to prevent loading any non-restricted modules.
       
   848 
       
   849   See PEP302 for more info on how this works:
       
   850     http://www.python.org/dev/peps/pep-0302/
       
   851   """
       
   852 
       
   853   ENABLE_LOGGING = False
       
   854 
       
   855   def log(self, message, *args):
       
   856     """Logs an import-related message to stderr, with indentation based on
       
   857     current call-stack depth.
       
   858 
       
   859     Args:
       
   860       message: Logging format string.
       
   861       args: Positional format parameters for the logging message.
       
   862     """
       
   863     if HardenedModulesHook.ENABLE_LOGGING:
       
   864       indent = self._indent_level * '  '
       
   865       print >>sys.stderr, indent + (message % args)
       
   866 
       
   867   EMPTY_MODULE_FILE = '<empty module>'
       
   868 
       
   869   _WHITE_LIST_C_MODULES = [
       
   870     'array',
       
   871     'binascii',
       
   872     'bz2',
       
   873     'cmath',
       
   874     'collections',
       
   875     'crypt',
       
   876     'cStringIO',
       
   877     'datetime',
       
   878     'errno',
       
   879     'exceptions',
       
   880     'gc',
       
   881     'itertools',
       
   882     'math',
       
   883     'md5',
       
   884     'operator',
       
   885     'posix',
       
   886     'posixpath',
       
   887     'pyexpat',
       
   888     'sha',
       
   889     'struct',
       
   890     'sys',
       
   891     'time',
       
   892     'timing',
       
   893     'unicodedata',
       
   894     'zlib',
       
   895     '_bisect',
       
   896     '_codecs',
       
   897     '_codecs_cn',
       
   898     '_codecs_hk',
       
   899     '_codecs_iso2022',
       
   900     '_codecs_jp',
       
   901     '_codecs_kr',
       
   902     '_codecs_tw',
       
   903     '_csv',
       
   904     '_elementtree',
       
   905     '_functools',
       
   906     '_hashlib',
       
   907     '_heapq',
       
   908     '_locale',
       
   909     '_lsprof',
       
   910     '_md5',
       
   911     '_multibytecodec',
       
   912     '_random',
       
   913     '_sha',
       
   914     '_sha256',
       
   915     '_sha512',
       
   916     '_sre',
       
   917     '_struct',
       
   918     '_types',
       
   919     '_weakref',
       
   920     '__main__',
       
   921   ]
       
   922 
       
   923   _WHITE_LIST_PARTIAL_MODULES = {
       
   924     'gc': [
       
   925       'enable',
       
   926       'disable',
       
   927       'isenabled',
       
   928       'collect',
       
   929       'get_debug',
       
   930       'set_threshold',
       
   931       'get_threshold',
       
   932       'get_count'
       
   933     ],
       
   934 
       
   935 
       
   936 
       
   937     'os': [
       
   938       'altsep',
       
   939       'curdir',
       
   940       'defpath',
       
   941       'devnull',
       
   942       'environ',
       
   943       'error',
       
   944       'extsep',
       
   945       'EX_NOHOST',
       
   946       'EX_NOINPUT',
       
   947       'EX_NOPERM',
       
   948       'EX_NOUSER',
       
   949       'EX_OK',
       
   950       'EX_OSERR',
       
   951       'EX_OSFILE',
       
   952       'EX_PROTOCOL',
       
   953       'EX_SOFTWARE',
       
   954       'EX_TEMPFAIL',
       
   955       'EX_UNAVAILABLE',
       
   956       'EX_USAGE',
       
   957       'F_OK',
       
   958       'getcwd',
       
   959       'getcwdu',
       
   960       'getenv',
       
   961       'listdir',
       
   962       'lstat',
       
   963       'name',
       
   964       'NGROUPS_MAX',
       
   965       'O_APPEND',
       
   966       'O_CREAT',
       
   967       'O_DIRECT',
       
   968       'O_DIRECTORY',
       
   969       'O_DSYNC',
       
   970       'O_EXCL',
       
   971       'O_LARGEFILE',
       
   972       'O_NDELAY',
       
   973       'O_NOCTTY',
       
   974       'O_NOFOLLOW',
       
   975       'O_NONBLOCK',
       
   976       'O_RDONLY',
       
   977       'O_RDWR',
       
   978       'O_RSYNC',
       
   979       'O_SYNC',
       
   980       'O_TRUNC',
       
   981       'O_WRONLY',
       
   982       'pardir',
       
   983       'path',
       
   984       'pathsep',
       
   985       'R_OK',
       
   986       'SEEK_CUR',
       
   987       'SEEK_END',
       
   988       'SEEK_SET',
       
   989       'sep',
       
   990       'stat',
       
   991       'stat_float_times',
       
   992       'stat_result',
       
   993       'strerror',
       
   994       'TMP_MAX',
       
   995       'urandom',
       
   996       'walk',
       
   997       'WCOREDUMP',
       
   998       'WEXITSTATUS',
       
   999       'WIFEXITED',
       
  1000       'WIFSIGNALED',
       
  1001       'WIFSTOPPED',
       
  1002       'WNOHANG',
       
  1003       'WSTOPSIG',
       
  1004       'WTERMSIG',
       
  1005       'WUNTRACED',
       
  1006       'W_OK',
       
  1007       'X_OK',
       
  1008     ],
       
  1009   }
       
  1010 
       
  1011   _EMPTY_MODULES = [
       
  1012     'imp',
       
  1013     'ftplib',
       
  1014     'select',
       
  1015     'socket',
       
  1016     'tempfile',
       
  1017   ]
       
  1018 
       
  1019   _MODULE_OVERRIDES = {
       
  1020     'os': {
       
  1021       'listdir': RestrictedPathFunction(os.listdir),
       
  1022       'lstat': RestrictedPathFunction(os.lstat),
       
  1023       'stat': RestrictedPathFunction(os.stat),
       
  1024       'uname': FakeUname,
       
  1025       'urandom': FakeURandom,
       
  1026     },
       
  1027 
       
  1028     'socket': {
       
  1029       'AF_INET': None,
       
  1030       'SOCK_STREAM': None,
       
  1031       'SOCK_DGRAM': None,
       
  1032     },
       
  1033 
       
  1034     'tempfile': {
       
  1035       'TemporaryFile': FakeTemporaryFile,
       
  1036       'gettempdir': NotImplementedFake,
       
  1037       'gettempprefix': NotImplementedFake,
       
  1038       'mkdtemp': NotImplementedFake,
       
  1039       'mkstemp': NotImplementedFake,
       
  1040       'mktemp': NotImplementedFake,
       
  1041       'NamedTemporaryFile': NotImplementedFake,
       
  1042       'tempdir': NotImplementedFake,
       
  1043     },
       
  1044   }
       
  1045 
       
  1046   _ENABLED_FILE_TYPES = (
       
  1047     imp.PKG_DIRECTORY,
       
  1048     imp.PY_SOURCE,
       
  1049     imp.PY_COMPILED,
       
  1050     imp.C_BUILTIN,
       
  1051   )
       
  1052 
       
  1053   def __init__(self,
       
  1054                module_dict,
       
  1055                imp_module=imp,
       
  1056                os_module=os,
       
  1057                dummy_thread_module=dummy_thread,
       
  1058                pickle_module=pickle):
       
  1059     """Initializer.
       
  1060 
       
  1061     Args:
       
  1062       module_dict: Module dictionary to use for managing system modules.
       
  1063         Should be sys.modules.
       
  1064       imp_module, os_module, dummy_thread_module, pickle_module: References to
       
  1065         modules that exist in the dev_appserver that must be used by this class
       
  1066         in order to function, even if these modules have been unloaded from
       
  1067         sys.modules.
       
  1068     """
       
  1069     self._module_dict = module_dict
       
  1070     self._imp = imp_module
       
  1071     self._os = os_module
       
  1072     self._dummy_thread = dummy_thread_module
       
  1073     self._pickle = pickle
       
  1074     self._indent_level = 0
       
  1075 
       
  1076   @Trace
       
  1077   def find_module(self, fullname, path=None):
       
  1078     """See PEP 302."""
       
  1079     if (fullname in ('cPickle', 'thread') or
       
  1080         fullname in HardenedModulesHook._EMPTY_MODULES):
       
  1081       return self
       
  1082 
       
  1083     search_path = path
       
  1084     all_modules = fullname.split('.')
       
  1085     try:
       
  1086       for index, current_module in enumerate(all_modules):
       
  1087         current_module_fullname = '.'.join(all_modules[:index + 1])
       
  1088         if current_module_fullname == fullname:
       
  1089           self.FindModuleRestricted(current_module,
       
  1090                                     current_module_fullname,
       
  1091                                     search_path)
       
  1092         else:
       
  1093           if current_module_fullname in self._module_dict:
       
  1094             module = self._module_dict[current_module_fullname]
       
  1095           else:
       
  1096             module = self.FindAndLoadModule(current_module,
       
  1097                                             current_module_fullname,
       
  1098                                             search_path)
       
  1099 
       
  1100           if hasattr(module, '__path__'):
       
  1101             search_path = module.__path__
       
  1102     except CouldNotFindModuleError:
       
  1103       return None
       
  1104 
       
  1105     return self
       
  1106 
       
  1107   @Trace
       
  1108   def FixModule(self, module):
       
  1109     """Prunes and overrides restricted module attributes.
       
  1110 
       
  1111     Args:
       
  1112       module: The module to prune. This should be a new module whose attributes
       
  1113         reference back to the real module's __dict__ members.
       
  1114     """
       
  1115     if module.__name__ in self._WHITE_LIST_PARTIAL_MODULES:
       
  1116       allowed_symbols = self._WHITE_LIST_PARTIAL_MODULES[module.__name__]
       
  1117       for symbol in set(module.__dict__) - set(allowed_symbols):
       
  1118         if not (symbol.startswith('__') and symbol.endswith('__')):
       
  1119           del module.__dict__[symbol]
       
  1120 
       
  1121     if module.__name__ in self._MODULE_OVERRIDES:
       
  1122       module.__dict__.update(self._MODULE_OVERRIDES[module.__name__])
       
  1123 
       
  1124   @Trace
       
  1125   def FindModuleRestricted(self,
       
  1126                            submodule,
       
  1127                            submodule_fullname,
       
  1128                            search_path):
       
  1129     """Locates a module while enforcing module import restrictions.
       
  1130 
       
  1131     Args:
       
  1132       submodule: The short name of the submodule (i.e., the last section of
       
  1133         the fullname; for 'foo.bar' this would be 'bar').
       
  1134       submodule_fullname: The fully qualified name of the module to find (e.g.,
       
  1135         'foo.bar').
       
  1136       search_path: List of paths to search for to find this module. Should be
       
  1137         None if the current sys.path should be used.
       
  1138 
       
  1139     Returns:
       
  1140       Tuple (source_file, pathname, description) where:
       
  1141         source_file: File-like object that contains the module; in the case
       
  1142           of packages, this will be None, which implies to look at __init__.py.
       
  1143         pathname: String containing the full path of the module on disk.
       
  1144         description: Tuple returned by imp.find_module().
       
  1145 
       
  1146     Raises:
       
  1147       ImportError exception if the requested module was found, but importing
       
  1148       it is disallowed.
       
  1149 
       
  1150       CouldNotFindModuleError exception if the request module could not even
       
  1151       be found for import.
       
  1152     """
       
  1153     try:
       
  1154       source_file, pathname, description = self._imp.find_module(submodule, search_path)
       
  1155     except ImportError:
       
  1156       self.log('Could not find module "%s"', submodule_fullname)
       
  1157       raise CouldNotFindModuleError()
       
  1158 
       
  1159     suffix, mode, file_type = description
       
  1160 
       
  1161     if (file_type not in (self._imp.C_BUILTIN, self._imp.C_EXTENSION) and
       
  1162         not FakeFile.IsFileAccessible(pathname)):
       
  1163       error_message = 'Access to module file denied: %s' % pathname
       
  1164       logging.debug(error_message)
       
  1165       raise ImportError(error_message)
       
  1166 
       
  1167     if (file_type not in self._ENABLED_FILE_TYPES and
       
  1168         submodule not in self._WHITE_LIST_C_MODULES):
       
  1169       error_message = ('Could not import "%s": Disallowed C-extension '
       
  1170                        'or built-in module' % submodule_fullname)
       
  1171       logging.debug(error_message)
       
  1172       raise ImportError(error_message)
       
  1173 
       
  1174     return source_file, pathname, description
       
  1175 
       
  1176   @Trace
       
  1177   def LoadModuleRestricted(self,
       
  1178                            submodule_fullname,
       
  1179                            source_file,
       
  1180                            pathname,
       
  1181                            description):
       
  1182     """Loads a module while enforcing module import restrictions.
       
  1183 
       
  1184     As a byproduct, the new module will be added to the module dictionary.
       
  1185 
       
  1186     Args:
       
  1187       submodule_fullname: The fully qualified name of the module to find (e.g.,
       
  1188         'foo.bar').
       
  1189       source_file: File-like object that contains the module's source code.
       
  1190       pathname: String containing the full path of the module on disk.
       
  1191       description: Tuple returned by imp.find_module().
       
  1192 
       
  1193     Returns:
       
  1194       The new module.
       
  1195 
       
  1196     Raises:
       
  1197       ImportError exception of the specified module could not be loaded for
       
  1198       whatever reason.
       
  1199     """
       
  1200     try:
       
  1201       try:
       
  1202         return self._imp.load_module(submodule_fullname,
       
  1203                                      source_file,
       
  1204                                      pathname,
       
  1205                                      description)
       
  1206       except:
       
  1207         if submodule_fullname in self._module_dict:
       
  1208           del self._module_dict[submodule_fullname]
       
  1209         raise
       
  1210 
       
  1211     finally:
       
  1212       if source_file is not None:
       
  1213         source_file.close()
       
  1214 
       
  1215   @Trace
       
  1216   def FindAndLoadModule(self,
       
  1217                         submodule,
       
  1218                         submodule_fullname,
       
  1219                         search_path):
       
  1220     """Finds and loads a module, loads it, and adds it to the module dictionary.
       
  1221 
       
  1222     Args:
       
  1223       submodule: Name of the module to import (e.g., baz).
       
  1224       submodule_fullname: Full name of the module to import (e.g., foo.bar.baz).
       
  1225       search_path: Path to use for searching for this submodule. For top-level
       
  1226         modules this should be None; otherwise it should be the __path__
       
  1227         attribute from the parent package.
       
  1228 
       
  1229     Returns:
       
  1230       A new module instance that has been inserted into the module dictionary
       
  1231       supplied to __init__.
       
  1232 
       
  1233     Raises:
       
  1234       ImportError exception if the module could not be loaded for whatever
       
  1235       reason (e.g., missing, not allowed).
       
  1236     """
       
  1237     module = self._imp.new_module(submodule_fullname)
       
  1238 
       
  1239     if submodule_fullname in self._EMPTY_MODULES:
       
  1240       module.__file__ = self.EMPTY_MODULE_FILE
       
  1241     elif submodule_fullname == 'thread':
       
  1242       module.__dict__.update(self._dummy_thread.__dict__)
       
  1243       module.__name__ = 'thread'
       
  1244     elif submodule_fullname == 'cPickle':
       
  1245       module.__dict__.update(self._pickle.__dict__)
       
  1246       module.__name__ = 'cPickle'
       
  1247     elif submodule_fullname == 'os':
       
  1248       module.__dict__.update(self._os.__dict__)
       
  1249       self._module_dict['os.path'] = module.path
       
  1250     else:
       
  1251       source_file, pathname, description = self.FindModuleRestricted(submodule, submodule_fullname, search_path)
       
  1252       module = self.LoadModuleRestricted(submodule_fullname,
       
  1253                                          source_file,
       
  1254                                          pathname,
       
  1255                                          description)
       
  1256 
       
  1257     module.__loader__ = self
       
  1258     self.FixModule(module)
       
  1259     if submodule_fullname not in self._module_dict:
       
  1260       self._module_dict[submodule_fullname] = module
       
  1261 
       
  1262     return module
       
  1263 
       
  1264   @Trace
       
  1265   def GetParentPackage(self, fullname):
       
  1266     """Retrieves the parent package of a fully qualified module name.
       
  1267 
       
  1268     Args:
       
  1269       fullname: Full name of the module whose parent should be retrieved (e.g.,
       
  1270         foo.bar).
       
  1271 
       
  1272     Returns:
       
  1273       Module instance for the parent or None if there is no parent module.
       
  1274 
       
  1275     Raise:
       
  1276       ImportError exception if the module's parent could not be found.
       
  1277     """
       
  1278     all_modules = fullname.split('.')
       
  1279     parent_module_fullname = '.'.join(all_modules[:-1])
       
  1280     if parent_module_fullname:
       
  1281       if self.find_module(fullname) is None:
       
  1282         raise ImportError('Could not find module %s' % fullname)
       
  1283 
       
  1284       return self._module_dict[parent_module_fullname]
       
  1285     return None
       
  1286 
       
  1287   @Trace
       
  1288   def GetParentSearchPath(self, fullname):
       
  1289     """Determines the search path of a module's parent package.
       
  1290 
       
  1291     Args:
       
  1292       fullname: Full name of the module to look up (e.g., foo.bar).
       
  1293 
       
  1294     Returns:
       
  1295       Tuple (submodule, search_path) where:
       
  1296         submodule: The last portion of the module name from fullname (e.g.,
       
  1297           if fullname is foo.bar, then this is bar).
       
  1298         search_path: List of paths that belong to the parent package's search
       
  1299           path or None if there is no parent package.
       
  1300 
       
  1301     Raises:
       
  1302       ImportError exception if the module or its parent could not be found.
       
  1303     """
       
  1304     submodule = GetSubmoduleName(fullname)
       
  1305     parent_package = self.GetParentPackage(fullname)
       
  1306     search_path = None
       
  1307     if parent_package is not None and hasattr(parent_package, '__path__'):
       
  1308       search_path = parent_package.__path__
       
  1309     return submodule, search_path
       
  1310 
       
  1311   @Trace
       
  1312   def GetModuleInfo(self, fullname):
       
  1313     """Determines the path on disk and the search path of a module or package.
       
  1314 
       
  1315     Args:
       
  1316       fullname: Full name of the module to look up (e.g., foo.bar).
       
  1317 
       
  1318     Returns:
       
  1319       Tuple (pathname, search_path, submodule) where:
       
  1320         pathname: String containing the full path of the module on disk.
       
  1321         search_path: List of paths that belong to the found package's search
       
  1322           path or None if found module is not a package.
       
  1323         submodule: The relative name of the submodule that's being imported.
       
  1324     """
       
  1325     submodule, search_path = self.GetParentSearchPath(fullname)
       
  1326     source_file, pathname, description = self.FindModuleRestricted(submodule, fullname, search_path)
       
  1327     suffix, mode, file_type = description
       
  1328     module_search_path = None
       
  1329     if file_type == self._imp.PKG_DIRECTORY:
       
  1330       module_search_path = [pathname]
       
  1331       pathname = os.path.join(pathname, '__init__%spy' % os.extsep)
       
  1332     return pathname, module_search_path, submodule
       
  1333 
       
  1334   @Trace
       
  1335   def load_module(self, fullname):
       
  1336     """See PEP 302."""
       
  1337     all_modules = fullname.split('.')
       
  1338     submodule = all_modules[-1]
       
  1339     parent_module_fullname = '.'.join(all_modules[:-1])
       
  1340     search_path = None
       
  1341     if parent_module_fullname and parent_module_fullname in self._module_dict:
       
  1342       parent_module = self._module_dict[parent_module_fullname]
       
  1343       if hasattr(parent_module, '__path__'):
       
  1344         search_path = parent_module.__path__
       
  1345 
       
  1346     return self.FindAndLoadModule(submodule, fullname, search_path)
       
  1347 
       
  1348   @Trace
       
  1349   def is_package(self, fullname):
       
  1350     """See PEP 302 extensions."""
       
  1351     submodule, search_path = self.GetParentSearchPath(fullname)
       
  1352     source_file, pathname, description = self.FindModuleRestricted(submodule, fullname, search_path)
       
  1353     suffix, mode, file_type = description
       
  1354     if file_type == self._imp.PKG_DIRECTORY:
       
  1355       return True
       
  1356     return False
       
  1357 
       
  1358   @Trace
       
  1359   def get_source(self, fullname):
       
  1360     """See PEP 302 extensions."""
       
  1361     full_path, search_path, submodule = self.GetModuleInfo(fullname)
       
  1362     source_file = open(full_path)
       
  1363     try:
       
  1364       return source_file.read()
       
  1365     finally:
       
  1366       source_file.close()
       
  1367 
       
  1368   @Trace
       
  1369   def get_code(self, fullname):
       
  1370     """See PEP 302 extensions."""
       
  1371     full_path, search_path, submodule = self.GetModuleInfo(fullname)
       
  1372     source_file = open(full_path)
       
  1373     try:
       
  1374       source_code = source_file.read()
       
  1375     finally:
       
  1376       source_file.close()
       
  1377 
       
  1378     source_code = source_code.replace('\r\n', '\n')
       
  1379     if not source_code.endswith('\n'):
       
  1380       source_code += '\n'
       
  1381 
       
  1382     return compile(source_code, full_path, 'exec')
       
  1383 
       
  1384 
       
  1385 def ModuleHasValidMainFunction(module):
       
  1386   """Determines if a module has a main function that takes no arguments.
       
  1387 
       
  1388   This includes functions that have arguments with defaults that are all
       
  1389   assigned, thus requiring no additional arguments in order to be called.
       
  1390 
       
  1391   Args:
       
  1392     module: A types.ModuleType instance.
       
  1393 
       
  1394   Returns:
       
  1395     True if the module has a valid, reusable main function; False otherwise.
       
  1396   """
       
  1397   if hasattr(module, 'main') and type(module.main) is types.FunctionType:
       
  1398     arg_names, var_args, var_kwargs, default_values = inspect.getargspec(module.main)
       
  1399     if len(arg_names) == 0:
       
  1400       return True
       
  1401     if default_values is not None and len(arg_names) == len(default_values):
       
  1402       return True
       
  1403   return False
       
  1404 
       
  1405 
       
  1406 def GetScriptModuleName(handler_path):
       
  1407   """Determines the fully-qualified Python module name of a script on disk.
       
  1408 
       
  1409   Args:
       
  1410     handler_path: CGI path stored in the application configuration (as a path
       
  1411       like 'foo/bar/baz.py'). May contain $PYTHON_LIB references.
       
  1412 
       
  1413   Returns:
       
  1414     String containing the corresponding module name (e.g., 'foo.bar.baz').
       
  1415   """
       
  1416   if handler_path.startswith(PYTHON_LIB_VAR + '/'):
       
  1417     handler_path = handler_path[len(PYTHON_LIB_VAR):]
       
  1418   handler_path = os.path.normpath(handler_path)
       
  1419 
       
  1420   extension_index = handler_path.rfind('.py')
       
  1421   if extension_index != -1:
       
  1422     handler_path = handler_path[:extension_index]
       
  1423   module_fullname = handler_path.replace(os.sep, '.')
       
  1424   module_fullname = module_fullname.strip('.')
       
  1425   module_fullname = re.sub('\.+', '.', module_fullname)
       
  1426 
       
  1427   if module_fullname.endswith('.__init__'):
       
  1428     module_fullname = module_fullname[:-len('.__init__')]
       
  1429 
       
  1430   return module_fullname
       
  1431 
       
  1432 
       
  1433 def FindMissingInitFiles(cgi_path, module_fullname, isfile=os.path.isfile):
       
  1434   """Determines which __init__.py files are missing from a module's parent
       
  1435   packages.
       
  1436 
       
  1437   Args:
       
  1438     cgi_path: Absolute path of the CGI module file on disk.
       
  1439     module_fullname: Fully qualified Python module name used to import the
       
  1440       cgi_path module.
       
  1441 
       
  1442   Returns:
       
  1443     List containing the paths to the missing __init__.py files.
       
  1444   """
       
  1445   missing_init_files = []
       
  1446 
       
  1447   if cgi_path.endswith('.py'):
       
  1448     module_base = os.path.dirname(cgi_path)
       
  1449   else:
       
  1450     module_base = cgi_path
       
  1451 
       
  1452   depth_count = module_fullname.count('.')
       
  1453   if cgi_path.endswith('__init__.py') or not cgi_path.endswith('.py'):
       
  1454     depth_count += 1
       
  1455 
       
  1456   for index in xrange(depth_count):
       
  1457     current_init_file = os.path.join(module_base, '__init__.py')
       
  1458 
       
  1459     if not isfile(current_init_file):
       
  1460       missing_init_files.append(current_init_file)
       
  1461 
       
  1462     module_base = os.path.abspath(os.path.join(module_base, os.pardir))
       
  1463 
       
  1464   return missing_init_files
       
  1465 
       
  1466 
       
  1467 def LoadTargetModule(handler_path,
       
  1468                      cgi_path,
       
  1469                      import_hook,
       
  1470                      module_dict=sys.modules):
       
  1471   """Loads a target CGI script by importing it as a Python module.
       
  1472 
       
  1473   If the module for the target CGI script has already been loaded before,
       
  1474   the new module will be loaded in its place using the same module object,
       
  1475   possibly overwriting existing module attributes.
       
  1476 
       
  1477   Args:
       
  1478     handler_path: CGI path stored in the application configuration (as a path
       
  1479       like 'foo/bar/baz.py'). Should not have $PYTHON_LIB references.
       
  1480     cgi_path: Absolute path to the CGI script file on disk.
       
  1481     import_hook: Instance of HardenedModulesHook to use for module loading.
       
  1482     module_dict: Used for dependency injection.
       
  1483 
       
  1484   Returns:
       
  1485     Tuple (module_fullname, script_module, module_code) where:
       
  1486       module_fullname: Fully qualified module name used to import the script.
       
  1487       script_module: The ModuleType object corresponding to the module_fullname.
       
  1488         If the module has not already been loaded, this will be an empty
       
  1489         shell of a module.
       
  1490       module_code: Code object (returned by compile built-in) corresponding
       
  1491         to the cgi_path to run. If the script_module was previously loaded
       
  1492         and has a main() function that can be reused, this will be None.
       
  1493   """
       
  1494   module_fullname = GetScriptModuleName(handler_path)
       
  1495   script_module = module_dict.get(module_fullname)
       
  1496   module_code = None
       
  1497   if script_module != None and ModuleHasValidMainFunction(script_module):
       
  1498     logging.debug('Reusing main() function of module "%s"', module_fullname)
       
  1499   else:
       
  1500     if script_module is None:
       
  1501       script_module = imp.new_module(module_fullname)
       
  1502       script_module.__loader__ = import_hook
       
  1503 
       
  1504     try:
       
  1505       module_code = import_hook.get_code(module_fullname)
       
  1506       full_path, search_path, submodule = import_hook.GetModuleInfo(module_fullname)
       
  1507       script_module.__file__ = full_path
       
  1508       if search_path is not None:
       
  1509         script_module.__path__ = search_path
       
  1510     except:
       
  1511       exc_type, exc_value, exc_tb = sys.exc_info()
       
  1512       import_error_message = str(exc_type)
       
  1513       if exc_value:
       
  1514         import_error_message += ': ' + str(exc_value)
       
  1515 
       
  1516       logging.error('Encountered error loading module "%s": %s',
       
  1517                     module_fullname, import_error_message)
       
  1518       missing_inits = FindMissingInitFiles(cgi_path, module_fullname)
       
  1519       if missing_inits:
       
  1520         logging.warning('Missing package initialization files: %s',
       
  1521                         ', '.join(missing_inits))
       
  1522       else:
       
  1523         logging.error('Parent package initialization files are present, '
       
  1524                       'but must be broken')
       
  1525 
       
  1526       independent_load_successful = True
       
  1527 
       
  1528       if not os.path.isfile(cgi_path):
       
  1529         independent_load_successful = False
       
  1530       else:
       
  1531         try:
       
  1532           source_file = open(cgi_path)
       
  1533           try:
       
  1534             module_code = compile(source_file.read(), cgi_path, 'exec')
       
  1535             script_module.__file__ = cgi_path
       
  1536           finally:
       
  1537             source_file.close()
       
  1538 
       
  1539         except OSError:
       
  1540           independent_load_successful = False
       
  1541 
       
  1542       if not independent_load_successful:
       
  1543         raise exc_type, exc_value, exc_tb
       
  1544 
       
  1545     module_dict[module_fullname] = script_module
       
  1546 
       
  1547   return module_fullname, script_module, module_code
       
  1548 
       
  1549 
       
  1550 def ExecuteOrImportScript(handler_path, cgi_path, import_hook):
       
  1551   """Executes a CGI script by importing it as a new module; possibly reuses
       
  1552   the module's main() function if it is defined and takes no arguments.
       
  1553 
       
  1554   Basic technique lifted from PEP 338 and Python2.5's runpy module. See:
       
  1555     http://www.python.org/dev/peps/pep-0338/
       
  1556 
       
  1557   See the section entitled "Import Statements and the Main Module" to understand
       
  1558   why a module named '__main__' cannot do relative imports. To get around this,
       
  1559   the requested module's path could be added to sys.path on each request.
       
  1560 
       
  1561   Args:
       
  1562     handler_path: CGI path stored in the application configuration (as a path
       
  1563       like 'foo/bar/baz.py'). Should not have $PYTHON_LIB references.
       
  1564     cgi_path: Absolute path to the CGI script file on disk.
       
  1565     import_hook: Instance of HardenedModulesHook to use for module loading.
       
  1566 
       
  1567   Returns:
       
  1568     True if the response code had an error status (e.g., 404), or False if it
       
  1569     did not.
       
  1570 
       
  1571   Raises:
       
  1572     Any kind of exception that could have been raised when loading the target
       
  1573     module, running a target script, or executing the application code itself.
       
  1574   """
       
  1575   module_fullname, script_module, module_code = LoadTargetModule(
       
  1576       handler_path, cgi_path, import_hook)
       
  1577   script_module.__name__ = '__main__'
       
  1578   sys.modules['__main__'] = script_module
       
  1579   try:
       
  1580     if module_code:
       
  1581       exec module_code in script_module.__dict__
       
  1582     else:
       
  1583       script_module.main()
       
  1584 
       
  1585     sys.stdout.flush()
       
  1586     sys.stdout.seek(0)
       
  1587     try:
       
  1588       headers = mimetools.Message(sys.stdout)
       
  1589     finally:
       
  1590       sys.stdout.seek(0, 2)
       
  1591     status_header = headers.get('status')
       
  1592     error_response = False
       
  1593     if status_header:
       
  1594       try:
       
  1595         status_code = int(status_header.split(' ', 1)[0])
       
  1596         error_response = status_code >= 400
       
  1597       except ValueError:
       
  1598         error_response = True
       
  1599 
       
  1600     if not error_response:
       
  1601       try:
       
  1602         parent_package = import_hook.GetParentPackage(module_fullname)
       
  1603       except Exception:
       
  1604         parent_package = None
       
  1605 
       
  1606       if parent_package is not None:
       
  1607         submodule = GetSubmoduleName(module_fullname)
       
  1608         setattr(parent_package, submodule, script_module)
       
  1609 
       
  1610     return error_response
       
  1611   finally:
       
  1612     script_module.__name__ = module_fullname
       
  1613 
       
  1614 
       
  1615 def ExecuteCGI(root_path,
       
  1616                handler_path,
       
  1617                cgi_path,
       
  1618                env,
       
  1619                infile,
       
  1620                outfile,
       
  1621                module_dict,
       
  1622                exec_script=ExecuteOrImportScript):
       
  1623   """Executes Python file in this process as if it were a CGI.
       
  1624 
       
  1625   Does not return an HTTP response line. CGIs should output headers followed by
       
  1626   the body content.
       
  1627 
       
  1628   The modules in sys.modules should be the same before and after the CGI is
       
  1629   executed, with the specific exception of encodings-related modules, which
       
  1630   cannot be reloaded and thus must always stay in sys.modules.
       
  1631 
       
  1632   Args:
       
  1633     root_path: Path to the root of the application.
       
  1634     handler_path: CGI path stored in the application configuration (as a path
       
  1635       like 'foo/bar/baz.py'). May contain $PYTHON_LIB references.
       
  1636     cgi_path: Absolute path to the CGI script file on disk.
       
  1637     env: Dictionary of environment variables to use for the execution.
       
  1638     infile: File-like object to read HTTP request input data from.
       
  1639     outfile: FIle-like object to write HTTP response data to.
       
  1640     module_dict: Dictionary in which application-loaded modules should be
       
  1641       preserved between requests. This removes the need to reload modules that
       
  1642       are reused between requests, significantly increasing load performance.
       
  1643       This dictionary must be separate from the sys.modules dictionary.
       
  1644     exec_script: Used for dependency injection.
       
  1645   """
       
  1646   old_module_dict = sys.modules.copy()
       
  1647   old_builtin = __builtin__.__dict__.copy()
       
  1648   old_argv = sys.argv
       
  1649   old_stdin = sys.stdin
       
  1650   old_stdout = sys.stdout
       
  1651   old_env = os.environ.copy()
       
  1652   old_cwd = os.getcwd()
       
  1653   old_file_type = types.FileType
       
  1654   reset_modules = False
       
  1655 
       
  1656   try:
       
  1657     ClearAllButEncodingsModules(sys.modules)
       
  1658     sys.modules.update(module_dict)
       
  1659     sys.argv = [cgi_path]
       
  1660     sys.stdin = infile
       
  1661     sys.stdout = outfile
       
  1662     os.environ.clear()
       
  1663     os.environ.update(env)
       
  1664     before_path = sys.path[:]
       
  1665     os.chdir(os.path.dirname(cgi_path))
       
  1666 
       
  1667     hook = HardenedModulesHook(sys.modules)
       
  1668     sys.meta_path = [hook]
       
  1669     if hasattr(sys, 'path_importer_cache'):
       
  1670       sys.path_importer_cache.clear()
       
  1671 
       
  1672     __builtin__.file = FakeFile
       
  1673     __builtin__.open = FakeFile
       
  1674     types.FileType = FakeFile
       
  1675 
       
  1676     __builtin__.buffer = NotImplementedFake
       
  1677 
       
  1678     logging.debug('Executing CGI with env:\n%s', pprint.pformat(env))
       
  1679     try:
       
  1680       reset_modules = exec_script(handler_path, cgi_path, hook)
       
  1681     except SystemExit, e:
       
  1682       logging.debug('CGI exited with status: %s', e)
       
  1683     except:
       
  1684       reset_modules = True
       
  1685       raise
       
  1686 
       
  1687   finally:
       
  1688     sys.meta_path = []
       
  1689     sys.path_importer_cache.clear()
       
  1690 
       
  1691     _ClearTemplateCache(sys.modules)
       
  1692 
       
  1693     module_dict.update(sys.modules)
       
  1694     ClearAllButEncodingsModules(sys.modules)
       
  1695     sys.modules.update(old_module_dict)
       
  1696 
       
  1697     __builtin__.__dict__.update(old_builtin)
       
  1698     sys.argv = old_argv
       
  1699     sys.stdin = old_stdin
       
  1700     sys.stdout = old_stdout
       
  1701 
       
  1702     sys.path[:] = before_path
       
  1703 
       
  1704     os.environ.clear()
       
  1705     os.environ.update(old_env)
       
  1706     os.chdir(old_cwd)
       
  1707 
       
  1708     types.FileType = old_file_type
       
  1709 
       
  1710 
       
  1711 class CGIDispatcher(URLDispatcher):
       
  1712   """Dispatcher that executes Python CGI scripts."""
       
  1713 
       
  1714   def __init__(self,
       
  1715                module_dict,
       
  1716                root_path,
       
  1717                path_adjuster,
       
  1718                setup_env=SetupEnvironment,
       
  1719                exec_cgi=ExecuteCGI,
       
  1720                create_logging_handler=ApplicationLoggingHandler):
       
  1721     """Initializer.
       
  1722 
       
  1723     Args:
       
  1724       module_dict: Dictionary in which application-loaded modules should be
       
  1725         preserved between requests. This dictionary must be separate from the
       
  1726         sys.modules dictionary.
       
  1727       path_adjuster: Instance of PathAdjuster to use for finding absolute
       
  1728         paths of CGI files on disk.
       
  1729       setup_env, exec_cgi, create_logging_handler: Used for dependency
       
  1730         injection.
       
  1731     """
       
  1732     self._module_dict = module_dict
       
  1733     self._root_path = root_path
       
  1734     self._path_adjuster = path_adjuster
       
  1735     self._setup_env = setup_env
       
  1736     self._exec_cgi = exec_cgi
       
  1737     self._create_logging_handler = create_logging_handler
       
  1738 
       
  1739   def Dispatch(self,
       
  1740                relative_url,
       
  1741                path,
       
  1742                headers,
       
  1743                infile,
       
  1744                outfile,
       
  1745                base_env_dict=None):
       
  1746     """Dispatches the Python CGI."""
       
  1747     handler = self._create_logging_handler()
       
  1748     logging.getLogger().addHandler(handler)
       
  1749     before_level = logging.root.level
       
  1750     try:
       
  1751       env = {}
       
  1752       if base_env_dict:
       
  1753         env.update(base_env_dict)
       
  1754       cgi_path = self._path_adjuster.AdjustPath(path)
       
  1755       env.update(self._setup_env(cgi_path, relative_url, headers))
       
  1756       self._exec_cgi(self._root_path,
       
  1757                      path,
       
  1758                      cgi_path,
       
  1759                      env,
       
  1760                      infile,
       
  1761                      outfile,
       
  1762                      self._module_dict)
       
  1763       handler.AddDebuggingConsole(relative_url, env, outfile)
       
  1764     finally:
       
  1765       logging.root.level = before_level
       
  1766       logging.getLogger().removeHandler(handler)
       
  1767 
       
  1768   def __str__(self):
       
  1769     """Returns a string representation of this dispatcher."""
       
  1770     return 'CGI dispatcher'
       
  1771 
       
  1772 
       
  1773 class LocalCGIDispatcher(CGIDispatcher):
       
  1774   """Dispatcher that executes local functions like they're CGIs.
       
  1775 
       
  1776   The contents of sys.modules will be preserved for local CGIs running this
       
  1777   dispatcher, but module hardening will still occur for any new imports. Thus,
       
  1778   be sure that any local CGIs have loaded all of their dependent modules
       
  1779   _before_ they are executed.
       
  1780   """
       
  1781 
       
  1782   def __init__(self, module_dict, path_adjuster, cgi_func):
       
  1783     """Initializer.
       
  1784 
       
  1785     Args:
       
  1786       module_dict: Passed to CGIDispatcher.
       
  1787       path_adjuster: Passed to CGIDispatcher.
       
  1788       cgi_func: Callable function taking no parameters that should be
       
  1789         executed in a CGI environment in the current process.
       
  1790     """
       
  1791     self._cgi_func = cgi_func
       
  1792 
       
  1793     def curried_exec_script(*args, **kwargs):
       
  1794       cgi_func()
       
  1795       return False
       
  1796 
       
  1797     def curried_exec_cgi(*args, **kwargs):
       
  1798       kwargs['exec_script'] = curried_exec_script
       
  1799       return ExecuteCGI(*args, **kwargs)
       
  1800 
       
  1801     CGIDispatcher.__init__(self,
       
  1802                            module_dict,
       
  1803                            '',
       
  1804                            path_adjuster,
       
  1805                            exec_cgi=curried_exec_cgi)
       
  1806 
       
  1807   def Dispatch(self, *args, **kwargs):
       
  1808     """Preserves sys.modules for CGIDispatcher.Dispatch."""
       
  1809     self._module_dict.update(sys.modules)
       
  1810     CGIDispatcher.Dispatch(self, *args, **kwargs)
       
  1811 
       
  1812   def __str__(self):
       
  1813     """Returns a string representation of this dispatcher."""
       
  1814     return 'Local CGI dispatcher for %s' % self._cgi_func
       
  1815 
       
  1816 
       
  1817 class PathAdjuster(object):
       
  1818   """Adjusts application file paths to paths relative to the application or
       
  1819   external library directories."""
       
  1820 
       
  1821   def __init__(self, root_path):
       
  1822     """Initializer.
       
  1823 
       
  1824     Args:
       
  1825       root_path: Path to the root of the application running on the server.
       
  1826     """
       
  1827     self._root_path = os.path.abspath(root_path)
       
  1828 
       
  1829   def AdjustPath(self, path):
       
  1830     """Adjusts application file path to paths relative to the application or
       
  1831     external library directories.
       
  1832 
       
  1833     Handler paths that start with $PYTHON_LIB will be converted to paths
       
  1834     relative to the google directory.
       
  1835 
       
  1836     Args:
       
  1837       path: File path that should be adjusted.
       
  1838 
       
  1839     Returns:
       
  1840       The adjusted path.
       
  1841     """
       
  1842     if path.startswith(PYTHON_LIB_VAR):
       
  1843       path = os.path.join(os.path.dirname(os.path.dirname(google.__file__)),
       
  1844                           path[len(PYTHON_LIB_VAR) + 1:])
       
  1845     else:
       
  1846       path = os.path.join(self._root_path, path)
       
  1847 
       
  1848     return path
       
  1849 
       
  1850 
       
  1851 class StaticFileMimeTypeMatcher(object):
       
  1852   """Computes mime type based on URLMap and file extension.
       
  1853 
       
  1854   To determine the mime type, we first see if there is any mime-type property
       
  1855   on each URLMap entry. If non is specified, we use the mimetypes module to
       
  1856   guess the mime type from the file path extension, and use
       
  1857   application/octet-stream if we can't find the mimetype.
       
  1858   """
       
  1859 
       
  1860   def __init__(self,
       
  1861                url_map_list,
       
  1862                path_adjuster):
       
  1863     """Initializer.
       
  1864 
       
  1865     Args:
       
  1866       url_map_list: List of appinfo.URLMap objects.
       
  1867         If empty or None, then we always use the mime type chosen by the
       
  1868         mimetypes module.
       
  1869       path_adjuster: PathAdjuster object used to adjust application file paths.
       
  1870     """
       
  1871     self._patterns = []
       
  1872 
       
  1873     if url_map_list:
       
  1874       for entry in url_map_list:
       
  1875         if entry.mime_type is None:
       
  1876           continue
       
  1877         handler_type = entry.GetHandlerType()
       
  1878         if handler_type not in (appinfo.STATIC_FILES, appinfo.STATIC_DIR):
       
  1879           continue
       
  1880 
       
  1881         if handler_type == appinfo.STATIC_FILES:
       
  1882           regex = entry.upload
       
  1883         else:
       
  1884           static_dir = entry.static_dir
       
  1885           if static_dir[-1] == '/':
       
  1886             static_dir = static_dir[:-1]
       
  1887           regex = '/'.join((entry.static_dir, r'(.*)'))
       
  1888 
       
  1889         adjusted_regex = r'^%s$' % path_adjuster.AdjustPath(regex)
       
  1890         try:
       
  1891           path_re = re.compile(adjusted_regex)
       
  1892         except re.error, e:
       
  1893           raise InvalidAppConfigError('regex does not compile: %s' % e)
       
  1894 
       
  1895         self._patterns.append((path_re, entry.mime_type))
       
  1896 
       
  1897   def GetMimeType(self, path):
       
  1898     """Returns the mime type that we should use when serving the specified file.
       
  1899 
       
  1900     Args:
       
  1901       path: String containing the file's path on disk.
       
  1902 
       
  1903     Returns:
       
  1904       String containing the mime type to use. Will be 'application/octet-stream'
       
  1905       if we have no idea what it should be.
       
  1906     """
       
  1907     for (path_re, mime_type) in self._patterns:
       
  1908       the_match = path_re.match(path)
       
  1909       if the_match:
       
  1910         return mime_type
       
  1911 
       
  1912     filename, extension = os.path.splitext(path)
       
  1913     return mimetypes.types_map.get(extension, 'application/octet-stream')
       
  1914 
       
  1915 
       
  1916 def ReadDataFile(data_path, openfile=file):
       
  1917   """Reads a file on disk, returning a corresponding HTTP status and data.
       
  1918 
       
  1919   Args:
       
  1920     data_path: Path to the file on disk to read.
       
  1921     openfile: Used for dependency injection.
       
  1922 
       
  1923   Returns:
       
  1924     Tuple (status, data) where status is an HTTP response code, and data is
       
  1925       the data read; will be an empty string if an error occurred or the
       
  1926       file was empty.
       
  1927   """
       
  1928   status = httplib.INTERNAL_SERVER_ERROR
       
  1929   data = ""
       
  1930 
       
  1931   try:
       
  1932     data_file = openfile(data_path, 'rb')
       
  1933     try:
       
  1934       data = data_file.read()
       
  1935     finally:
       
  1936       data_file.close()
       
  1937       status = httplib.OK
       
  1938   except (OSError, IOError), e:
       
  1939     logging.error('Error encountered reading file "%s":\n%s', data_path, e)
       
  1940     if e.errno in FILE_MISSING_EXCEPTIONS:
       
  1941       status = httplib.NOT_FOUND
       
  1942     else:
       
  1943       status = httplib.FORBIDDEN
       
  1944 
       
  1945   return status, data
       
  1946 
       
  1947 
       
  1948 class FileDispatcher(URLDispatcher):
       
  1949   """Dispatcher that reads data files from disk."""
       
  1950 
       
  1951   def __init__(self,
       
  1952                path_adjuster,
       
  1953                static_file_mime_type_matcher,
       
  1954                read_data_file=ReadDataFile):
       
  1955     """Initializer.
       
  1956 
       
  1957     Args:
       
  1958       path_adjuster: Instance of PathAdjuster to use for finding absolute
       
  1959         paths of data files on disk.
       
  1960       static_file_mime_type_matcher: StaticFileMimeTypeMatcher object.
       
  1961       read_data_file: Used for dependency injection.
       
  1962     """
       
  1963     self._path_adjuster = path_adjuster
       
  1964     self._static_file_mime_type_matcher = static_file_mime_type_matcher
       
  1965     self._read_data_file = read_data_file
       
  1966 
       
  1967   def Dispatch(self,
       
  1968                relative_url,
       
  1969                path,
       
  1970                headers,
       
  1971                infile,
       
  1972                outfile,
       
  1973                base_env_dict=None):
       
  1974     """Reads the file and returns the response status and data."""
       
  1975     full_path = self._path_adjuster.AdjustPath(path)
       
  1976     status, data = self._read_data_file(full_path)
       
  1977     content_type = self._static_file_mime_type_matcher.GetMimeType(full_path)
       
  1978 
       
  1979     outfile.write('Status: %d\r\n' % status)
       
  1980     outfile.write('Content-type: %s\r\n' % content_type)
       
  1981     outfile.write('\r\n')
       
  1982     outfile.write(data)
       
  1983 
       
  1984   def __str__(self):
       
  1985     """Returns a string representation of this dispatcher."""
       
  1986     return 'File dispatcher'
       
  1987 
       
  1988 
       
  1989 def RewriteResponse(response_file):
       
  1990   """Interprets server-side headers and adjusts the HTTP response accordingly.
       
  1991 
       
  1992   Handles the server-side 'status' header, which instructs the server to change
       
  1993   the HTTP response code accordingly. Handles the 'location' header, which
       
  1994   issues an HTTP 302 redirect to the client. Also corrects the 'content-length'
       
  1995   header to reflect actual content length in case extra information has been
       
  1996   appended to the response body.
       
  1997 
       
  1998   If the 'status' header supplied by the client is invalid, this method will
       
  1999   set the response to a 500 with an error message as content.
       
  2000 
       
  2001   Args:
       
  2002     response_file: File-like object containing the full HTTP response including
       
  2003       the response code, all headers, and the request body.
       
  2004 
       
  2005   Returns:
       
  2006     Tuple (status_code, status_message, header, body) where:
       
  2007       status_code: Integer HTTP response status (e.g., 200, 302, 404, 500)
       
  2008       status_message: String containing an informational message about the
       
  2009         response code, possibly derived from the 'status' header, if supplied.
       
  2010       header: String containing the HTTP headers of the response, without
       
  2011         a trailing new-line (CRLF).
       
  2012       body: String containing the body of the response.
       
  2013   """
       
  2014   headers = mimetools.Message(response_file)
       
  2015 
       
  2016   response_status = '%d Good to go' % httplib.OK
       
  2017 
       
  2018   location_value = headers.getheader('location')
       
  2019   status_value = headers.getheader('status')
       
  2020   if status_value:
       
  2021     response_status = status_value
       
  2022     del headers['status']
       
  2023   elif location_value:
       
  2024     response_status = '%d Redirecting' % httplib.FOUND
       
  2025 
       
  2026   if not 'Cache-Control' in headers:
       
  2027     headers['Cache-Control'] = 'no-cache'
       
  2028 
       
  2029   status_parts = response_status.split(' ', 1)
       
  2030   status_code, status_message = (status_parts + [''])[:2]
       
  2031   try:
       
  2032     status_code = int(status_code)
       
  2033   except ValueError:
       
  2034     status_code = 500
       
  2035     body = 'Error: Invalid "status" header value returned.'
       
  2036   else:
       
  2037     body = response_file.read()
       
  2038 
       
  2039   headers['content-length'] = str(len(body))
       
  2040 
       
  2041   header_list = []
       
  2042   for header in headers.headers:
       
  2043     header = header.rstrip('\n')
       
  2044     header = header.rstrip('\r')
       
  2045     header_list.append(header)
       
  2046 
       
  2047   header_data = '\r\n'.join(header_list) + '\r\n'
       
  2048   return status_code, status_message, header_data, body
       
  2049 
       
  2050 
       
  2051 class ModuleManager(object):
       
  2052   """Manages loaded modules in the runtime.
       
  2053 
       
  2054   Responsible for monitoring and reporting about file modification times.
       
  2055   Modules can be loaded from source or precompiled byte-code files.  When a
       
  2056   file has source code, the ModuleManager monitors the modification time of
       
  2057   the source file even if the module itself is loaded from byte-code.
       
  2058   """
       
  2059 
       
  2060   def __init__(self, modules):
       
  2061     """Initializer.
       
  2062 
       
  2063     Args:
       
  2064       modules: Dictionary containing monitored modules.
       
  2065     """
       
  2066     self._modules = modules
       
  2067     self._default_modules = self._modules.copy()
       
  2068 
       
  2069     self._modification_times = {}
       
  2070 
       
  2071   @staticmethod
       
  2072   def GetModuleFile(module, is_file=os.path.isfile):
       
  2073     """Helper method to try to determine modules source file.
       
  2074 
       
  2075     Args:
       
  2076       module: Module object to get file for.
       
  2077       is_file: Function used to determine if a given path is a file.
       
  2078 
       
  2079     Returns:
       
  2080       Path of the module's corresponding Python source file if it exists, or
       
  2081       just the module's compiled Python file. If the module has an invalid
       
  2082       __file__ attribute, None will be returned.
       
  2083       """
       
  2084     module_file = getattr(module, '__file__', None)
       
  2085     if not module_file or module_file == HardenedModulesHook.EMPTY_MODULE_FILE:
       
  2086       return None
       
  2087 
       
  2088     source_file = module_file[:module_file.rfind('py') + 2]
       
  2089 
       
  2090     if is_file(source_file):
       
  2091       return source_file
       
  2092     return module.__file__
       
  2093 
       
  2094   def AreModuleFilesModified(self):
       
  2095     """Determines if any monitored files have been modified.
       
  2096 
       
  2097     Returns:
       
  2098       True if one or more files have been modified, False otherwise.
       
  2099     """
       
  2100     for name, (mtime, fname) in self._modification_times.iteritems():
       
  2101       if name not in self._modules:
       
  2102         continue
       
  2103 
       
  2104       module = self._modules[name]
       
  2105 
       
  2106       if not os.path.isfile(fname):
       
  2107         return True
       
  2108 
       
  2109       if mtime != os.path.getmtime(fname):
       
  2110         return True
       
  2111 
       
  2112     return False
       
  2113 
       
  2114   def UpdateModuleFileModificationTimes(self):
       
  2115     """Records the current modification times of all monitored modules.
       
  2116     """
       
  2117     self._modification_times.clear()
       
  2118     for name, module in self._modules.items():
       
  2119       if not isinstance(module, types.ModuleType):
       
  2120         continue
       
  2121       module_file = self.GetModuleFile(module)
       
  2122       if not module_file:
       
  2123         continue
       
  2124       try:
       
  2125         self._modification_times[name] = (os.path.getmtime(module_file),
       
  2126                                           module_file)
       
  2127       except OSError, e:
       
  2128         if e.errno not in FILE_MISSING_EXCEPTIONS:
       
  2129           raise e
       
  2130 
       
  2131   def ResetModules(self):
       
  2132     """Clear modules so that when request is run they are reloaded."""
       
  2133     self._modules.clear()
       
  2134     self._modules.update(self._default_modules)
       
  2135 
       
  2136 
       
  2137 def _ClearTemplateCache(module_dict=sys.modules):
       
  2138   """Clear template cache in webapp.template module.
       
  2139 
       
  2140   Attempts to load template module.  Ignores failure.  If module loads, the
       
  2141   template cache is cleared.
       
  2142   """
       
  2143   template_module = module_dict.get('google.appengine.ext.webapp.template')
       
  2144   if template_module is not None:
       
  2145     template_module.template_cache.clear()
       
  2146 
       
  2147 
       
  2148 def CreateRequestHandler(root_path, login_url, require_indexes=False):
       
  2149   """Creates a new BaseHTTPRequestHandler sub-class for use with the Python
       
  2150   BaseHTTPServer module's HTTP server.
       
  2151 
       
  2152   Python's built-in HTTP server does not support passing context information
       
  2153   along to instances of its request handlers. This function gets around that
       
  2154   by creating a sub-class of the handler in a closure that has access to
       
  2155   this context information.
       
  2156 
       
  2157   Args:
       
  2158     root_path: Path to the root of the application running on the server.
       
  2159     login_url: Relative URL which should be used for handling user logins.
       
  2160     require_indexes: True if index.yaml is read-only gospel; default False.
       
  2161 
       
  2162   Returns:
       
  2163     Sub-class of BaseHTTPRequestHandler.
       
  2164   """
       
  2165   application_module_dict = SetupSharedModules(sys.modules)
       
  2166 
       
  2167   if require_indexes:
       
  2168     index_yaml_updater = None
       
  2169   else:
       
  2170     index_yaml_updater = dev_appserver_index.IndexYamlUpdater(root_path)
       
  2171 
       
  2172   class DevAppServerRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
       
  2173     """Dispatches URLs using patterns from a URLMatcher, which is created by
       
  2174     loading an application's configuration file. Executes CGI scripts in the
       
  2175     local process so the scripts can use mock versions of APIs.
       
  2176 
       
  2177     HTTP requests that correctly specify a user info cookie
       
  2178     (dev_appserver_login.COOKIE_NAME) will have the 'USER_EMAIL' environment
       
  2179     variable set accordingly. If the user is also an admin, the
       
  2180     'USER_IS_ADMIN' variable will exist and be set to '1'. If the user is not
       
  2181     logged in, 'USER_EMAIL' will be set to the empty string.
       
  2182 
       
  2183     On each request, raises an InvalidAppConfigError exception if the
       
  2184     application configuration file in the directory specified by the root_path
       
  2185     argument is invalid.
       
  2186     """
       
  2187     server_version = 'Development/1.0'
       
  2188 
       
  2189     module_dict = application_module_dict
       
  2190     module_manager = ModuleManager(application_module_dict)
       
  2191 
       
  2192     def __init__(self, *args, **kwargs):
       
  2193       """Initializer.
       
  2194 
       
  2195       Args:
       
  2196         args, kwargs: Positional and keyword arguments passed to the constructor
       
  2197           of the super class.
       
  2198       """
       
  2199       BaseHTTPServer.BaseHTTPRequestHandler.__init__(self, *args, **kwargs)
       
  2200 
       
  2201     def do_GET(self):
       
  2202       """Handle GET requests."""
       
  2203       self._HandleRequest()
       
  2204 
       
  2205     def do_POST(self):
       
  2206       """Handles POST requests."""
       
  2207       self._HandleRequest()
       
  2208 
       
  2209     def do_PUT(self):
       
  2210       """Handle PUT requests."""
       
  2211       self._HandleRequest()
       
  2212 
       
  2213     def do_HEAD(self):
       
  2214       """Handle HEAD requests."""
       
  2215       self._HandleRequest()
       
  2216 
       
  2217     def do_OPTIONS(self):
       
  2218       """Handles OPTIONS requests."""
       
  2219       self._HandleRequest()
       
  2220 
       
  2221     def do_DELETE(self):
       
  2222       """Handle DELETE requests."""
       
  2223       self._HandleRequest()
       
  2224 
       
  2225     def do_TRACE(self):
       
  2226       """Handles TRACE requests."""
       
  2227       self._HandleRequest()
       
  2228 
       
  2229     def _HandleRequest(self):
       
  2230       """Handles any type of request and prints exceptions if they occur."""
       
  2231       server_name = self.headers.get('host') or self.server.server_name
       
  2232       server_name = server_name.split(':', 1)[0]
       
  2233 
       
  2234       env_dict = {
       
  2235         'REQUEST_METHOD': self.command,
       
  2236         'REMOTE_ADDR': self.client_address[0],
       
  2237         'SERVER_SOFTWARE': self.server_version,
       
  2238         'SERVER_NAME': server_name,
       
  2239         'SERVER_PROTOCOL': self.protocol_version,
       
  2240         'SERVER_PORT': str(self.server.server_port),
       
  2241       }
       
  2242 
       
  2243       full_url = GetFullURL(server_name, self.server.server_port, self.path)
       
  2244       if len(full_url) > MAX_URL_LENGTH:
       
  2245         msg = 'Requested URI too long: %s' % full_url
       
  2246         logging.error(msg)
       
  2247         self.send_response(httplib.REQUEST_URI_TOO_LONG, msg)
       
  2248         return
       
  2249 
       
  2250       tbhandler = cgitb.Hook(file=self.wfile).handle
       
  2251       try:
       
  2252         if self.module_manager.AreModuleFilesModified():
       
  2253           self.module_manager.ResetModules()
       
  2254 
       
  2255         implicit_matcher = CreateImplicitMatcher(self.module_dict,
       
  2256                                                  root_path,
       
  2257                                                  login_url)
       
  2258         config, explicit_matcher = LoadAppConfig(root_path, self.module_dict)
       
  2259         env_dict['CURRENT_VERSION_ID'] = config.version + ".1"
       
  2260         env_dict['APPLICATION_ID'] = config.application
       
  2261         dispatcher = MatcherDispatcher(login_url,
       
  2262                                        [implicit_matcher, explicit_matcher])
       
  2263 
       
  2264         if require_indexes:
       
  2265           dev_appserver_index.SetupIndexes(config.application, root_path)
       
  2266 
       
  2267         infile = cStringIO.StringIO(self.rfile.read(
       
  2268             int(self.headers.get('content-length', 0))))
       
  2269         outfile = cStringIO.StringIO()
       
  2270         try:
       
  2271           dispatcher.Dispatch(self.path,
       
  2272                               None,
       
  2273                               self.headers,
       
  2274                               infile,
       
  2275                               outfile,
       
  2276                               base_env_dict=env_dict)
       
  2277         finally:
       
  2278           self.module_manager.UpdateModuleFileModificationTimes()
       
  2279 
       
  2280         outfile.flush()
       
  2281         outfile.seek(0)
       
  2282         status_code, status_message, header_data, body = RewriteResponse(outfile)
       
  2283 
       
  2284       except yaml_errors.EventListenerError, e:
       
  2285         title = 'Fatal error when loading application configuration'
       
  2286         msg = '%s:\n%s' % (title, str(e))
       
  2287         logging.error(msg)
       
  2288         self.send_response(httplib.INTERNAL_SERVER_ERROR, title)
       
  2289         self.wfile.write('Content-Type: text/html\n\n')
       
  2290         self.wfile.write('<pre>%s</pre>' % cgi.escape(msg))
       
  2291       except:
       
  2292         msg = 'Exception encountered handling request'
       
  2293         logging.exception(msg)
       
  2294         self.send_response(httplib.INTERNAL_SERVER_ERROR, msg)
       
  2295         tbhandler()
       
  2296       else:
       
  2297         try:
       
  2298           self.send_response(status_code, status_message)
       
  2299           self.wfile.write(header_data)
       
  2300           self.wfile.write('\r\n')
       
  2301           if self.command != 'HEAD':
       
  2302             self.wfile.write(body)
       
  2303           elif body:
       
  2304             logging.warning('Dropping unexpected body in response '
       
  2305                             'to HEAD request')
       
  2306         except (IOError, OSError), e:
       
  2307           if e.errno != errno.EPIPE:
       
  2308             raise e
       
  2309         except socket.error, e:
       
  2310           if len(e.args) >= 1 and e.args[0] != errno.EPIPE:
       
  2311             raise e
       
  2312         else:
       
  2313           if index_yaml_updater is not None:
       
  2314             index_yaml_updater.UpdateIndexYaml()
       
  2315 
       
  2316     def log_error(self, format, *args):
       
  2317       """Redirect error messages through the logging module."""
       
  2318       logging.error(format, *args)
       
  2319 
       
  2320     def log_message(self, format, *args):
       
  2321       """Redirect log messages through the logging module."""
       
  2322       logging.info(format, *args)
       
  2323 
       
  2324   return DevAppServerRequestHandler
       
  2325 
       
  2326 
       
  2327 def ReadAppConfig(appinfo_path, parse_app_config=appinfo.LoadSingleAppInfo):
       
  2328   """Reads app.yaml file and returns its app id and list of URLMap instances.
       
  2329 
       
  2330   Args:
       
  2331     appinfo_path: String containing the path to the app.yaml file.
       
  2332     parse_app_config: Used for dependency injection.
       
  2333 
       
  2334   Returns:
       
  2335     AppInfoExternal instance.
       
  2336 
       
  2337   Raises:
       
  2338     If the config file could not be read or the config does not contain any
       
  2339     URLMap instances, this function will raise an InvalidAppConfigError
       
  2340     exception.
       
  2341   """
       
  2342   try:
       
  2343     appinfo_file = file(appinfo_path, 'r')
       
  2344     try:
       
  2345       return parse_app_config(appinfo_file)
       
  2346     finally:
       
  2347       appinfo_file.close()
       
  2348   except IOError, e:
       
  2349     raise InvalidAppConfigError(
       
  2350       'Application configuration could not be read from "%s"' % appinfo_path)
       
  2351 
       
  2352 
       
  2353 def CreateURLMatcherFromMaps(root_path,
       
  2354                              url_map_list,
       
  2355                              module_dict,
       
  2356                              create_url_matcher=URLMatcher,
       
  2357                              create_cgi_dispatcher=CGIDispatcher,
       
  2358                              create_file_dispatcher=FileDispatcher,
       
  2359                              create_path_adjuster=PathAdjuster):
       
  2360   """Creates a URLMatcher instance from URLMap.
       
  2361 
       
  2362   Creates all of the correct URLDispatcher instances to handle the various
       
  2363   content types in the application configuration.
       
  2364 
       
  2365   Args:
       
  2366     root_path: Path to the root of the application running on the server.
       
  2367     url_map_list: List of appinfo.URLMap objects to initialize this
       
  2368       matcher with. Can be an empty list if you would like to add patterns
       
  2369       manually.
       
  2370     module_dict: Dictionary in which application-loaded modules should be
       
  2371       preserved between requests. This dictionary must be separate from the
       
  2372       sys.modules dictionary.
       
  2373     create_url_matcher, create_cgi_dispatcher, create_file_dispatcher,
       
  2374     create_path_adjuster: Used for dependency injection.
       
  2375 
       
  2376   Returns:
       
  2377     Instance of URLMatcher with the supplied URLMap objects properly loaded.
       
  2378   """
       
  2379   url_matcher = create_url_matcher()
       
  2380   path_adjuster = create_path_adjuster(root_path)
       
  2381   cgi_dispatcher = create_cgi_dispatcher(module_dict, root_path, path_adjuster)
       
  2382   file_dispatcher = create_file_dispatcher(path_adjuster,
       
  2383       StaticFileMimeTypeMatcher(url_map_list, path_adjuster))
       
  2384 
       
  2385   for url_map in url_map_list:
       
  2386     admin_only = url_map.login == appinfo.LOGIN_ADMIN
       
  2387     requires_login = url_map.login == appinfo.LOGIN_REQUIRED or admin_only
       
  2388 
       
  2389     handler_type = url_map.GetHandlerType()
       
  2390     if handler_type == appinfo.HANDLER_SCRIPT:
       
  2391       dispatcher = cgi_dispatcher
       
  2392     elif handler_type in (appinfo.STATIC_FILES, appinfo.STATIC_DIR):
       
  2393       dispatcher = file_dispatcher
       
  2394     else:
       
  2395       raise InvalidAppConfigError('Unknown handler type "%s"' % handler_type)
       
  2396 
       
  2397     regex = url_map.url
       
  2398     path = url_map.GetHandler()
       
  2399     if handler_type == appinfo.STATIC_DIR:
       
  2400       if regex[-1] == r'/':
       
  2401         regex = regex[:-1]
       
  2402       if path[-1] == os.path.sep:
       
  2403         path = path[:-1]
       
  2404       regex = '/'.join((re.escape(regex), '(.*)'))
       
  2405       if os.path.sep == '\\':
       
  2406         backref = r'\\1'
       
  2407       else:
       
  2408         backref = r'\1'
       
  2409       path = os.path.normpath(path) + os.path.sep + backref
       
  2410 
       
  2411     url_matcher.AddURL(regex,
       
  2412                        dispatcher,
       
  2413                        path,
       
  2414                        requires_login, admin_only)
       
  2415 
       
  2416   return url_matcher
       
  2417 
       
  2418 
       
  2419 def LoadAppConfig(root_path,
       
  2420                   module_dict,
       
  2421                   read_app_config=ReadAppConfig,
       
  2422                   create_matcher=CreateURLMatcherFromMaps):
       
  2423   """Creates a Matcher instance for an application configuration file.
       
  2424 
       
  2425   Raises an InvalidAppConfigError exception if there is anything wrong with
       
  2426   the application configuration file.
       
  2427 
       
  2428   Args:
       
  2429     root_path: Path to the root of the application to load.
       
  2430     module_dict: Dictionary in which application-loaded modules should be
       
  2431       preserved between requests. This dictionary must be separate from the
       
  2432       sys.modules dictionary.
       
  2433     read_url_map, create_matcher: Used for dependency injection.
       
  2434 
       
  2435   Returns:
       
  2436      tuple: (AppInfoExternal, URLMatcher)
       
  2437   """
       
  2438 
       
  2439   for appinfo_path in [os.path.join(root_path, 'app.yaml'),
       
  2440                        os.path.join(root_path, 'app.yml')]:
       
  2441 
       
  2442     if os.path.isfile(appinfo_path):
       
  2443       try:
       
  2444         config = read_app_config(appinfo_path, appinfo.LoadSingleAppInfo)
       
  2445 
       
  2446         matcher = create_matcher(root_path,
       
  2447                                  config.handlers,
       
  2448                                  module_dict)
       
  2449 
       
  2450         return (config, matcher)
       
  2451       except gexcept.AbstractMethod:
       
  2452         pass
       
  2453 
       
  2454   raise AppConfigNotFoundError
       
  2455 
       
  2456 
       
  2457 def SetupStubs(app_id, **config):
       
  2458   """Sets up testing stubs of APIs.
       
  2459 
       
  2460   Args:
       
  2461     app_id: Application ID being served.
       
  2462 
       
  2463   Keywords:
       
  2464     login_url: Relative URL which should be used for handling user login/logout.
       
  2465     datastore_path: Path to the file to store Datastore file stub data in.
       
  2466     history_path: Path to the file to store Datastore history in.
       
  2467     clear_datastore: If the datastore and history should be cleared on startup.
       
  2468     smtp_host: SMTP host used for sending test mail.
       
  2469     smtp_port: SMTP port.
       
  2470     smtp_user: SMTP user.
       
  2471     smtp_password: SMTP password.
       
  2472     enable_sendmail: Whether to use sendmail as an alternative to SMTP.
       
  2473     show_mail_body: Whether to log the body of emails.
       
  2474     remove: Used for dependency injection.
       
  2475   """
       
  2476   login_url = config['login_url']
       
  2477   datastore_path = config['datastore_path']
       
  2478   history_path = config['history_path']
       
  2479   clear_datastore = config['clear_datastore']
       
  2480   require_indexes = config.get('require_indexes', False)
       
  2481   smtp_host = config.get('smtp_host', None)
       
  2482   smtp_port = config.get('smtp_port', 25)
       
  2483   smtp_user = config.get('smtp_user', '')
       
  2484   smtp_password = config.get('smtp_password', '')
       
  2485   enable_sendmail = config.get('enable_sendmail', False)
       
  2486   show_mail_body = config.get('show_mail_body', False)
       
  2487   remove = config.get('remove', os.remove)
       
  2488 
       
  2489   if clear_datastore:
       
  2490     for path in (datastore_path, history_path):
       
  2491       if os.path.lexists(path):
       
  2492         logging.info('Attempting to remove file at %s', path)
       
  2493         try:
       
  2494           remove(path)
       
  2495         except OSError, e:
       
  2496           logging.warning('Removing file failed: %s', e)
       
  2497 
       
  2498   apiproxy_stub_map.apiproxy = apiproxy_stub_map.APIProxyStubMap()
       
  2499 
       
  2500   datastore = datastore_file_stub.DatastoreFileStub(
       
  2501       app_id, datastore_path, history_path, require_indexes=require_indexes)
       
  2502   apiproxy_stub_map.apiproxy.RegisterStub('datastore_v3', datastore)
       
  2503 
       
  2504   fixed_login_url = '%s?%s=%%s' % (login_url,
       
  2505                                    dev_appserver_login.CONTINUE_PARAM)
       
  2506   fixed_logout_url = '%s&%s' % (fixed_login_url,
       
  2507                                 dev_appserver_login.LOGOUT_PARAM)
       
  2508 
       
  2509   apiproxy_stub_map.apiproxy.RegisterStub(
       
  2510     'user',
       
  2511     user_service_stub.UserServiceStub(login_url=fixed_login_url,
       
  2512                                       logout_url=fixed_logout_url))
       
  2513 
       
  2514   apiproxy_stub_map.apiproxy.RegisterStub(
       
  2515     'urlfetch',
       
  2516     urlfetch_stub.URLFetchServiceStub())
       
  2517 
       
  2518   apiproxy_stub_map.apiproxy.RegisterStub(
       
  2519     'mail',
       
  2520     mail_stub.MailServiceStub(smtp_host,
       
  2521                               smtp_port,
       
  2522                               smtp_user,
       
  2523                               smtp_password,
       
  2524                               enable_sendmail=enable_sendmail,
       
  2525                               show_mail_body=show_mail_body))
       
  2526 
       
  2527   apiproxy_stub_map.apiproxy.RegisterStub(
       
  2528     'memcache',
       
  2529     memcache_stub.MemcacheServiceStub())
       
  2530 
       
  2531   try:
       
  2532     from google.appengine.api.images import images_stub
       
  2533     apiproxy_stub_map.apiproxy.RegisterStub(
       
  2534       'images',
       
  2535       images_stub.ImagesServiceStub())
       
  2536   except ImportError, e:
       
  2537     logging.warning('Could not initialize images API; you are likely missing '
       
  2538                     'the Python "PIL" module. ImportError: %s', e)
       
  2539     from google.appengine.api.images import images_not_implemented_stub
       
  2540     apiproxy_stub_map.apiproxy.RegisterStub('images',
       
  2541       images_not_implemented_stub.ImagesNotImplementedServiceStub())
       
  2542 
       
  2543 
       
  2544 def CreateImplicitMatcher(module_dict,
       
  2545                           root_path,
       
  2546                           login_url,
       
  2547                           create_path_adjuster=PathAdjuster,
       
  2548                           create_local_dispatcher=LocalCGIDispatcher,
       
  2549                           create_cgi_dispatcher=CGIDispatcher):
       
  2550   """Creates a URLMatcher instance that handles internal URLs.
       
  2551 
       
  2552   Used to facilitate handling user login/logout, debugging, info about the
       
  2553   currently running app, etc.
       
  2554 
       
  2555   Args:
       
  2556     module_dict: Dictionary in the form used by sys.modules.
       
  2557     root_path: Path to the root of the application.
       
  2558     login_url: Relative URL which should be used for handling user login/logout.
       
  2559     create_local_dispatcher: Used for dependency injection.
       
  2560 
       
  2561   Returns:
       
  2562     Instance of URLMatcher with appropriate dispatchers.
       
  2563   """
       
  2564   url_matcher = URLMatcher()
       
  2565   path_adjuster = create_path_adjuster(root_path)
       
  2566 
       
  2567   login_dispatcher = create_local_dispatcher(sys.modules, path_adjuster,
       
  2568                                              dev_appserver_login.main)
       
  2569   url_matcher.AddURL(login_url,
       
  2570                      login_dispatcher,
       
  2571                      '',
       
  2572                      False,
       
  2573                      False)
       
  2574 
       
  2575 
       
  2576   admin_dispatcher = create_cgi_dispatcher(module_dict, root_path,
       
  2577                                            path_adjuster)
       
  2578   url_matcher.AddURL('/_ah/admin(?:/.*)?',
       
  2579                      admin_dispatcher,
       
  2580                      DEVEL_CONSOLE_PATH,
       
  2581                      False,
       
  2582                      False)
       
  2583 
       
  2584   return url_matcher
       
  2585 
       
  2586 
       
  2587 def SetupTemplates(template_dir):
       
  2588   """Reads debugging console template files and initializes the console.
       
  2589 
       
  2590   Does nothing if templates have already been initialized.
       
  2591 
       
  2592   Args:
       
  2593     template_dir: Path to the directory containing the templates files.
       
  2594 
       
  2595   Raises:
       
  2596     OSError or IOError if any of the template files could not be read.
       
  2597   """
       
  2598   if ApplicationLoggingHandler.AreTemplatesInitialized():
       
  2599     return
       
  2600 
       
  2601   try:
       
  2602     header = open(os.path.join(template_dir, HEADER_TEMPLATE)).read()
       
  2603     script = open(os.path.join(template_dir, SCRIPT_TEMPLATE)).read()
       
  2604     middle = open(os.path.join(template_dir, MIDDLE_TEMPLATE)).read()
       
  2605     footer = open(os.path.join(template_dir, FOOTER_TEMPLATE)).read()
       
  2606   except (OSError, IOError):
       
  2607     logging.error('Could not read template files from %s', template_dir)
       
  2608     raise
       
  2609 
       
  2610   ApplicationLoggingHandler.InitializeTemplates(header, script, middle, footer)
       
  2611 
       
  2612 
       
  2613 def CreateServer(root_path,
       
  2614                  login_url,
       
  2615                  port,
       
  2616                  template_dir,
       
  2617                  serve_address='',
       
  2618                  require_indexes=False,
       
  2619                  python_path_list=sys.path):
       
  2620   """Creates an new HTTPServer for an application.
       
  2621 
       
  2622   Args:
       
  2623     root_path: String containing the path to the root directory of the
       
  2624       application where the app.yaml file is.
       
  2625     login_url: Relative URL which should be used for handling user login/logout.
       
  2626     port: Port to start the application server on.
       
  2627     template_dir: Path to the directory in which the debug console templates
       
  2628       are stored.
       
  2629     serve_address: Address on which the server should serve.
       
  2630     require_indexes: True if index.yaml is read-only gospel; default False.
       
  2631     python_path_list: Used for dependency injection.
       
  2632 
       
  2633   Returns:
       
  2634     Instance of BaseHTTPServer.HTTPServer that's ready to start accepting.
       
  2635   """
       
  2636   absolute_root_path = os.path.abspath(root_path)
       
  2637 
       
  2638   SetupTemplates(template_dir)
       
  2639   FakeFile.SetAllowedPaths([absolute_root_path,
       
  2640                             os.path.dirname(os.path.dirname(google.__file__)),
       
  2641                             template_dir])
       
  2642 
       
  2643   handler_class = CreateRequestHandler(absolute_root_path, login_url,
       
  2644                                        require_indexes)
       
  2645 
       
  2646   if absolute_root_path not in python_path_list:
       
  2647     python_path_list.insert(0, absolute_root_path)
       
  2648 
       
  2649   return BaseHTTPServer.HTTPServer((serve_address, port), handler_class)