App Engine Python SDK version 1.8.5
[gae.git] / python / google / appengine / tools / dev_appserver.py
blob4352cd698a233862ad65e98eb3c930767d49233f
1 #!/usr/bin/env python
3 # Copyright 2007 Google Inc.
5 # Licensed under the Apache License, Version 2.0 (the "License");
6 # you may not use this file except in compliance with the License.
7 # You may obtain a copy of the License at
9 # http://www.apache.org/licenses/LICENSE-2.0
11 # Unless required by applicable law or agreed to in writing, software
12 # distributed under the License is distributed on an "AS IS" BASIS,
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 # See the License for the specific language governing permissions and
15 # limitations under the License.
20 """Pure-Python application server for testing applications locally.
22 Given a port and the paths to a valid application directory (with an 'app.yaml'
23 file), the external library directory, and a relative URL to use for logins,
24 creates an HTTP server that can be used to test an application locally. Uses
25 stubs instead of actual APIs when SetupStubs() is called first.
27 Example:
28 root_path = '/path/to/application/directory'
29 login_url = '/login'
30 port = 8080
31 server = dev_appserver.CreateServer(root_path, login_url, port)
32 server.serve_forever()
33 """
35 from __future__ import with_statement
39 from google.appengine.tools import os_compat
41 import __builtin__
42 import BaseHTTPServer
43 import base64
44 import binascii
45 import calendar
46 import cStringIO
47 import cgi
48 import cgitb
49 import email.Utils
50 import errno
51 import hashlib
52 import heapq
53 import httplib
54 import imp
55 import inspect
56 import logging
57 import mimetools
58 import mimetypes
59 import os
60 import select
61 import shutil
62 import simplejson
63 import StringIO
64 import struct
65 import tempfile
66 import wsgiref.headers
67 import yaml
74 import re
75 import sre_compile
76 import sre_constants
77 import sre_parse
79 import socket
80 import sys
81 import time
82 import types
83 import urlparse
84 import urllib
85 import zlib
87 import google
91 try:
92 from google.third_party.apphosting.python.webapp2 import v2_3 as tmp
93 sys.path.append(os.path.dirname(tmp.__file__))
94 del tmp
95 except ImportError:
96 pass
98 from google.appengine.api import apiproxy_stub_map
99 from google.appengine.api import appinfo
100 from google.appengine.api import appinfo_includes
101 from google.appengine.api import app_logging
102 from google.appengine.api import blobstore
103 from google.appengine.api import croninfo
104 from google.appengine.api import datastore
105 from google.appengine.api import datastore_file_stub
106 from google.appengine.api import lib_config
107 from google.appengine.api import mail
108 from google.appengine.api import mail_stub
109 from google.appengine.api import namespace_manager
110 from google.appengine.api import request_info
111 from google.appengine.api import urlfetch_stub
112 from google.appengine.api import user_service_stub
113 from google.appengine.api import yaml_errors
114 from google.appengine.api.app_identity import app_identity_stub
115 from google.appengine.api.blobstore import blobstore_stub
116 from google.appengine.api.blobstore import file_blob_storage
117 from google.appengine.api.capabilities import capability_stub
118 from google.appengine.api.channel import channel_service_stub
119 from google.appengine.api.files import file_service_stub
120 from google.appengine.api.logservice import logservice
121 from google.appengine.api.logservice import logservice_stub
122 from google.appengine.api.search import simple_search_stub
123 from google.appengine.api.taskqueue import taskqueue_stub
124 from google.appengine.api.prospective_search import prospective_search_stub
125 from google.appengine.api.remote_socket import _remote_socket_stub
126 from google.appengine.api.memcache import memcache_stub
127 from google.appengine.api import rdbms_mysqldb
129 from google.appengine.api.system import system_stub
130 from google.appengine.api.xmpp import xmpp_service_stub
131 from google.appengine.datastore import datastore_sqlite_stub
132 from google.appengine.datastore import datastore_stub_util
133 from google.appengine.ext.cloudstorage import stub_dispatcher as gcs_dispatcher
134 from google.appengine import dist
136 try:
137 from google.appengine.runtime import request_environment
138 from google.appengine.runtime import runtime
139 except:
141 request_environment = None
142 runtime = None
144 from google.appengine.tools import dev_appserver_apiserver
145 from google.appengine.tools import dev_appserver_blobimage
146 from google.appengine.tools import dev_appserver_blobstore
147 from google.appengine.tools import dev_appserver_channel
148 from google.appengine.tools import dev_appserver_import_hook
149 from google.appengine.tools import dev_appserver_login
150 from google.appengine.tools import dev_appserver_multiprocess as multiprocess
151 from google.appengine.tools import dev_appserver_oauth
152 from google.appengine.tools import dev_appserver_upload
154 from google.storage.speckle.python.api import rdbms
157 CouldNotFindModuleError = dev_appserver_import_hook.CouldNotFindModuleError
158 FakeAccess = dev_appserver_import_hook.FakeAccess
159 FakeFile = dev_appserver_import_hook.FakeFile
160 FakeReadlink = dev_appserver_import_hook.FakeReadlink
161 FakeSetLocale = dev_appserver_import_hook.FakeSetLocale
162 FakeUnlink = dev_appserver_import_hook.FakeUnlink
163 GetSubmoduleName = dev_appserver_import_hook.GetSubmoduleName
164 HardenedModulesHook = dev_appserver_import_hook.HardenedModulesHook
168 SDK_ROOT = dev_appserver_import_hook.SDK_ROOT
171 PYTHON_LIB_VAR = '$PYTHON_LIB'
172 DEVEL_CONSOLE_PATH = PYTHON_LIB_VAR + '/google/appengine/ext/admin'
173 REMOTE_API_PATH = (PYTHON_LIB_VAR +
174 '/google/appengine/ext/remote_api/handler.py')
177 FILE_MISSING_EXCEPTIONS = frozenset([errno.ENOENT, errno.ENOTDIR])
181 MAX_URL_LENGTH = 2047
185 DEFAULT_ENV = {
186 'GATEWAY_INTERFACE': 'CGI/1.1',
187 'AUTH_DOMAIN': 'gmail.com',
188 'USER_ORGANIZATION': '',
189 'TZ': 'UTC',
193 DEFAULT_SELECT_DELAY = 30.0
197 for ext, mime_type in mail.EXTENSION_MIME_MAP.iteritems():
198 mimetypes.add_type(mime_type, '.' + ext)
202 MAX_RUNTIME_RESPONSE_SIZE = 32 << 20
206 MAX_REQUEST_SIZE = 32 * 1024 * 1024
209 COPY_BLOCK_SIZE = 1 << 20
213 API_VERSION = '1'
218 VERSION_FILE = '../../VERSION'
223 DEVEL_PAYLOAD_HEADER = 'HTTP_X_APPENGINE_DEVELOPMENT_PAYLOAD'
224 DEVEL_PAYLOAD_RAW_HEADER = 'X-AppEngine-Development-Payload'
226 DEVEL_FAKE_IS_ADMIN_HEADER = 'HTTP_X_APPENGINE_FAKE_IS_ADMIN'
227 DEVEL_FAKE_IS_ADMIN_RAW_HEADER = 'X-AppEngine-Fake-Is-Admin'
229 FILE_STUB_DEPRECATION_MESSAGE = (
230 """The datastore file stub is deprecated, and
231 will stop being the default in a future release.
232 Append the --use_sqlite flag to use the new SQLite stub.
234 You can port your existing data using the --port_sqlite_data flag or
235 purge your previous test data with --clear_datastore.
236 """)
242 NON_PUBLIC_CACHE_CONTROLS = frozenset(['private', 'no-cache', 'no-store'])
246 class Error(Exception):
247 """Base-class for exceptions in this module."""
250 class InvalidAppConfigError(Error):
251 """The supplied application configuration file is invalid."""
254 class AppConfigNotFoundError(Error):
255 """Application configuration file not found."""
258 class CompileError(Error):
259 """Application could not be compiled."""
260 def __init__(self, text):
261 self.text = text
263 class ExecuteError(Error):
264 """Application could not be executed."""
265 def __init__(self, text, log):
266 self.text = text
267 self.log = log
271 def MonkeyPatchPdb(pdb):
272 """Given a reference to the pdb module, fix its set_trace function.
274 This will allow the standard trick of setting a breakpoint in your
275 code by inserting a call to pdb.set_trace() to work properly, as
276 long as the original stdin and stdout of dev_appserver.py are
277 connected to a console or shell window.
280 def NewSetTrace():
281 """Replacement for set_trace() that uses the original i/o streams.
283 This is necessary because by the time the user code that might
284 invoke pdb.set_trace() runs, the default sys.stdin and sys.stdout
285 are redirected to the HTTP request and response streams instead,
286 so that pdb will encounter garbage (or EOF) in its input, and its
287 output will garble the HTTP response. Fortunately, sys.__stdin__
288 and sys.__stderr__ retain references to the original streams --
289 this is a standard Python feature. Also, fortunately, as of
290 Python 2.5, the Pdb class lets you easily override stdin and
291 stdout. The original set_trace() function does essentially the
292 same thing as the code here except it instantiates Pdb() without
293 arguments.
295 p = pdb.Pdb(stdin=sys.__stdin__, stdout=sys.__stdout__)
296 p.set_trace(sys._getframe().f_back)
298 pdb.set_trace = NewSetTrace
301 def MonkeyPatchThreadingLocal(_threading_local):
302 """Given a reference to the _threading_local module, fix _localbase.__new__.
304 This ensures that using dev_appserver with a Python interpreter older than
305 2.7 will include the fix to the _threading_local._localbase.__new__ method
306 which was introduced in Python 2.7 (http://bugs.python.org/issue1522237).
309 @staticmethod
310 def New(cls, *args, **kw):
311 self = object.__new__(cls)
312 key = '_local__key', 'thread.local.' + str(id(self))
313 object.__setattr__(self, '_local__key', key)
314 object.__setattr__(self, '_local__args', (args, kw))
315 object.__setattr__(self, '_local__lock', _threading_local.RLock())
316 if (args or kw) and (cls.__init__ is object.__init__):
317 raise TypeError('Initialization arguments are not supported')
318 dict = object.__getattribute__(self, '__dict__')
319 _threading_local.current_thread().__dict__[key] = dict
320 return self
322 _threading_local._localbase.__new__ = New
325 def SplitURL(relative_url):
326 """Splits a relative URL into its path and query-string components.
328 Args:
329 relative_url: String containing the relative URL (often starting with '/')
330 to split. Should be properly escaped as www-form-urlencoded data.
332 Returns:
333 Tuple (script_name, query_string) where:
334 script_name: Relative URL of the script that was accessed.
335 query_string: String containing everything after the '?' character.
337 (unused_scheme, unused_netloc, path, query,
338 unused_fragment) = urlparse.urlsplit(relative_url)
339 return path, query
342 def GetFullURL(server_name, server_port, relative_url):
343 """Returns the full, original URL used to access the relative URL.
345 Args:
346 server_name: Name of the local host, or the value of the 'host' header
347 from the request.
348 server_port: Port on which the request was served (string or int).
349 relative_url: Relative URL that was accessed, including query string.
351 Returns:
352 String containing the original URL.
354 if str(server_port) != '80':
355 netloc = '%s:%s' % (server_name, server_port)
356 else:
357 netloc = server_name
358 return 'http://%s%s' % (netloc, relative_url)
360 def CopyStreamPart(source, destination, content_size):
361 """Copy a portion of a stream from one file-like object to another.
363 Args:
364 source: Source stream to copy from.
365 destination: Destination stream to copy to.
366 content_size: Maximum bytes to copy.
368 Returns:
369 Number of bytes actually copied.
371 bytes_copied = 0
372 bytes_left = content_size
373 while bytes_left > 0:
374 bytes = source.read(min(bytes_left, COPY_BLOCK_SIZE))
375 bytes_read = len(bytes)
376 if bytes_read == 0:
377 break
378 destination.write(bytes)
379 bytes_copied += bytes_read
380 bytes_left -= bytes_read
381 return bytes_copied
384 def AppIdWithDefaultPartition(app_id, default_partition):
385 """Add a partition to an application id if necessary."""
386 if not default_partition:
387 return app_id
391 if '~' in app_id:
392 return app_id
394 return default_partition + '~' + app_id
399 class AppServerRequest(object):
400 """Encapsulates app-server request.
402 Object used to hold a full appserver request. Used as a container that is
403 passed through the request forward chain and ultimately sent to the
404 URLDispatcher instances.
406 Attributes:
407 relative_url: String containing the URL accessed.
408 path: Local path of the resource that was matched; back-references will be
409 replaced by values matched in the relative_url. Path may be relative
410 or absolute, depending on the resource being served (e.g., static files
411 will have an absolute path; scripts will be relative).
412 headers: Instance of mimetools.Message with headers from the request.
413 infile: File-like object with input data from the request.
414 force_admin: Allow request admin-only URLs to proceed regardless of whether
415 user is logged in or is an admin.
418 ATTRIBUTES = ['relative_url',
419 'path',
420 'headers',
421 'infile',
422 'force_admin',
425 def __init__(self,
426 relative_url,
427 path,
428 headers,
429 infile,
430 force_admin=False):
431 """Constructor.
433 Args:
434 relative_url: Mapped directly to attribute.
435 path: Mapped directly to attribute.
436 headers: Mapped directly to attribute.
437 infile: Mapped directly to attribute.
438 force_admin: Mapped directly to attribute.
440 self.relative_url = relative_url
441 self.path = path
442 self.headers = headers
443 self.infile = infile
444 self.force_admin = force_admin
445 if (DEVEL_PAYLOAD_RAW_HEADER in self.headers or
446 DEVEL_FAKE_IS_ADMIN_RAW_HEADER in self.headers):
447 self.force_admin = True
449 def __eq__(self, other):
450 """Used mainly for testing.
452 Returns:
453 True if all fields of both requests are equal, else False.
455 if type(self) == type(other):
456 for attribute in self.ATTRIBUTES:
457 if getattr(self, attribute) != getattr(other, attribute):
458 return False
459 return True
461 def __repr__(self):
462 """String representation of request.
464 Used mainly for testing.
466 Returns:
467 String representation of AppServerRequest. Strings of different
468 request objects that have the same values for all fields compare
469 as equal.
471 results = []
472 for attribute in self.ATTRIBUTES:
473 results.append('%s: %s' % (attribute, getattr(self, attribute)))
474 return '<AppServerRequest %s>' % ' '.join(results)
477 class URLDispatcher(object):
478 """Base-class for handling HTTP requests."""
480 def Dispatch(self,
481 request,
482 outfile,
483 base_env_dict=None):
484 """Dispatch and handle an HTTP request.
486 base_env_dict should contain at least these CGI variables:
487 REQUEST_METHOD, REMOTE_ADDR, SERVER_SOFTWARE, SERVER_NAME,
488 SERVER_PROTOCOL, SERVER_PORT
490 Args:
491 request: AppServerRequest instance.
492 outfile: File-like object where output data should be written.
493 base_env_dict: Dictionary of CGI environment parameters if available.
494 Defaults to None.
496 Returns:
497 None if request handling is complete.
498 A new AppServerRequest instance if internal redirect is required.
500 raise NotImplementedError
502 def EndRedirect(self, dispatched_output, original_output):
503 """Process the end of an internal redirect.
505 This method is called after all subsequent dispatch requests have finished.
506 By default the output from the dispatched process is copied to the original.
508 This will not be called on dispatchers that do not return an internal
509 redirect.
511 Args:
512 dispatched_output: StringIO buffer containing the results from the
513 dispatched
514 original_output: The original output file.
516 Returns:
517 None if request handling is complete.
518 A new AppServerRequest instance if internal redirect is required.
520 original_output.write(dispatched_output.read())
523 class URLMatcher(object):
524 """Matches an arbitrary URL using a list of URL patterns from an application.
526 Each URL pattern has an associated URLDispatcher instance and path to the
527 resource's location on disk. See AddURL for more details. The first pattern
528 that matches an inputted URL will have its associated values returned by
529 Match().
532 def __init__(self):
533 """Initializer."""
537 self._url_patterns = []
539 def AddURL(self, regex, dispatcher, path, requires_login, admin_only,
540 auth_fail_action):
541 """Adds a URL pattern to the list of patterns.
543 If the supplied regex starts with a '^' or ends with a '$' an
544 InvalidAppConfigError exception will be raised. Start and end symbols
545 and implicitly added to all regexes, meaning we assume that all regexes
546 consume all input from a URL.
548 Args:
549 regex: String containing the regular expression pattern.
550 dispatcher: Instance of URLDispatcher that should handle requests that
551 match this regex.
552 path: Path on disk for the resource. May contain back-references like
553 r'\1', r'\2', etc, which will be replaced by the corresponding groups
554 matched by the regex if present.
555 requires_login: True if the user must be logged-in before accessing this
556 URL; False if anyone can access this URL.
557 admin_only: True if the user must be a logged-in administrator to
558 access the URL; False if anyone can access the URL.
559 auth_fail_action: either appinfo.AUTH_FAIL_ACTION_REDIRECT (default)
560 which indicates that the server should redirect to the login page when
561 an authentication is needed, or appinfo.AUTH_FAIL_ACTION_UNAUTHORIZED
562 which indicates that the server should just return a 401 Unauthorized
563 message immediately.
565 Raises:
566 TypeError: if dispatcher is not a URLDispatcher sub-class instance.
567 InvalidAppConfigError: if regex isn't valid.
569 if not isinstance(dispatcher, URLDispatcher):
570 raise TypeError('dispatcher must be a URLDispatcher sub-class')
572 if regex.startswith('^') or regex.endswith('$'):
573 raise InvalidAppConfigError('regex starts with "^" or ends with "$"')
575 adjusted_regex = '^%s$' % regex
577 try:
578 url_re = re.compile(adjusted_regex)
579 except re.error, e:
580 raise InvalidAppConfigError('regex invalid: %s' % e)
582 match_tuple = (url_re, dispatcher, path, requires_login, admin_only,
583 auth_fail_action)
584 self._url_patterns.append(match_tuple)
586 def Match(self,
587 relative_url,
588 split_url=SplitURL):
589 """Matches a URL from a request against the list of URL patterns.
591 The supplied relative_url may include the query string (i.e., the '?'
592 character and everything following).
594 Args:
595 relative_url: Relative URL being accessed in a request.
596 split_url: Used for dependency injection.
598 Returns:
599 Tuple (dispatcher, matched_path, requires_login, admin_only,
600 auth_fail_action), which are the corresponding values passed to
601 AddURL when the matching URL pattern was added to this matcher.
602 The matched_path will have back-references replaced using values
603 matched by the URL pattern. If no match was found, dispatcher will
604 be None.
607 adjusted_url, unused_query_string = split_url(relative_url)
609 for url_tuple in self._url_patterns:
610 url_re, dispatcher, path, requires_login, admin_only, auth_fail_action = url_tuple
611 the_match = url_re.match(adjusted_url)
613 if the_match:
614 adjusted_path = the_match.expand(path)
615 return (dispatcher, adjusted_path, requires_login, admin_only,
616 auth_fail_action)
618 return None, None, None, None, None
620 def GetDispatchers(self):
621 """Retrieves the URLDispatcher objects that could be matched.
623 Should only be used in tests.
625 Returns:
626 A set of URLDispatcher objects.
628 return set([url_tuple[1] for url_tuple in self._url_patterns])
633 class MatcherDispatcher(URLDispatcher):
634 """Dispatcher across multiple URLMatcher instances."""
636 def __init__(self,
637 config,
638 login_url,
639 module_manager,
640 url_matchers,
641 get_user_info=dev_appserver_login.GetUserInfo,
642 login_redirect=dev_appserver_login.LoginRedirect):
643 """Initializer.
645 Args:
646 config: AppInfoExternal instance representing the parsed app.yaml file.
647 login_url: Relative URL which should be used for handling user logins.
648 module_manager: ModuleManager instance that is used to detect and reload
649 modules if the matched Dispatcher is dynamic.
650 url_matchers: Sequence of URLMatcher objects.
651 get_user_info: Used for dependency injection.
652 login_redirect: Used for dependency injection.
654 self._config = config
655 self._login_url = login_url
656 self._module_manager = module_manager
657 self._url_matchers = tuple(url_matchers)
658 self._get_user_info = get_user_info
659 self._login_redirect = login_redirect
661 def Dispatch(self,
662 request,
663 outfile,
664 base_env_dict=None):
665 """Dispatches a request to the first matching dispatcher.
667 Matchers are checked in the order they were supplied to the constructor.
668 If no matcher matches, a 404 error will be written to the outfile. The
669 path variable supplied to this method is ignored.
671 The value of request.path is ignored.
673 cookies = ', '.join(request.headers.getheaders('cookie'))
674 email_addr, admin, user_id = self._get_user_info(cookies)
676 for matcher in self._url_matchers:
677 dispatcher, matched_path, requires_login, admin_only, auth_fail_action = matcher.Match(request.relative_url)
678 if dispatcher is None:
679 continue
681 logging.debug('Matched "%s" to %s with path %s',
682 request.relative_url, dispatcher, matched_path)
684 if ((requires_login or admin_only) and
685 not email_addr and
686 not request.force_admin):
687 logging.debug('Login required, redirecting user')
688 if auth_fail_action == appinfo.AUTH_FAIL_ACTION_REDIRECT:
689 self._login_redirect(self._login_url,
690 base_env_dict['SERVER_NAME'],
691 base_env_dict['SERVER_PORT'],
692 request.relative_url,
693 outfile)
694 elif auth_fail_action == appinfo.AUTH_FAIL_ACTION_UNAUTHORIZED:
695 outfile.write('Status: %d Not authorized\r\n'
696 '\r\n'
697 'Login required to view page.'
698 % (httplib.UNAUTHORIZED))
699 elif admin_only and not admin and not request.force_admin:
700 outfile.write('Status: %d Not authorized\r\n'
701 '\r\n'
702 'Current logged in user %s is not '
703 'authorized to view this page.'
704 % (httplib.FORBIDDEN, email_addr))
705 else:
706 request.path = matched_path
712 if (not isinstance(dispatcher, FileDispatcher) and
713 self._module_manager.AreModuleFilesModified()):
714 self._module_manager.ResetModules()
716 forward_request = dispatcher.Dispatch(request,
717 outfile,
718 base_env_dict=base_env_dict)
720 while forward_request:
722 logging.info('Internal redirection to %s',
723 forward_request.relative_url)
724 new_outfile = cStringIO.StringIO()
725 self.Dispatch(forward_request,
726 new_outfile,
727 dict(base_env_dict))
729 new_outfile.seek(0)
730 forward_request = dispatcher.EndRedirect(new_outfile, outfile)
733 return
735 outfile.write('Status: %d URL did not match\r\n'
736 '\r\n'
737 'Not found error: %s did not match any patterns '
738 'in application configuration.'
739 % (httplib.NOT_FOUND, request.relative_url))
745 _IGNORE_REQUEST_HEADERS = frozenset([
746 'accept-encoding',
747 'connection',
748 'keep-alive',
749 'proxy-authorization',
750 'te',
751 'trailer',
752 'transfer-encoding',
755 'content-type',
756 'content-length',
760 _request_id = 0
761 _request_time = 0
764 def _generate_request_id_hash():
765 """Generates a hash of the current request id."""
766 return hashlib.sha1(str(_request_id)).hexdigest()[:8].upper()
769 def _GenerateRequestLogId():
770 """Generates the request log id for the current request."""
771 sec = int(_request_time)
772 usec = int(1000000 * (_request_time - sec))
773 h = hashlib.sha1(str(_request_id)).digest()[:4]
774 packed = struct.Struct('> L L').pack(sec, usec)
775 return binascii.b2a_hex(packed + h)
778 def GetGoogleSqlOAuth2RefreshToken(oauth_file_path):
779 """Reads the user's Google Cloud SQL OAuth2.0 token from disk."""
780 if not os.path.exists(oauth_file_path):
781 return None
782 try:
783 with open(oauth_file_path) as oauth_file:
784 token = simplejson.load(oauth_file)
785 return token['refresh_token']
786 except (IOError, KeyError, simplejson.decoder.JSONDecodeError):
787 logging.exception(
788 'Could not read OAuth2.0 token from %s', oauth_file_path)
789 return None
792 def SetupEnvironment(cgi_path,
793 relative_url,
794 headers,
795 infile,
796 split_url=SplitURL,
797 get_user_info=dev_appserver_login.GetUserInfo):
798 """Sets up environment variables for a CGI.
800 Args:
801 cgi_path: Full file-system path to the CGI being executed.
802 relative_url: Relative URL used to access the CGI.
803 headers: Instance of mimetools.Message containing request headers.
804 infile: File-like object with input data from the request.
805 split_url, get_user_info: Used for dependency injection.
807 Returns:
808 Dictionary containing CGI environment variables.
810 env = DEFAULT_ENV.copy()
812 script_name, query_string = split_url(relative_url)
817 env['_AH_ENCODED_SCRIPT_NAME'] = script_name
818 env['SCRIPT_NAME'] = ''
819 env['QUERY_STRING'] = query_string
820 env['PATH_INFO'] = urllib.unquote(script_name)
821 env['PATH_TRANSLATED'] = cgi_path
822 env['CONTENT_TYPE'] = headers.getheader('content-type',
823 'application/x-www-form-urlencoded')
824 env['CONTENT_LENGTH'] = headers.getheader('content-length', '')
826 cookies = ', '.join(headers.getheaders('cookie'))
827 email_addr, admin, user_id = get_user_info(cookies)
828 env['USER_EMAIL'] = email_addr
829 env['USER_ID'] = user_id
830 if admin:
831 env['USER_IS_ADMIN'] = '1'
832 if env['AUTH_DOMAIN'] == '*':
834 auth_domain = 'gmail.com'
835 parts = email_addr.split('@')
836 if len(parts) == 2 and parts[1]:
837 auth_domain = parts[1]
838 env['AUTH_DOMAIN'] = auth_domain
840 env['REQUEST_LOG_ID'] = _GenerateRequestLogId()
841 env['REQUEST_ID_HASH'] = _generate_request_id_hash()
844 for key in headers:
845 if key in _IGNORE_REQUEST_HEADERS:
846 continue
847 adjusted_name = key.replace('-', '_').upper()
848 env['HTTP_' + adjusted_name] = ', '.join(headers.getheaders(key))
853 if DEVEL_PAYLOAD_HEADER in env:
854 del env[DEVEL_PAYLOAD_HEADER]
855 new_data = base64.standard_b64decode(infile.getvalue())
856 infile.seek(0)
857 infile.truncate()
858 infile.write(new_data)
859 infile.seek(0)
860 env['CONTENT_LENGTH'] = str(len(new_data))
864 if DEVEL_FAKE_IS_ADMIN_HEADER in env:
865 del env[DEVEL_FAKE_IS_ADMIN_HEADER]
867 token = GetGoogleSqlOAuth2RefreshToken(os.path.expanduser(
868 rdbms.OAUTH_CREDENTIALS_PATH))
869 if token:
870 env['GOOGLE_SQL_OAUTH2_REFRESH_TOKEN'] = token
872 return env
875 def NotImplementedFake(*args, **kwargs):
876 """Fake for methods/functions that are not implemented in the production
877 environment.
879 raise NotImplementedError('This class/method is not available.')
882 class NotImplementedFakeClass(object):
883 """Fake class for classes that are not implemented in the production env.
885 __init__ = NotImplementedFake
888 def IsEncodingsModule(module_name):
889 """Determines if the supplied module is related to encodings in any way.
891 Encodings-related modules cannot be reloaded, so they need to be treated
892 specially when sys.modules is modified in any way.
894 Args:
895 module_name: Absolute name of the module regardless of how it is imported
896 into the local namespace (e.g., foo.bar.baz).
898 Returns:
899 True if it's an encodings-related module; False otherwise.
901 if (module_name in ('codecs', 'encodings') or
902 module_name.startswith('encodings.')):
903 return True
904 return False
907 def ClearAllButEncodingsModules(module_dict):
908 """Clear all modules in a module dictionary except for those modules that
909 are in any way related to encodings.
911 Args:
912 module_dict: Dictionary in the form used by sys.modules.
914 for module_name in module_dict.keys():
917 if not IsEncodingsModule(module_name) and module_name != 'sys':
918 del module_dict[module_name]
921 def ConnectAndDisconnectChildModules(old_module_dict, new_module_dict):
922 """Prepares for switching from old_module_dict to new_module_dict.
924 Disconnects child modules going away from parents that remain, and reconnects
925 child modules that are being added back in to old parents. This is needed to
926 satisfy code that follows the getattr() descendant chain rather than looking
927 up the desired module directly in the module dict.
929 Args:
930 old_module_dict: The module dict being replaced, looks like sys.modules.
931 new_module_dict: The module dict takings its place, looks like sys.modules.
933 old_keys = set(old_module_dict.keys())
934 new_keys = set(new_module_dict.keys())
935 for deleted_module_name in old_keys - new_keys:
936 if old_module_dict[deleted_module_name] is None:
937 continue
938 segments = deleted_module_name.rsplit('.', 1)
939 if len(segments) == 2:
940 parent_module = new_module_dict.get(segments[0])
941 if parent_module and hasattr(parent_module, segments[1]):
942 delattr(parent_module, segments[1])
943 for added_module_name in new_keys - old_keys:
944 if new_module_dict[added_module_name] is None:
945 continue
946 segments = added_module_name.rsplit('.', 1)
947 if len(segments) == 2:
948 parent_module = old_module_dict.get(segments[0])
949 child_module = new_module_dict[added_module_name]
950 if (parent_module and
951 getattr(parent_module, segments[1], None) is not child_module):
952 setattr(parent_module, segments[1], child_module)
958 SHARED_MODULE_PREFIXES = set([
959 'google',
960 'logging',
961 'sys',
962 'warnings',
967 're',
968 'sre_compile',
969 'sre_constants',
970 'sre_parse',
973 'email',
978 'wsgiref',
980 'MySQLdb',
988 'decimal',
991 NOT_SHARED_MODULE_PREFIXES = set([
992 'google.appengine.ext',
996 def ModuleNameHasPrefix(module_name, prefix_set):
997 """Determines if a module's name belongs to a set of prefix strings.
999 Args:
1000 module_name: String containing the fully qualified module name.
1001 prefix_set: Iterable set of module name prefixes to check against.
1003 Returns:
1004 True if the module_name belongs to the prefix set or is a submodule of
1005 any of the modules specified in the prefix_set. Otherwise False.
1007 for prefix in prefix_set:
1008 if prefix == module_name:
1009 return True
1011 if module_name.startswith(prefix + '.'):
1012 return True
1014 return False
1017 def SetupSharedModules(module_dict):
1018 """Creates a module dictionary for the hardened part of the process.
1020 Module dictionary will contain modules that should be shared between the
1021 hardened and unhardened parts of the process.
1023 Args:
1024 module_dict: Module dictionary from which existing modules should be
1025 pulled (usually sys.modules).
1027 Returns:
1028 A new module dictionary.
1030 output_dict = {}
1031 for module_name, module in module_dict.iteritems():
1038 if module is None:
1039 continue
1041 if IsEncodingsModule(module_name):
1042 output_dict[module_name] = module
1043 continue
1045 shared_prefix = ModuleNameHasPrefix(module_name, SHARED_MODULE_PREFIXES)
1046 banned_prefix = ModuleNameHasPrefix(module_name, NOT_SHARED_MODULE_PREFIXES)
1048 if shared_prefix and not banned_prefix:
1049 output_dict[module_name] = module
1051 return output_dict
1057 def ModuleHasValidMainFunction(module):
1058 """Determines if a module has a main function that takes no arguments.
1060 This includes functions that have arguments with defaults that are all
1061 assigned, thus requiring no additional arguments in order to be called.
1063 Args:
1064 module: A types.ModuleType instance.
1066 Returns:
1067 True if the module has a valid, reusable main function; False otherwise.
1069 if hasattr(module, 'main') and type(module.main) is types.FunctionType:
1070 arg_names, var_args, var_kwargs, default_values = inspect.getargspec(
1071 module.main)
1072 if len(arg_names) == 0:
1073 return True
1074 if default_values is not None and len(arg_names) == len(default_values):
1075 return True
1076 return False
1079 def CheckScriptExists(cgi_path, handler_path):
1080 """Check that the given handler_path is a file that exists on disk.
1082 Args:
1083 cgi_path: Absolute path to the CGI script file on disk.
1084 handler_path: CGI path stored in the application configuration (as a path
1085 like 'foo/bar/baz.py'). May contain $PYTHON_LIB references.
1087 Raises:
1088 CouldNotFindModuleError: if the given handler_path is a file and doesn't
1089 have the expected extension.
1091 if handler_path.startswith(PYTHON_LIB_VAR + '/'):
1093 return
1095 if (not os.path.isdir(cgi_path) and
1096 not os.path.isfile(cgi_path) and
1097 os.path.isfile(cgi_path + '.py')):
1098 raise CouldNotFindModuleError(
1099 'Perhaps you meant to have the line "script: %s.py" in your app.yaml' %
1100 handler_path)
1103 def GetScriptModuleName(handler_path):
1104 """Determines the fully-qualified Python module name of a script on disk.
1106 Args:
1107 handler_path: CGI path stored in the application configuration (as a path
1108 like 'foo/bar/baz.py'). May contain $PYTHON_LIB references.
1110 Returns:
1111 String containing the corresponding module name (e.g., 'foo.bar.baz').
1113 if handler_path.startswith(PYTHON_LIB_VAR + '/'):
1114 handler_path = handler_path[len(PYTHON_LIB_VAR):]
1115 handler_path = os.path.normpath(handler_path)
1118 extension_index = handler_path.rfind('.py')
1119 if extension_index != -1:
1120 handler_path = handler_path[:extension_index]
1121 module_fullname = handler_path.replace(os.sep, '.')
1122 module_fullname = module_fullname.strip('.')
1123 module_fullname = re.sub('\.+', '.', module_fullname)
1127 if module_fullname.endswith('.__init__'):
1128 module_fullname = module_fullname[:-len('.__init__')]
1130 return module_fullname
1133 def FindMissingInitFiles(cgi_path, module_fullname, isfile=os.path.isfile):
1134 """Determines which __init__.py files are missing from a module's parent
1135 packages.
1137 Args:
1138 cgi_path: Absolute path of the CGI module file on disk.
1139 module_fullname: Fully qualified Python module name used to import the
1140 cgi_path module.
1141 isfile: Used for testing.
1143 Returns:
1144 List containing the paths to the missing __init__.py files.
1146 missing_init_files = []
1148 if cgi_path.endswith('.py'):
1149 module_base = os.path.dirname(cgi_path)
1150 else:
1151 module_base = cgi_path
1153 depth_count = module_fullname.count('.')
1159 if cgi_path.endswith('__init__.py') or not cgi_path.endswith('.py'):
1160 depth_count += 1
1162 for index in xrange(depth_count):
1165 current_init_file = os.path.abspath(
1166 os.path.join(module_base, '__init__.py'))
1168 if not isfile(current_init_file):
1169 missing_init_files.append(current_init_file)
1171 module_base = os.path.abspath(os.path.join(module_base, os.pardir))
1173 return missing_init_files
1176 def LoadTargetModule(handler_path,
1177 cgi_path,
1178 import_hook,
1179 module_dict=sys.modules):
1180 """Loads a target CGI script by importing it as a Python module.
1182 If the module for the target CGI script has already been loaded before,
1183 the new module will be loaded in its place using the same module object,
1184 possibly overwriting existing module attributes.
1186 Args:
1187 handler_path: CGI path stored in the application configuration (as a path
1188 like 'foo/bar/baz.py'). Should not have $PYTHON_LIB references.
1189 cgi_path: Absolute path to the CGI script file on disk.
1190 import_hook: Instance of HardenedModulesHook to use for module loading.
1191 module_dict: Used for dependency injection.
1193 Returns:
1194 Tuple (module_fullname, script_module, module_code) where:
1195 module_fullname: Fully qualified module name used to import the script.
1196 script_module: The ModuleType object corresponding to the module_fullname.
1197 If the module has not already been loaded, this will be an empty
1198 shell of a module.
1199 module_code: Code object (returned by compile built-in) corresponding
1200 to the cgi_path to run. If the script_module was previously loaded
1201 and has a main() function that can be reused, this will be None.
1203 Raises:
1204 CouldNotFindModuleError if the given handler_path is a file and doesn't have
1205 the expected extension.
1207 CheckScriptExists(cgi_path, handler_path)
1208 module_fullname = GetScriptModuleName(handler_path)
1209 script_module = module_dict.get(module_fullname)
1210 module_code = None
1211 if script_module is not None and ModuleHasValidMainFunction(script_module):
1215 logging.debug('Reusing main() function of module "%s"', module_fullname)
1216 else:
1223 if script_module is None:
1224 script_module = imp.new_module(module_fullname)
1225 script_module.__loader__ = import_hook
1228 try:
1229 module_code = import_hook.get_code(module_fullname)
1230 full_path, search_path, submodule = (
1231 import_hook.GetModuleInfo(module_fullname))
1232 script_module.__file__ = full_path
1233 if search_path is not None:
1234 script_module.__path__ = search_path
1235 except UnicodeDecodeError, e:
1239 error = ('%s please see http://www.python.org/peps'
1240 '/pep-0263.html for details (%s)' % (e, handler_path))
1241 raise SyntaxError(error)
1242 except:
1243 exc_type, exc_value, exc_tb = sys.exc_info()
1244 import_error_message = str(exc_type)
1245 if exc_value:
1246 import_error_message += ': ' + str(exc_value)
1254 logging.exception('Encountered error loading module "%s": %s',
1255 module_fullname, import_error_message)
1256 missing_inits = FindMissingInitFiles(cgi_path, module_fullname)
1257 if missing_inits:
1258 logging.warning('Missing package initialization files: %s',
1259 ', '.join(missing_inits))
1260 else:
1261 logging.error('Parent package initialization files are present, '
1262 'but must be broken')
1265 independent_load_successful = True
1267 if not os.path.isfile(cgi_path):
1272 independent_load_successful = False
1273 else:
1274 try:
1275 source_file = open(cgi_path)
1276 try:
1277 module_code = compile(source_file.read(), cgi_path, 'exec')
1278 script_module.__file__ = cgi_path
1279 finally:
1280 source_file.close()
1282 except OSError:
1286 independent_load_successful = False
1289 if not independent_load_successful:
1290 raise exc_type, exc_value, exc_tb
1295 module_dict[module_fullname] = script_module
1297 return module_fullname, script_module, module_code
1300 def _WriteErrorToOutput(status, message, outfile):
1301 """Writes an error status response to the response outfile.
1303 Args:
1304 status: The status to return, e.g. '411 Length Required'.
1305 message: A human-readable error message.
1306 outfile: Response outfile.
1308 logging.error(message)
1309 outfile.write('Status: %s\r\n\r\n%s' % (status, message))
1312 def GetRequestSize(request, env_dict, outfile):
1313 """Gets the size (content length) of the given request.
1315 On error, this method writes an error message to the response outfile and
1316 returns None. Errors include the request missing a required header and the
1317 request being too large.
1319 Args:
1320 request: AppServerRequest instance.
1321 env_dict: Environment dictionary. May be None.
1322 outfile: Response outfile.
1324 Returns:
1325 The calculated request size, or None on error.
1327 if 'content-length' in request.headers:
1328 request_size = int(request.headers['content-length'])
1329 elif env_dict and env_dict.get('REQUEST_METHOD', '') == 'POST':
1330 _WriteErrorToOutput('%d Length required' % httplib.LENGTH_REQUIRED,
1331 'POST requests require a Content-length header.',
1332 outfile)
1333 return None
1334 else:
1335 request_size = 0
1337 if request_size <= MAX_REQUEST_SIZE:
1338 return request_size
1339 else:
1340 msg = ('HTTP request was too large: %d. The limit is: %d.'
1341 % (request_size, MAX_REQUEST_SIZE))
1342 _WriteErrorToOutput(
1343 '%d Request entity too large' % httplib.REQUEST_ENTITY_TOO_LARGE,
1344 msg, outfile)
1345 return None
1348 def ExecuteOrImportScript(config, handler_path, cgi_path, import_hook):
1349 """Executes a CGI script by importing it as a new module.
1351 This possibly reuses the module's main() function if it is defined and
1352 takes no arguments.
1354 Basic technique lifted from PEP 338 and Python2.5's runpy module. See:
1355 http://www.python.org/dev/peps/pep-0338/
1357 See the section entitled "Import Statements and the Main Module" to understand
1358 why a module named '__main__' cannot do relative imports. To get around this,
1359 the requested module's path could be added to sys.path on each request.
1361 Args:
1362 config: AppInfoExternal instance representing the parsed app.yaml file.
1363 handler_path: CGI path stored in the application configuration (as a path
1364 like 'foo/bar/baz.py'). Should not have $PYTHON_LIB references.
1365 cgi_path: Absolute path to the CGI script file on disk.
1366 import_hook: Instance of HardenedModulesHook to use for module loading.
1368 Returns:
1369 True if the response code had an error status (e.g., 404), or False if it
1370 did not.
1372 Raises:
1373 Any kind of exception that could have been raised when loading the target
1374 module, running a target script, or executing the application code itself.
1376 module_fullname, script_module, module_code = LoadTargetModule(
1377 handler_path, cgi_path, import_hook)
1378 script_module.__name__ = '__main__'
1379 sys.modules['__main__'] = script_module
1380 try:
1382 import pdb
1383 MonkeyPatchPdb(pdb)
1386 if module_code:
1387 exec module_code in script_module.__dict__
1388 else:
1389 script_module.main()
1395 sys.stdout.flush()
1396 sys.stdout.seek(0)
1397 try:
1398 headers = mimetools.Message(sys.stdout)
1399 finally:
1402 sys.stdout.seek(0, 2)
1403 status_header = headers.get('status')
1404 error_response = False
1405 if status_header:
1406 try:
1407 status_code = int(status_header.split(' ', 1)[0])
1408 error_response = status_code >= 400
1409 except ValueError:
1410 error_response = True
1413 if not error_response:
1414 try:
1415 parent_package = import_hook.GetParentPackage(module_fullname)
1416 except Exception:
1417 parent_package = None
1419 if parent_package is not None:
1420 submodule = GetSubmoduleName(module_fullname)
1421 setattr(parent_package, submodule, script_module)
1423 return error_response
1424 finally:
1425 script_module.__name__ = module_fullname
1428 def ExecutePy27Handler(config, handler_path, cgi_path, import_hook):
1429 """Equivalent to ExecuteOrImportScript for Python 2.7 runtime.
1431 This dispatches to google.appengine.runtime.runtime,
1432 which in turn will dispatch to either the cgi or the wsgi module in
1433 the same package, depending on the form of handler_path.
1435 Args:
1436 config: AppInfoExternal instance representing the parsed app.yaml file.
1437 handler_path: handler ("script") from the application configuration;
1438 either a script reference like foo/bar.py, or an object reference
1439 like foo.bar.app.
1440 cgi_path: Absolute path to the CGI script file on disk;
1441 typically the app dir joined with handler_path.
1442 import_hook: Instance of HardenedModulesHook to use for module loading.
1444 Returns:
1445 True if the response code had an error status (e.g., 404), or False if it
1446 did not.
1448 Raises:
1449 Any kind of exception that could have been raised when loading the target
1450 module, running a target script, or executing the application code itself.
1452 if request_environment is None or runtime is None:
1453 raise RuntimeError('Python 2.5 is too old to emulate the Python 2.7 runtime.'
1454 ' Please use Python 2.6 or Python 2.7.')
1457 import os
1459 save_environ = os.environ
1460 save_getenv = os.getenv
1462 env = dict(save_environ)
1465 if env.get('_AH_THREADSAFE'):
1466 env['wsgi.multithread'] = True
1468 url = 'http://%s%s' % (env.get('HTTP_HOST', 'localhost:8080'),
1469 env.get('_AH_ENCODED_SCRIPT_NAME', '/'))
1470 qs = env.get('QUERY_STRING')
1471 if qs:
1472 url += '?' + qs
1475 post_data = sys.stdin.read()
1484 if 'CONTENT_TYPE' in env:
1485 if post_data:
1486 env['HTTP_CONTENT_TYPE'] = env['CONTENT_TYPE']
1487 del env['CONTENT_TYPE']
1488 if 'CONTENT_LENGTH' in env:
1489 if env['CONTENT_LENGTH']:
1490 env['HTTP_CONTENT_LENGTH'] = env['CONTENT_LENGTH']
1491 del env['CONTENT_LENGTH']
1493 if cgi_path.endswith(handler_path):
1494 application_root = cgi_path[:-len(handler_path)]
1495 if application_root.endswith('/') and application_root != '/':
1496 application_root = application_root[:-1]
1497 else:
1498 application_root = ''
1501 try:
1503 import pdb
1504 MonkeyPatchPdb(pdb)
1506 import _threading_local
1507 MonkeyPatchThreadingLocal(_threading_local)
1511 os.environ = request_environment.RequestLocalEnviron(
1512 request_environment.current_request)
1516 os.getenv = os.environ.get
1518 response = runtime.HandleRequest(env, handler_path, url,
1519 post_data, application_root, SDK_ROOT,
1520 import_hook)
1521 finally:
1523 os.environ = save_environ
1524 os.getenv = save_getenv
1528 error = response.get('error')
1529 if error:
1530 status = 500
1531 else:
1532 status = 200
1533 status = response.get('response_code', status)
1534 sys.stdout.write('Status: %s\r\n' % status)
1535 for key, value in response.get('headers', ()):
1538 key = '-'.join(key.split())
1539 value = value.replace('\r', ' ').replace('\n', ' ')
1540 sys.stdout.write('%s: %s\r\n' % (key, value))
1541 sys.stdout.write('\r\n')
1542 body = response.get('body')
1543 if body:
1544 sys.stdout.write(body)
1545 logs = response.get('logs')
1546 if logs:
1547 for timestamp_usec, severity, message in logs:
1549 logging.log(severity*10 + 10, '@%s: %s',
1550 time.ctime(timestamp_usec*1e-6), message)
1551 return error
1554 class LoggingStream(object):
1555 """A stream that writes logs at level error."""
1557 def write(self, message):
1560 logging.getLogger()._log(logging.ERROR, message, ())
1562 def writelines(self, lines):
1563 for line in lines:
1564 logging.getLogger()._log(logging.ERROR, line, ())
1566 def __getattr__(self, key):
1567 return getattr(sys.__stderr__, key)
1570 def ExecuteCGI(config,
1571 root_path,
1572 handler_path,
1573 cgi_path,
1574 env,
1575 infile,
1576 outfile,
1577 module_dict,
1578 exec_script=ExecuteOrImportScript,
1579 exec_py27_handler=ExecutePy27Handler):
1580 """Executes Python file in this process as if it were a CGI.
1582 Does not return an HTTP response line. CGIs should output headers followed by
1583 the body content.
1585 The modules in sys.modules should be the same before and after the CGI is
1586 executed, with the specific exception of encodings-related modules, which
1587 cannot be reloaded and thus must always stay in sys.modules.
1589 Args:
1590 config: AppInfoExternal instance representing the parsed app.yaml file.
1591 root_path: Path to the root of the application.
1592 handler_path: CGI path stored in the application configuration (as a path
1593 like 'foo/bar/baz.py'). May contain $PYTHON_LIB references.
1594 cgi_path: Absolute path to the CGI script file on disk.
1595 env: Dictionary of environment variables to use for the execution.
1596 infile: File-like object to read HTTP request input data from.
1597 outfile: FIle-like object to write HTTP response data to.
1598 module_dict: Dictionary in which application-loaded modules should be
1599 preserved between requests. This removes the need to reload modules that
1600 are reused between requests, significantly increasing load performance.
1601 This dictionary must be separate from the sys.modules dictionary.
1602 exec_script: Used for dependency injection.
1603 exec_py27_handler: Used for dependency injection.
1606 old_module_dict = sys.modules.copy()
1607 old_builtin = __builtin__.__dict__.copy()
1608 old_argv = sys.argv
1609 old_stdin = sys.stdin
1610 old_stdout = sys.stdout
1611 old_stderr = sys.stderr
1612 old_env = os.environ.copy()
1613 old_cwd = os.getcwd()
1614 old_file_type = types.FileType
1615 reset_modules = False
1616 app_log_handler = None
1618 try:
1619 ConnectAndDisconnectChildModules(sys.modules, module_dict)
1620 ClearAllButEncodingsModules(sys.modules)
1621 sys.modules.update(module_dict)
1622 sys.argv = [cgi_path]
1624 sys.stdin = cStringIO.StringIO(infile.getvalue())
1625 sys.stdout = outfile
1629 sys.stderr = LoggingStream()
1631 logservice._global_buffer = logservice.LogsBuffer()
1633 app_log_handler = app_logging.AppLogsHandler()
1634 logging.getLogger().addHandler(app_log_handler)
1636 os.environ.clear()
1637 os.environ.update(env)
1641 cgi_dir = os.path.normpath(os.path.dirname(cgi_path))
1642 root_path = os.path.normpath(os.path.abspath(root_path))
1643 if (cgi_dir.startswith(root_path + os.sep) and
1644 not (config and config.runtime == 'python27')):
1645 os.chdir(cgi_dir)
1646 else:
1647 os.chdir(root_path)
1649 dist.fix_paths(root_path, SDK_ROOT)
1654 hook = HardenedModulesHook(config, sys.modules, root_path)
1655 sys.meta_path = [finder for finder in sys.meta_path
1656 if not isinstance(finder, HardenedModulesHook)]
1657 sys.meta_path.insert(0, hook)
1658 if hasattr(sys, 'path_importer_cache'):
1659 sys.path_importer_cache.clear()
1662 __builtin__.file = FakeFile
1663 __builtin__.open = FakeFile
1664 types.FileType = FakeFile
1666 if not (config and config.runtime == 'python27'):
1668 __builtin__.buffer = NotImplementedFakeClass
1675 sys.modules['__builtin__'] = __builtin__
1677 logging.debug('Executing CGI with env:\n%s', repr(env))
1678 try:
1681 if handler_path and config and config.runtime == 'python27':
1682 reset_modules = exec_py27_handler(config, handler_path, cgi_path, hook)
1683 else:
1684 reset_modules = exec_script(config, handler_path, cgi_path, hook)
1685 except SystemExit, e:
1686 logging.debug('CGI exited with status: %s', e)
1687 except:
1688 reset_modules = True
1689 raise
1691 finally:
1692 sys.path_importer_cache.clear()
1694 _ClearTemplateCache(sys.modules)
1698 module_dict.update(sys.modules)
1699 ConnectAndDisconnectChildModules(sys.modules, old_module_dict)
1700 ClearAllButEncodingsModules(sys.modules)
1701 sys.modules.update(old_module_dict)
1703 __builtin__.__dict__.update(old_builtin)
1704 sys.argv = old_argv
1705 sys.stdin = old_stdin
1706 sys.stdout = old_stdout
1708 sys.stderr = old_stderr
1709 logging.getLogger().removeHandler(app_log_handler)
1711 os.environ.clear()
1712 os.environ.update(old_env)
1713 os.chdir(old_cwd)
1716 types.FileType = old_file_type
1719 class CGIDispatcher(URLDispatcher):
1720 """Dispatcher that executes Python CGI scripts."""
1722 def __init__(self,
1723 config,
1724 module_dict,
1725 root_path,
1726 path_adjuster,
1727 setup_env=SetupEnvironment,
1728 exec_cgi=ExecuteCGI):
1729 """Initializer.
1731 Args:
1732 config: AppInfoExternal instance representing the parsed app.yaml file.
1733 module_dict: Dictionary in which application-loaded modules should be
1734 preserved between requests. This dictionary must be separate from the
1735 sys.modules dictionary.
1736 path_adjuster: Instance of PathAdjuster to use for finding absolute
1737 paths of CGI files on disk.
1738 setup_env, exec_cgi: Used for dependency injection.
1740 self._config = config
1741 self._module_dict = module_dict
1742 self._root_path = root_path
1743 self._path_adjuster = path_adjuster
1744 self._setup_env = setup_env
1745 self._exec_cgi = exec_cgi
1747 def Dispatch(self,
1748 request,
1749 outfile,
1750 base_env_dict=None):
1751 """Dispatches the Python CGI."""
1752 request_size = GetRequestSize(request, base_env_dict, outfile)
1753 if request_size is None:
1754 return
1757 memory_file = cStringIO.StringIO()
1758 CopyStreamPart(request.infile, memory_file, request_size)
1759 memory_file.seek(0)
1761 before_level = logging.root.level
1762 try:
1763 env = {}
1766 if self._config.env_variables:
1767 env.update(self._config.env_variables)
1768 if base_env_dict:
1769 env.update(base_env_dict)
1770 cgi_path = self._path_adjuster.AdjustPath(request.path)
1771 env.update(self._setup_env(cgi_path,
1772 request.relative_url,
1773 request.headers,
1774 memory_file))
1775 self._exec_cgi(self._config,
1776 self._root_path,
1777 request.path,
1778 cgi_path,
1779 env,
1780 memory_file,
1781 outfile,
1782 self._module_dict)
1783 finally:
1784 logging.root.level = before_level
1786 def __str__(self):
1787 """Returns a string representation of this dispatcher."""
1788 return 'CGI dispatcher'
1791 class LocalCGIDispatcher(CGIDispatcher):
1792 """Dispatcher that executes local functions like they're CGIs.
1794 The contents of sys.modules will be preserved for local CGIs running this
1795 dispatcher, but module hardening will still occur for any new imports. Thus,
1796 be sure that any local CGIs have loaded all of their dependent modules
1797 _before_ they are executed.
1800 def __init__(self, config, module_dict, path_adjuster, cgi_func):
1801 """Initializer.
1803 Args:
1804 config: AppInfoExternal instance representing the parsed app.yaml file.
1805 module_dict: Passed to CGIDispatcher.
1806 path_adjuster: Passed to CGIDispatcher.
1807 cgi_func: Callable function taking no parameters that should be
1808 executed in a CGI environment in the current process.
1810 self._cgi_func = cgi_func
1812 def curried_exec_script(*args, **kwargs):
1813 cgi_func()
1814 return False
1816 def curried_exec_cgi(*args, **kwargs):
1817 kwargs['exec_script'] = curried_exec_script
1818 return ExecuteCGI(*args, **kwargs)
1820 CGIDispatcher.__init__(self,
1821 config,
1822 module_dict,
1824 path_adjuster,
1825 exec_cgi=curried_exec_cgi)
1827 def Dispatch(self, *args, **kwargs):
1828 """Preserves sys.modules for CGIDispatcher.Dispatch."""
1829 self._module_dict.update(sys.modules)
1830 CGIDispatcher.Dispatch(self, *args, **kwargs)
1832 def __str__(self):
1833 """Returns a string representation of this dispatcher."""
1834 return 'Local CGI dispatcher for %s' % self._cgi_func
1839 class PathAdjuster(object):
1840 """Adjusts application file paths to paths relative to the application or
1841 external library directories."""
1843 def __init__(self, root_path):
1844 """Initializer.
1846 Args:
1847 root_path: Path to the root of the application running on the server.
1849 self._root_path = os.path.abspath(root_path)
1851 def AdjustPath(self, path):
1852 """Adjusts application file paths to relative to the application.
1854 More precisely this method adjusts application file path to paths
1855 relative to the application or external library directories.
1857 Handler paths that start with $PYTHON_LIB will be converted to paths
1858 relative to the google directory.
1860 Args:
1861 path: File path that should be adjusted.
1863 Returns:
1864 The adjusted path.
1866 if path.startswith(PYTHON_LIB_VAR):
1867 path = os.path.join(SDK_ROOT, path[len(PYTHON_LIB_VAR) + 1:])
1868 else:
1869 path = os.path.join(self._root_path, path)
1871 return path
1876 class StaticFileConfigMatcher(object):
1877 """Keeps track of file/directory specific application configuration.
1879 Specifically:
1880 - Computes mime type based on URLMap and file extension.
1881 - Decides on cache expiration time based on URLMap and default expiration.
1882 - Decides what HTTP headers to add to responses.
1884 To determine the mime type, we first see if there is any mime-type property
1885 on each URLMap entry. If non is specified, we use the mimetypes module to
1886 guess the mime type from the file path extension, and use
1887 application/octet-stream if we can't find the mimetype.
1890 def __init__(self,
1891 url_map_list,
1892 default_expiration):
1893 """Initializer.
1895 Args:
1896 url_map_list: List of appinfo.URLMap objects.
1897 If empty or None, then we always use the mime type chosen by the
1898 mimetypes module.
1899 default_expiration: String describing default expiration time for browser
1900 based caching of static files. If set to None this disallows any
1901 browser caching of static content.
1903 if default_expiration is not None:
1904 self._default_expiration = appinfo.ParseExpiration(default_expiration)
1905 else:
1906 self._default_expiration = None
1909 self._patterns = []
1910 for url_map in url_map_list or []:
1912 handler_type = url_map.GetHandlerType()
1913 if handler_type not in (appinfo.STATIC_FILES, appinfo.STATIC_DIR):
1914 continue
1916 path_re = _StaticFilePathRe(url_map)
1917 try:
1918 self._patterns.append((re.compile(path_re), url_map))
1919 except re.error, e:
1920 raise InvalidAppConfigError('regex %s does not compile: %s' %
1921 (path_re, e))
1923 _DUMMY_URLMAP = appinfo.URLMap()
1925 def _FirstMatch(self, path):
1926 """Returns the first appinfo.URLMap that matches path, or a dummy instance.
1928 A dummy instance is returned when no appinfo.URLMap matches path (see the
1929 URLMap.static_file_path_re property). When a dummy instance is returned, it
1930 is always the same one. The dummy instance is constructed simply by doing
1931 the following:
1933 appinfo.URLMap()
1935 Args:
1936 path: A string containing the file's path relative to the app.
1938 Returns:
1939 The first appinfo.URLMap (in the list that was passed to the constructor)
1940 that matches path. Matching depends on whether URLMap is a static_dir
1941 handler or a static_files handler. In either case, matching is done
1942 according to the URLMap.static_file_path_re property.
1944 for path_re, url_map in self._patterns:
1945 if path_re.match(path):
1946 return url_map
1947 return StaticFileConfigMatcher._DUMMY_URLMAP
1949 def IsStaticFile(self, path):
1950 """Tests if the given path points to a "static" file.
1952 Args:
1953 path: A string containing the file's path relative to the app.
1955 Returns:
1956 Boolean, True if the file was configured to be static.
1958 return self._FirstMatch(path) is not self._DUMMY_URLMAP
1960 def GetMimeType(self, path):
1961 """Returns the mime type that we should use when serving the specified file.
1963 Args:
1964 path: A string containing the file's path relative to the app.
1966 Returns:
1967 String containing the mime type to use. Will be 'application/octet-stream'
1968 if we have no idea what it should be.
1970 url_map = self._FirstMatch(path)
1971 if url_map.mime_type is not None:
1972 return url_map.mime_type
1975 unused_filename, extension = os.path.splitext(path)
1976 return mimetypes.types_map.get(extension, 'application/octet-stream')
1978 def GetExpiration(self, path):
1979 """Returns the cache expiration duration to be users for the given file.
1981 Args:
1982 path: A string containing the file's path relative to the app.
1984 Returns:
1985 Integer number of seconds to be used for browser cache expiration time.
1988 if self._default_expiration is None:
1989 return 0
1991 url_map = self._FirstMatch(path)
1992 if url_map.expiration is None:
1993 return self._default_expiration
1995 return appinfo.ParseExpiration(url_map.expiration)
1997 def GetHttpHeaders(self, path):
1998 """Returns http_headers of the matching appinfo.URLMap, or an empty one.
2000 Args:
2001 path: A string containing the file's path relative to the app.
2003 Returns:
2004 A user-specified HTTP headers to be used in static content response. These
2005 headers are contained in an appinfo.HttpHeadersDict, which maps header
2006 names to values (both strings).
2008 return self._FirstMatch(path).http_headers or appinfo.HttpHeadersDict()
2014 def ReadDataFile(data_path, openfile=file):
2015 """Reads a file on disk, returning a corresponding HTTP status and data.
2017 Args:
2018 data_path: Path to the file on disk to read.
2019 openfile: Used for dependency injection.
2021 Returns:
2022 Tuple (status, data) where status is an HTTP response code, and data is
2023 the data read; will be an empty string if an error occurred or the
2024 file was empty.
2026 status = httplib.INTERNAL_SERVER_ERROR
2027 data = ""
2029 try:
2030 data_file = openfile(data_path, 'rb')
2031 try:
2032 data = data_file.read()
2033 finally:
2034 data_file.close()
2035 status = httplib.OK
2036 except (OSError, IOError), e:
2037 logging.error('Error encountered reading file "%s":\n%s', data_path, e)
2038 if e.errno in FILE_MISSING_EXCEPTIONS:
2039 status = httplib.NOT_FOUND
2040 else:
2041 status = httplib.FORBIDDEN
2043 return status, data
2046 class FileDispatcher(URLDispatcher):
2047 """Dispatcher that reads data files from disk."""
2049 def __init__(self,
2050 config,
2051 path_adjuster,
2052 static_file_config_matcher,
2053 read_data_file=ReadDataFile):
2054 """Initializer.
2056 Args:
2057 config: AppInfoExternal instance representing the parsed app.yaml file.
2058 path_adjuster: Instance of PathAdjuster to use for finding absolute
2059 paths of data files on disk.
2060 static_file_config_matcher: StaticFileConfigMatcher object.
2061 read_data_file: Used for dependency injection.
2063 self._config = config
2064 self._path_adjuster = path_adjuster
2065 self._static_file_config_matcher = static_file_config_matcher
2066 self._read_data_file = read_data_file
2068 def Dispatch(self, request, outfile, base_env_dict=None):
2069 """Reads the file and returns the response status and data."""
2070 full_path = self._path_adjuster.AdjustPath(request.path)
2071 status, data = self._read_data_file(full_path)
2072 content_type = self._static_file_config_matcher.GetMimeType(request.path)
2073 static_file = self._static_file_config_matcher.IsStaticFile(request.path)
2074 expiration = self._static_file_config_matcher.GetExpiration(request.path)
2075 current_etag = self.CreateEtag(data)
2076 if_match_etag = request.headers.get('if-match', None)
2077 if_none_match_etag = request.headers.get('if-none-match', '').split(',')
2079 http_headers = self._static_file_config_matcher.GetHttpHeaders(request.path)
2080 def WriteHeader(name, value):
2081 if http_headers.Get(name) is None:
2082 outfile.write('%s: %s\r\n' % (name, value))
2088 if if_match_etag and not self._CheckETagMatches(if_match_etag.split(','),
2089 current_etag,
2090 False):
2091 outfile.write('Status: %s\r\n' % httplib.PRECONDITION_FAILED)
2092 WriteHeader('ETag', current_etag)
2093 outfile.write('\r\n')
2094 elif self._CheckETagMatches(if_none_match_etag, current_etag, True):
2095 outfile.write('Status: %s\r\n' % httplib.NOT_MODIFIED)
2096 WriteHeader('ETag', current_etag)
2097 outfile.write('\r\n')
2098 else:
2102 outfile.write('Status: %d\r\n' % status)
2104 WriteHeader('Content-Type', content_type)
2107 if expiration:
2108 fmt = email.Utils.formatdate
2109 WriteHeader('Expires', fmt(time.time() + expiration, usegmt=True))
2110 WriteHeader('Cache-Control', 'public, max-age=%i' % expiration)
2113 if static_file:
2114 WriteHeader('ETag', '"%s"' % current_etag)
2116 for header in http_headers.iteritems():
2117 outfile.write('%s: %s\r\n' % header)
2119 outfile.write('\r\n')
2120 outfile.write(data)
2122 def __str__(self):
2123 """Returns a string representation of this dispatcher."""
2124 return 'File dispatcher'
2126 @staticmethod
2127 def CreateEtag(data):
2128 """Returns string of hash of file content, unique per URL."""
2129 data_crc = zlib.crc32(data)
2130 return base64.b64encode(str(data_crc))
2132 @staticmethod
2133 def _CheckETagMatches(supplied_etags, current_etag, allow_weak_match):
2134 """Checks if there is an entity tag match.
2136 Args:
2137 supplied_etags: list of input etags
2138 current_etag: the calculated etag for the entity
2139 allow_weak_match: Allow for weak tag comparison.
2141 Returns:
2142 True if there is a match, False otherwise.
2145 for tag in supplied_etags:
2146 if allow_weak_match and tag.startswith('W/'):
2147 tag = tag[2:]
2148 tag_data = tag.strip('"')
2149 if tag_data == '*' or tag_data == current_etag:
2150 return True
2151 return False
2160 _IGNORE_RESPONSE_HEADERS = frozenset([
2161 'connection',
2162 'content-encoding',
2163 'date',
2164 'keep-alive',
2165 'proxy-authenticate',
2166 'server',
2167 'trailer',
2168 'transfer-encoding',
2169 'upgrade',
2170 blobstore.BLOB_KEY_HEADER
2174 class AppServerResponse(object):
2175 """Development appserver response object.
2177 Object used to hold the full appserver response. Used as a container
2178 that is passed through the request rewrite chain and ultimately sent
2179 to the web client.
2181 Attributes:
2182 status_code: Integer HTTP response status (e.g., 200, 302, 404, 500)
2183 status_message: String containing an informational message about the
2184 response code, possibly derived from the 'status' header, if supplied.
2185 headers: mimetools.Message containing the HTTP headers of the response.
2186 body: File-like object containing the body of the response.
2187 large_response: Indicates that response is permitted to be larger than
2188 MAX_RUNTIME_RESPONSE_SIZE.
2192 __slots__ = ['status_code',
2193 'status_message',
2194 'headers',
2195 'body',
2196 'large_response']
2198 def __init__(self, response_file=None, **kwds):
2199 """Initializer.
2201 Args:
2202 response_file: A file-like object that contains the full response
2203 generated by the user application request handler. If present
2204 the headers and body are set from this value, although the values
2205 may be further overridden by the keyword parameters.
2206 kwds: All keywords are mapped to attributes of AppServerResponse.
2208 self.status_code = 200
2209 self.status_message = 'Good to go'
2210 self.large_response = False
2212 if response_file:
2213 self.SetResponse(response_file)
2214 else:
2215 self.headers = mimetools.Message(cStringIO.StringIO())
2216 self.body = None
2218 for name, value in kwds.iteritems():
2219 setattr(self, name, value)
2221 def SetResponse(self, response_file):
2222 """Sets headers and body from the response file.
2224 Args:
2225 response_file: File like object to set body and headers from.
2227 self.headers = mimetools.Message(response_file)
2228 self.body = response_file
2230 @property
2231 def header_data(self):
2232 """Get header data as a string.
2234 Returns:
2235 String representation of header with line breaks cleaned up.
2238 header_list = []
2239 for header in self.headers.headers:
2240 header = header.rstrip('\n\r')
2241 header_list.append(header)
2242 if not self.headers.getheader('Content-Type'):
2244 header_list.append('Content-Type: text/html')
2246 return '\r\n'.join(header_list) + '\r\n'
2249 def IgnoreHeadersRewriter(response):
2250 """Ignore specific response headers.
2252 Certain response headers cannot be modified by an Application. For a
2253 complete list of these headers please see:
2255 https://developers.google.com/appengine/docs/python/tools/webapp/responseclass#Disallowed_HTTP_Response_Headers
2257 This rewriter simply removes those headers.
2259 for h in _IGNORE_RESPONSE_HEADERS:
2260 if h in response.headers:
2261 del response.headers[h]
2264 def ValidHeadersRewriter(response):
2265 """Remove invalid response headers.
2267 Response headers must be printable ascii characters. This is enforced in
2268 production by http_proto.cc IsValidHeader.
2270 This rewriter will remove headers that contain non ascii characters.
2272 for (key, value) in response.headers.items():
2273 try:
2274 key.decode('ascii')
2275 value.decode('ascii')
2276 except UnicodeDecodeError:
2277 del response.headers[key]
2280 def ParseStatusRewriter(response):
2281 """Parse status header, if it exists.
2283 Handles the server-side 'status' header, which instructs the server to change
2284 the HTTP response code accordingly. Handles the 'location' header, which
2285 issues an HTTP 302 redirect to the client. Also corrects the 'content-length'
2286 header to reflect actual content length in case extra information has been
2287 appended to the response body.
2289 If the 'status' header supplied by the client is invalid, this method will
2290 set the response to a 500 with an error message as content.
2292 location_value = response.headers.getheader('location')
2293 status_value = response.headers.getheader('status')
2294 if status_value:
2295 response_status = status_value
2296 del response.headers['status']
2297 elif location_value:
2298 response_status = '%d Redirecting' % httplib.FOUND
2299 else:
2300 return response
2302 status_parts = response_status.split(' ', 1)
2303 response.status_code, response.status_message = (status_parts + [''])[:2]
2304 try:
2305 response.status_code = int(response.status_code)
2306 except ValueError:
2307 response.status_code = 500
2308 response.body = cStringIO.StringIO(
2309 'Error: Invalid "status" header value returned.')
2312 def GetAllHeaders(message, name):
2313 """Get all headers of a given name in a message.
2315 Args:
2316 message: A mimetools.Message object.
2317 name: The name of the header.
2319 Yields:
2320 A sequence of values of all headers with the given name.
2322 for header_line in message.getallmatchingheaders(name):
2323 yield header_line.split(':', 1)[1].strip()
2326 def CacheRewriter(response):
2327 """Update the cache header."""
2330 if response.status_code == httplib.NOT_MODIFIED:
2331 return
2333 if not 'Cache-Control' in response.headers:
2334 response.headers['Cache-Control'] = 'no-cache'
2335 if not 'Expires' in response.headers:
2336 response.headers['Expires'] = 'Fri, 01 Jan 1990 00:00:00 GMT'
2339 if 'Set-Cookie' in response.headers:
2343 current_date = time.time()
2344 expires = response.headers.get('Expires')
2345 reset_expires = True
2346 if expires:
2347 expires_time = email.Utils.parsedate(expires)
2348 if expires_time:
2349 reset_expires = calendar.timegm(expires_time) >= current_date
2350 if reset_expires:
2351 response.headers['Expires'] = time.strftime('%a, %d %b %Y %H:%M:%S GMT',
2352 time.gmtime(current_date))
2356 cache_directives = []
2357 for header in GetAllHeaders(response.headers, 'Cache-Control'):
2358 cache_directives.extend(v.strip() for v in header.split(','))
2359 cache_directives = [d for d in cache_directives if d != 'public']
2360 if not NON_PUBLIC_CACHE_CONTROLS.intersection(cache_directives):
2361 cache_directives.append('private')
2362 response.headers['Cache-Control'] = ', '.join(cache_directives)
2365 def _RemainingDataSize(input_buffer):
2366 """Computes how much data is remaining in the buffer.
2368 It leaves the buffer in its initial state.
2370 Args:
2371 input_buffer: a file-like object with seek and tell methods.
2373 Returns:
2374 integer representing how much data is remaining in the buffer.
2376 current_position = input_buffer.tell()
2377 input_buffer.seek(0, 2)
2378 remaining_data_size = input_buffer.tell() - current_position
2379 input_buffer.seek(current_position)
2380 return remaining_data_size
2383 def ContentLengthRewriter(response, request_headers, env_dict):
2384 """Rewrite the Content-Length header.
2386 Even though Content-Length is not a user modifiable header, App Engine
2387 sends a correct Content-Length to the user based on the actual response.
2390 if env_dict and env_dict.get('REQUEST_METHOD', '') == 'HEAD':
2391 return
2394 if response.status_code != httplib.NOT_MODIFIED:
2397 response.headers['Content-Length'] = str(_RemainingDataSize(response.body))
2398 elif 'Content-Length' in response.headers:
2399 del response.headers['Content-Length']
2402 def CreateResponseRewritersChain():
2403 """Create the default response rewriter chain.
2405 A response rewriter is the a function that gets a final chance to change part
2406 of the dev_appservers response. A rewriter is not like a dispatcher in that
2407 it is called after every request has been handled by the dispatchers
2408 regardless of which dispatcher was used.
2410 The order in which rewriters are registered will be the order in which they
2411 are used to rewrite the response. Modifications from earlier rewriters
2412 are used as input to later rewriters.
2414 A response rewriter is a function that can rewrite the request in any way.
2415 Thefunction can returned modified values or the original values it was
2416 passed.
2418 A rewriter function has the following parameters and return values:
2420 Args:
2421 status_code: Status code of response from dev_appserver or previous
2422 rewriter.
2423 status_message: Text corresponding to status code.
2424 headers: mimetools.Message instance with parsed headers. NOTE: These
2425 headers can contain its own 'status' field, but the default
2426 dev_appserver implementation will remove this. Future rewriters
2427 should avoid re-introducing the status field and return new codes
2428 instead.
2429 body: File object containing the body of the response. This position of
2430 this file may not be at the start of the file. Any content before the
2431 files position is considered not to be part of the final body.
2433 Returns:
2434 An AppServerResponse instance.
2436 Returns:
2437 List of response rewriters.
2439 rewriters = [ParseStatusRewriter,
2440 dev_appserver_blobstore.DownloadRewriter,
2441 IgnoreHeadersRewriter,
2442 ValidHeadersRewriter,
2443 CacheRewriter,
2444 ContentLengthRewriter,
2446 return rewriters
2450 def RewriteResponse(response_file,
2451 response_rewriters=None,
2452 request_headers=None,
2453 env_dict=None):
2454 """Allows final rewrite of dev_appserver response.
2456 This function receives the unparsed HTTP response from the application
2457 or internal handler, parses out the basic structure and feeds that structure
2458 in to a chain of response rewriters.
2460 It also makes sure the final HTTP headers are properly terminated.
2462 For more about response rewriters, please see documentation for
2463 CreateResponeRewritersChain.
2465 Args:
2466 response_file: File-like object containing the full HTTP response including
2467 the response code, all headers, and the request body.
2468 response_rewriters: A list of response rewriters. If none is provided it
2469 will create a new chain using CreateResponseRewritersChain.
2470 request_headers: Original request headers.
2471 env_dict: Environment dictionary.
2473 Returns:
2474 An AppServerResponse instance configured with the rewritten response.
2476 if response_rewriters is None:
2477 response_rewriters = CreateResponseRewritersChain()
2479 response = AppServerResponse(response_file)
2480 for response_rewriter in response_rewriters:
2483 if response_rewriter.func_code.co_argcount == 1:
2484 response_rewriter(response)
2485 elif response_rewriter.func_code.co_argcount == 2:
2486 response_rewriter(response, request_headers)
2487 else:
2488 response_rewriter(response, request_headers, env_dict)
2490 return response
2495 class ModuleManager(object):
2496 """Manages loaded modules in the runtime.
2498 Responsible for monitoring and reporting about file modification times.
2499 Modules can be loaded from source or precompiled byte-code files. When a
2500 file has source code, the ModuleManager monitors the modification time of
2501 the source file even if the module itself is loaded from byte-code.
2504 def __init__(self, modules):
2505 """Initializer.
2507 Args:
2508 modules: Dictionary containing monitored modules.
2510 self._modules = modules
2512 self._default_modules = self._modules.copy()
2514 self._save_path_hooks = sys.path_hooks[:]
2523 self._modification_times = {}
2526 self._dirty = True
2528 @staticmethod
2529 def GetModuleFile(module, is_file=os.path.isfile):
2530 """Helper method to try to determine modules source file.
2532 Args:
2533 module: Module object to get file for.
2534 is_file: Function used to determine if a given path is a file.
2536 Returns:
2537 Path of the module's corresponding Python source file if it exists, or
2538 just the module's compiled Python file. If the module has an invalid
2539 __file__ attribute, None will be returned.
2541 module_file = getattr(module, '__file__', None)
2542 if module_file is None:
2543 return None
2546 source_file = module_file[:module_file.rfind('py') + 2]
2548 if is_file(source_file):
2549 return source_file
2550 return module.__file__
2552 def AreModuleFilesModified(self):
2553 """Determines if any monitored files have been modified.
2555 Returns:
2556 True if one or more files have been modified, False otherwise.
2558 self._dirty = True
2559 for name, (mtime, fname) in self._modification_times.iteritems():
2561 if name not in self._modules:
2562 continue
2564 module = self._modules[name]
2566 try:
2568 if mtime != os.path.getmtime(fname):
2569 self._dirty = True
2570 return True
2571 except OSError, e:
2573 if e.errno in FILE_MISSING_EXCEPTIONS:
2574 self._dirty = True
2575 return True
2576 raise e
2578 return False
2580 def UpdateModuleFileModificationTimes(self):
2581 """Records the current modification times of all monitored modules."""
2582 if not self._dirty:
2583 return
2585 self._modification_times.clear()
2586 for name, module in self._modules.items():
2587 if not isinstance(module, types.ModuleType):
2588 continue
2589 module_file = self.GetModuleFile(module)
2590 if not module_file:
2591 continue
2592 try:
2593 self._modification_times[name] = (os.path.getmtime(module_file),
2594 module_file)
2595 except OSError, e:
2596 if e.errno not in FILE_MISSING_EXCEPTIONS:
2597 raise e
2599 self._dirty = False
2601 def ResetModules(self):
2602 """Clear modules so that when request is run they are reloaded."""
2603 lib_config._default_registry.reset()
2604 self._modules.clear()
2605 self._modules.update(self._default_modules)
2608 sys.path_hooks[:] = self._save_path_hooks
2611 sys.meta_path = []
2617 apiproxy_stub_map.apiproxy.GetPreCallHooks().Clear()
2618 apiproxy_stub_map.apiproxy.GetPostCallHooks().Clear()
2624 def GetVersionObject(isfile=os.path.isfile, open_fn=open):
2625 """Gets the version of the SDK by parsing the VERSION file.
2627 Args:
2628 isfile: used for testing.
2629 open_fn: Used for testing.
2631 Returns:
2632 A Yaml object or None if the VERSION file does not exist.
2634 version_filename = os.path.join(os.path.dirname(google.appengine.__file__),
2635 VERSION_FILE)
2636 if not isfile(version_filename):
2637 logging.error('Could not find version file at %s', version_filename)
2638 return None
2640 version_fh = open_fn(version_filename, 'r')
2641 try:
2642 version = yaml.safe_load(version_fh)
2643 finally:
2644 version_fh.close()
2646 return version
2651 def _ClearTemplateCache(module_dict=sys.modules):
2652 """Clear template cache in webapp.template module.
2654 Attempts to load template module. Ignores failure. If module loads, the
2655 template cache is cleared.
2657 Args:
2658 module_dict: Used for dependency injection.
2660 template_module = module_dict.get('google.appengine.ext.webapp.template')
2661 if template_module is not None:
2662 template_module.template_cache.clear()
2667 def CreateRequestHandler(root_path,
2668 login_url,
2669 static_caching=True,
2670 default_partition=None,
2671 interactive_console=True):
2672 """Creates a new BaseHTTPRequestHandler sub-class.
2674 This class will be used with the Python BaseHTTPServer module's HTTP server.
2676 Python's built-in HTTP server does not support passing context information
2677 along to instances of its request handlers. This function gets around that
2678 by creating a sub-class of the handler in a closure that has access to
2679 this context information.
2681 Args:
2682 root_path: Path to the root of the application running on the server.
2683 login_url: Relative URL which should be used for handling user logins.
2684 static_caching: True if browser caching of static files should be allowed.
2685 default_partition: Default partition to use in the application id.
2686 interactive_console: Whether to add the interactive console.
2688 Returns:
2689 Sub-class of BaseHTTPRequestHandler.
2711 application_module_dict = SetupSharedModules(sys.modules)
2714 application_config_cache = AppConfigCache()
2716 class DevAppServerRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
2717 """Dispatches URLs using patterns from a URLMatcher.
2719 The URLMatcher is created by loading an application's configuration file.
2720 Executes CGI scripts in the local process so the scripts can use mock
2721 versions of APIs.
2723 HTTP requests that correctly specify a user info cookie
2724 (dev_appserver_login.COOKIE_NAME) will have the 'USER_EMAIL' environment
2725 variable set accordingly. If the user is also an admin, the
2726 'USER_IS_ADMIN' variable will exist and be set to '1'. If the user is not
2727 logged in, 'USER_EMAIL' will be set to the empty string.
2729 On each request, raises an InvalidAppConfigError exception if the
2730 application configuration file in the directory specified by the root_path
2731 argument is invalid.
2733 server_version = 'Development/1.0'
2738 module_dict = application_module_dict
2739 module_manager = ModuleManager(application_module_dict)
2742 config_cache = application_config_cache
2744 rewriter_chain = CreateResponseRewritersChain()
2746 channel_poll_path_re = re.compile(
2747 dev_appserver_channel.CHANNEL_POLL_PATTERN)
2749 def __init__(self, *args, **kwargs):
2750 """Initializer.
2752 Args:
2753 args: Positional arguments passed to the superclass constructor.
2754 kwargs: Keyword arguments passed to the superclass constructor.
2756 self._log_record_writer = apiproxy_stub_map.apiproxy.GetStub('logservice')
2757 BaseHTTPServer.BaseHTTPRequestHandler.__init__(self, *args, **kwargs)
2759 def version_string(self):
2760 """Returns server's version string used for Server HTTP header."""
2762 return self.server_version
2764 def do_GET(self):
2765 """Handle GET requests."""
2766 if self._HasNoBody('GET'):
2767 self._HandleRequest()
2769 def do_POST(self):
2770 """Handles POST requests."""
2771 self._HandleRequest()
2773 def do_PUT(self):
2774 """Handle PUT requests."""
2775 self._HandleRequest()
2777 def do_HEAD(self):
2778 """Handle HEAD requests."""
2779 if self._HasNoBody('HEAD'):
2780 self._HandleRequest()
2782 def do_OPTIONS(self):
2783 """Handles OPTIONS requests."""
2784 self._HandleRequest()
2786 def do_DELETE(self):
2787 """Handle DELETE requests."""
2788 self._HandleRequest()
2790 def do_TRACE(self):
2791 """Handles TRACE requests."""
2792 if self._HasNoBody('TRACE'):
2793 self._HandleRequest()
2795 def _HasNoBody(self, method):
2796 """Check for request body in HTTP methods where no body is permitted.
2798 If a request body is present a 400 (Invalid request) response is sent.
2800 Args:
2801 method: The request method.
2803 Returns:
2804 True if no request body is present, False otherwise.
2808 content_length = int(self.headers.get('content-length', 0))
2809 if content_length:
2810 body = self.rfile.read(content_length)
2811 logging.warning('Request body in %s is not permitted: %s', method, body)
2812 self.send_response(httplib.BAD_REQUEST)
2813 return False
2814 return True
2816 def _Dispatch(self, dispatcher, socket_infile, outfile, env_dict):
2817 """Copy request data from socket and dispatch.
2819 Args:
2820 dispatcher: Dispatcher to handle request (MatcherDispatcher).
2821 socket_infile: Original request file stream.
2822 outfile: Output file to write response to.
2823 env_dict: Environment dictionary.
2827 request_descriptor, request_file_name = tempfile.mkstemp('.tmp',
2828 'request.')
2830 try:
2831 request_file = open(request_file_name, 'wb')
2832 try:
2833 CopyStreamPart(self.rfile,
2834 request_file,
2835 int(self.headers.get('content-length', 0)))
2836 finally:
2837 request_file.close()
2839 request_file = open(request_file_name, 'rb')
2840 try:
2841 app_server_request = AppServerRequest(self.path,
2842 None,
2843 self.headers,
2844 request_file)
2845 dispatcher.Dispatch(app_server_request,
2846 outfile,
2847 base_env_dict=env_dict)
2848 finally:
2849 request_file.close()
2850 finally:
2851 try:
2852 os.close(request_descriptor)
2856 try:
2857 os.remove(request_file_name)
2858 except OSError, err:
2859 if getattr(err, 'winerror', 0) == os_compat.ERROR_SHARING_VIOLATION:
2860 logging.warning('Failed removing %s', request_file_name)
2861 else:
2862 raise
2863 except OSError, err:
2864 if err.errno != errno.ENOENT:
2865 raise
2867 def _HandleRequest(self):
2868 """Handles any type of request and prints exceptions if they occur."""
2872 host_name = self.headers.get('host') or self.server.server_name
2873 server_name = host_name.split(':', 1)[0]
2875 env_dict = {
2876 'REQUEST_METHOD': self.command,
2877 'REMOTE_ADDR': self.client_address[0],
2878 'SERVER_SOFTWARE': self.server_version,
2879 'SERVER_NAME': server_name,
2880 'SERVER_PROTOCOL': self.protocol_version,
2881 'SERVER_PORT': str(self.server.server_port),
2884 full_url = GetFullURL(server_name, self.server.server_port, self.path)
2885 if len(full_url) > MAX_URL_LENGTH:
2886 msg = 'Requested URI too long: %s' % full_url
2887 logging.error(msg)
2888 self.send_response(httplib.REQUEST_URI_TOO_LONG, msg)
2889 return
2891 tbhandler = cgitb.Hook(file=self.wfile).handle
2892 try:
2894 config, explicit_matcher, from_cache = LoadAppConfig(
2895 root_path, self.module_dict, cache=self.config_cache,
2896 static_caching=static_caching, default_partition=default_partition)
2899 if not from_cache:
2900 self.module_manager.ResetModules()
2904 implicit_matcher = CreateImplicitMatcher(config,
2905 self.module_dict,
2906 root_path,
2907 login_url)
2909 if self.path.startswith('/_ah/admin'):
2912 if any((handler.url == '/_ah/datastore_admin.*'
2913 for handler in config.handlers)):
2914 self.headers['X-AppEngine-Datastore-Admin-Enabled'] = 'True'
2915 self.headers['X-AppEngine-Interactive-Console-Enabled'] = str(
2916 interactive_console)
2918 if config.api_version != API_VERSION:
2919 logging.error(
2920 "API versions cannot be switched dynamically: %r != %r",
2921 config.api_version, API_VERSION)
2922 sys.exit(1)
2924 (exclude, service_match) = ReservedPathFilter(
2925 config.inbound_services).ExcludePath(self.path)
2926 if exclude:
2927 logging.warning(
2928 'Request to %s excluded because %s is not enabled '
2929 'in inbound_services in app.yaml' % (self.path, service_match))
2930 self.send_response(httplib.NOT_FOUND)
2931 return
2933 if config.runtime == 'go':
2935 from google.appengine.ext import go
2936 go.APP_CONFIG = config
2938 version = GetVersionObject()
2939 env_dict['SDK_VERSION'] = version['release']
2940 env_dict['CURRENT_VERSION_ID'] = config.version + ".1"
2941 env_dict['APPLICATION_ID'] = config.application
2942 env_dict['DEFAULT_VERSION_HOSTNAME'] = self.server.frontend_hostport
2943 env_dict['APPENGINE_RUNTIME'] = config.runtime
2944 if config.runtime == 'python27' and config.threadsafe:
2945 env_dict['_AH_THREADSAFE'] = '1'
2949 global _request_time
2950 global _request_id
2951 _request_time = time.time()
2952 _request_id += 1
2954 request_id_hash = _generate_request_id_hash()
2955 env_dict['REQUEST_ID_HASH'] = request_id_hash
2956 os.environ['REQUEST_ID_HASH'] = request_id_hash
2959 multiprocess.GlobalProcess().UpdateEnv(env_dict)
2961 cookies = ', '.join(self.headers.getheaders('cookie'))
2962 email_addr, admin, user_id = dev_appserver_login.GetUserInfo(cookies)
2964 self._log_record_writer.start_request(
2965 request_id=None,
2966 user_request_id=_GenerateRequestLogId(),
2967 ip=env_dict['REMOTE_ADDR'],
2968 app_id=env_dict['APPLICATION_ID'],
2969 version_id=env_dict['CURRENT_VERSION_ID'],
2970 nickname=email_addr.split('@')[0],
2971 user_agent=self.headers.get('user-agent', ''),
2972 host=host_name,
2973 method=self.command,
2974 resource=self.path,
2975 http_version=self.request_version)
2977 dispatcher = MatcherDispatcher(config, login_url, self.module_manager,
2978 [implicit_matcher, explicit_matcher])
2983 if multiprocess.GlobalProcess().HandleRequest(self):
2984 return
2986 outfile = cStringIO.StringIO()
2987 try:
2988 self._Dispatch(dispatcher, self.rfile, outfile, env_dict)
2989 finally:
2990 self.module_manager.UpdateModuleFileModificationTimes()
2992 outfile.flush()
2993 outfile.seek(0)
2995 response = RewriteResponse(outfile, self.rewriter_chain, self.headers,
2996 env_dict)
2998 runtime_response_size = _RemainingDataSize(response.body)
2999 if self.command == 'HEAD' and runtime_response_size > 0:
3000 logging.warning('Dropping unexpected body in response to HEAD '
3001 'request')
3002 response.body = cStringIO.StringIO('')
3003 elif (not response.large_response and
3004 runtime_response_size > MAX_RUNTIME_RESPONSE_SIZE):
3005 logging.error('Response too large: %d, max is %d',
3006 runtime_response_size, MAX_RUNTIME_RESPONSE_SIZE)
3009 response.status_code = 500
3010 response.status_message = 'Forbidden'
3012 new_response = ('HTTP response was too large: %d. '
3013 'The limit is: %d.'
3014 % (runtime_response_size,
3015 MAX_RUNTIME_RESPONSE_SIZE))
3016 response.headers['Content-Length'] = str(len(new_response))
3017 response.body = cStringIO.StringIO(new_response)
3020 multiprocess.GlobalProcess().RequestComplete(self, response)
3022 except yaml_errors.EventListenerError, e:
3023 title = 'Fatal error when loading application configuration'
3024 msg = '%s:\n%s' % (title, str(e))
3025 logging.error(msg)
3026 self.send_response(httplib.INTERNAL_SERVER_ERROR, title)
3027 self.wfile.write('Content-Type: text/html\r\n\r\n')
3028 self.wfile.write('<pre>%s</pre>' % cgi.escape(msg))
3029 except KeyboardInterrupt, e:
3033 logging.info('Server interrupted by user, terminating')
3034 self.server.stop_serving_forever()
3035 except CompileError, e:
3036 msg = 'Compile error:\n' + e.text + '\n'
3037 logging.error(msg)
3038 self.send_response(httplib.INTERNAL_SERVER_ERROR, 'Compile error')
3039 self.wfile.write('Content-Type: text/plain; charset=utf-8\r\n\r\n')
3040 self.wfile.write(msg)
3041 except ExecuteError, e:
3042 logging.error(e.text)
3043 self.send_response(httplib.INTERNAL_SERVER_ERROR, 'Execute error')
3044 self.wfile.write('Content-Type: text/html; charset=utf-8\r\n\r\n')
3045 self.wfile.write('<title>App failure</title>\n')
3046 self.wfile.write(e.text + '\n<pre>\n')
3047 for l in e.log:
3048 self.wfile.write(cgi.escape(l))
3049 self.wfile.write('</pre>\n')
3050 except:
3051 msg = 'Exception encountered handling request'
3052 logging.exception(msg)
3053 self.send_response(httplib.INTERNAL_SERVER_ERROR, msg)
3054 tbhandler()
3055 else:
3056 try:
3057 self.send_response(response.status_code, response.status_message)
3058 self.wfile.write(response.header_data)
3059 self.wfile.write('\r\n')
3061 shutil.copyfileobj(response.body, self.wfile, COPY_BLOCK_SIZE)
3062 except (IOError, OSError), e:
3073 if e.errno not in [errno.EPIPE, os_compat.WSAECONNABORTED]:
3074 raise e
3075 except socket.error, e:
3076 if len(e.args) >= 1 and e.args[0] != errno.EPIPE:
3077 raise e
3079 def log_error(self, format, *args):
3080 """Redirect error messages through the logging module."""
3081 logging.error(format, *args)
3083 def log_message(self, format, *args):
3084 """Redirect log messages through the logging module."""
3087 if hasattr(self, 'path') and self.channel_poll_path_re.match(self.path):
3088 logging.debug(format, *args)
3089 else:
3090 logging.info(format, *args)
3092 def log_request(self, code='-', size='-'):
3093 """Indicate that this request has completed."""
3094 BaseHTTPServer.BaseHTTPRequestHandler.log_request(self, code, size)
3095 if code == '-':
3096 code = 0
3097 if size == '-':
3098 size = 0
3101 logservice.logs_buffer().flush()
3102 self._log_record_writer.end_request(None, code, size)
3103 return DevAppServerRequestHandler
3108 def ReadAppConfig(appinfo_path, parse_app_config=appinfo_includes.Parse):
3109 """Reads app.yaml file and returns its app id and list of URLMap instances.
3111 Args:
3112 appinfo_path: String containing the path to the app.yaml file.
3113 parse_app_config: Used for dependency injection.
3115 Returns:
3116 AppInfoExternal instance.
3118 Raises:
3119 If the config file could not be read or the config does not contain any
3120 URLMap instances, this function will raise an InvalidAppConfigError
3121 exception.
3123 try:
3124 appinfo_file = file(appinfo_path, 'r')
3125 except IOError, unused_e:
3126 raise InvalidAppConfigError(
3127 'Application configuration could not be read from "%s"' % appinfo_path)
3128 try:
3131 return parse_app_config(appinfo_file)
3132 finally:
3133 appinfo_file.close()
3136 def _StaticFilePathRe(url_map):
3137 """Returns a regular expression string that matches static file paths.
3139 Args:
3140 url_map: A fully initialized static_files or static_dir appinfo.URLMap
3141 instance.
3143 Returns:
3144 The regular expression matches paths, relative to the application's root
3145 directory, of files that this static handler serves. re.compile should
3146 accept the returned string.
3148 Raises:
3149 AssertionError: The url_map argument was not an URLMap for a static handler.
3151 handler_type = url_map.GetHandlerType()
3154 if handler_type == 'static_files':
3155 return url_map.upload + '$'
3157 elif handler_type == 'static_dir':
3158 path = url_map.static_dir.rstrip(os.path.sep)
3159 return path + re.escape(os.path.sep) + r'(.*)'
3161 assert False, 'This property only applies to static handlers.'
3164 def CreateURLMatcherFromMaps(config,
3165 root_path,
3166 url_map_list,
3167 module_dict,
3168 default_expiration,
3169 create_url_matcher=URLMatcher,
3170 create_cgi_dispatcher=CGIDispatcher,
3171 create_file_dispatcher=FileDispatcher,
3172 create_path_adjuster=PathAdjuster,
3173 normpath=os.path.normpath):
3174 """Creates a URLMatcher instance from URLMap.
3176 Creates all of the correct URLDispatcher instances to handle the various
3177 content types in the application configuration.
3179 Args:
3180 config: AppInfoExternal instance representing the parsed app.yaml file.
3181 root_path: Path to the root of the application running on the server.
3182 url_map_list: List of appinfo.URLMap objects to initialize this
3183 matcher with. Can be an empty list if you would like to add patterns
3184 manually or use config.handlers as a default.
3185 module_dict: Dictionary in which application-loaded modules should be
3186 preserved between requests. This dictionary must be separate from the
3187 sys.modules dictionary.
3188 default_expiration: String describing default expiration time for browser
3189 based caching of static files. If set to None this disallows any
3190 browser caching of static content.
3191 create_url_matcher: Used for dependency injection.
3192 create_cgi_dispatcher: Used for dependency injection.
3193 create_file_dispatcher: Used for dependency injection.
3194 create_path_adjuster: Used for dependency injection.
3195 normpath: Used for dependency injection.
3197 Returns:
3198 Instance of URLMatcher with the supplied URLMap objects properly loaded.
3200 Raises:
3201 InvalidAppConfigError: if a handler is an unknown type.
3203 if config and config.handlers and not url_map_list:
3204 url_map_list = config.handlers
3205 url_matcher = create_url_matcher()
3206 path_adjuster = create_path_adjuster(root_path)
3207 cgi_dispatcher = create_cgi_dispatcher(config, module_dict,
3208 root_path, path_adjuster)
3209 static_file_config_matcher = StaticFileConfigMatcher(url_map_list,
3210 default_expiration)
3211 file_dispatcher = create_file_dispatcher(config, path_adjuster,
3212 static_file_config_matcher)
3214 FakeFile.SetStaticFileConfigMatcher(static_file_config_matcher)
3216 for url_map in url_map_list:
3217 admin_only = url_map.login == appinfo.LOGIN_ADMIN
3218 requires_login = url_map.login == appinfo.LOGIN_REQUIRED or admin_only
3219 auth_fail_action = url_map.auth_fail_action
3221 handler_type = url_map.GetHandlerType()
3222 if handler_type == appinfo.HANDLER_SCRIPT:
3223 dispatcher = cgi_dispatcher
3224 elif handler_type in (appinfo.STATIC_FILES, appinfo.STATIC_DIR):
3225 dispatcher = file_dispatcher
3226 else:
3228 raise InvalidAppConfigError('Unknown handler type "%s"' % handler_type)
3231 regex = url_map.url
3232 path = url_map.GetHandler()
3233 if handler_type == appinfo.STATIC_DIR:
3234 if regex[-1] == r'/':
3235 regex = regex[:-1]
3236 if path[-1] == os.path.sep:
3237 path = path[:-1]
3238 regex = '/'.join((re.escape(regex), '(.*)'))
3239 if os.path.sep == '\\':
3240 backref = r'\\1'
3241 else:
3242 backref = r'\1'
3243 path = (normpath(path).replace('\\', '\\\\') +
3244 os.path.sep + backref)
3246 url_matcher.AddURL(regex,
3247 dispatcher,
3248 path,
3249 requires_login, admin_only, auth_fail_action)
3251 return url_matcher
3254 class AppConfigCache(object):
3255 """Cache used by LoadAppConfig.
3257 If given to LoadAppConfig instances of this class are used to cache contents
3258 of the app config (app.yaml or app.yml) and the Matcher created from it.
3260 Code outside LoadAppConfig should treat instances of this class as opaque
3261 objects and not access its members.
3265 path = None
3270 mtime = None
3272 config = None
3274 matcher = None
3277 def LoadAppConfig(root_path,
3278 module_dict,
3279 cache=None,
3280 static_caching=True,
3281 read_app_config=ReadAppConfig,
3282 create_matcher=CreateURLMatcherFromMaps,
3283 default_partition=None):
3284 """Creates a Matcher instance for an application configuration file.
3286 Raises an InvalidAppConfigError exception if there is anything wrong with
3287 the application configuration file.
3289 Args:
3290 root_path: Path to the root of the application to load.
3291 module_dict: Dictionary in which application-loaded modules should be
3292 preserved between requests. This dictionary must be separate from the
3293 sys.modules dictionary.
3294 cache: Instance of AppConfigCache or None.
3295 static_caching: True if browser caching of static files should be allowed.
3296 read_app_config: Used for dependency injection.
3297 create_matcher: Used for dependency injection.
3298 default_partition: Default partition to use for the appid.
3300 Returns:
3301 tuple: (AppInfoExternal, URLMatcher, from_cache)
3303 Raises:
3304 AppConfigNotFound: if an app.yaml file cannot be found.
3306 for appinfo_path in [os.path.join(root_path, 'app.yaml'),
3307 os.path.join(root_path, 'app.yml')]:
3309 if os.path.isfile(appinfo_path):
3310 if cache is not None:
3312 mtime = os.path.getmtime(appinfo_path)
3313 if cache.path == appinfo_path and cache.mtime == mtime:
3314 return (cache.config, cache.matcher, True)
3317 cache.config = cache.matcher = cache.path = None
3318 cache.mtime = mtime
3320 config = read_app_config(appinfo_path, appinfo_includes.Parse)
3322 if config.application:
3323 config.application = AppIdWithDefaultPartition(config.application,
3324 default_partition)
3325 multiprocess.GlobalProcess().NewAppInfo(config)
3327 if static_caching:
3328 if config.default_expiration:
3329 default_expiration = config.default_expiration
3330 else:
3333 default_expiration = '0'
3334 else:
3336 default_expiration = None
3338 matcher = create_matcher(config,
3339 root_path,
3340 config.handlers,
3341 module_dict,
3342 default_expiration)
3344 FakeFile.SetSkippedFiles(config.skip_files)
3346 if cache is not None:
3347 cache.path = appinfo_path
3348 cache.config = config
3349 cache.matcher = matcher
3351 return config, matcher, False
3353 raise AppConfigNotFoundError(
3354 'Could not find app.yaml in "%s".' % (root_path,))
3357 class ReservedPathFilter():
3358 """Checks a path against a set of inbound_services."""
3360 reserved_paths = {
3361 '/_ah/channel/connect': 'channel_presence',
3362 '/_ah/channel/disconnect': 'channel_presence'
3365 def __init__(self, inbound_services):
3366 self.inbound_services = inbound_services
3368 def ExcludePath(self, path):
3369 """Check to see if this is a service url and matches inbound_services."""
3370 skip = False
3371 for reserved_path in self.reserved_paths.keys():
3372 if path.startswith(reserved_path):
3373 if (not self.inbound_services or
3374 self.reserved_paths[reserved_path] not in self.inbound_services):
3375 return (True, self.reserved_paths[reserved_path])
3377 return (False, None)
3380 def CreateInboundServiceFilter(inbound_services):
3381 return ReservedPathFilter(inbound_services)
3384 def ReadCronConfig(croninfo_path, parse_cron_config=croninfo.LoadSingleCron):
3385 """Reads cron.yaml file and returns a list of CronEntry instances.
3387 Args:
3388 croninfo_path: String containing the path to the cron.yaml file.
3389 parse_cron_config: Used for dependency injection.
3391 Returns:
3392 A CronInfoExternal object.
3394 Raises:
3395 If the config file is unreadable, empty or invalid, this function will
3396 raise an InvalidAppConfigError or a MalformedCronConfiguration exception.
3398 try:
3399 croninfo_file = file(croninfo_path, 'r')
3400 except IOError, e:
3401 raise InvalidAppConfigError(
3402 'Cron configuration could not be read from "%s": %s'
3403 % (croninfo_path, e))
3404 try:
3405 return parse_cron_config(croninfo_file)
3406 finally:
3407 croninfo_file.close()
3412 def _RemoveFile(file_path):
3413 if file_path and os.path.lexists(file_path):
3414 logging.info('Attempting to remove file at %s', file_path)
3415 try:
3416 os.remove(file_path)
3417 except OSError, e:
3418 logging.warning('Removing file failed: %s', e)
3421 def SetupStubs(app_id, **config):
3422 """Sets up testing stubs of APIs.
3424 Args:
3425 app_id: Application ID being served.
3426 config: keyword arguments.
3428 Keywords:
3429 root_path: Root path to the directory of the application which should
3430 contain the app.yaml, index.yaml, and queue.yaml files.
3431 login_url: Relative URL which should be used for handling user login/logout.
3432 blobstore_path: Path to the directory to store Blobstore blobs in.
3433 datastore_path: Path to the file to store Datastore file stub data in.
3434 prospective_search_path: Path to the file to store Prospective Search stub
3435 data in.
3436 use_sqlite: Use the SQLite stub for the datastore.
3437 auto_id_policy: How datastore stub assigns IDs, sequential or scattered.
3438 high_replication: Use the high replication consistency model
3439 history_path: DEPRECATED, No-op.
3440 clear_datastore: If the datastore should be cleared on startup.
3441 smtp_host: SMTP host used for sending test mail.
3442 smtp_port: SMTP port.
3443 smtp_user: SMTP user.
3444 smtp_password: SMTP password.
3445 mysql_host: MySQL host.
3446 mysql_port: MySQL port.
3447 mysql_user: MySQL user.
3448 mysql_password: MySQL password.
3449 mysql_socket: MySQL socket.
3450 enable_sendmail: Whether to use sendmail as an alternative to SMTP.
3451 show_mail_body: Whether to log the body of emails.
3452 remove: Used for dependency injection.
3453 disable_task_running: True if tasks should not automatically run after
3454 they are enqueued.
3455 task_retry_seconds: How long to wait after an auto-running task before it
3456 is tried again.
3457 logs_path: Path to the file to store the logs data in.
3458 trusted: True if this app can access data belonging to other apps. This
3459 behavior is different from the real app server and should be left False
3460 except for advanced uses of dev_appserver.
3461 port: The port that this dev_appserver is bound to. Defaults to 8080
3462 address: The host that this dev_appsever is running on. Defaults to
3463 localhost.
3464 search_index_path: Path to the file to store search indexes in.
3465 clear_search_index: If the search indices should be cleared on startup.
3471 root_path = config.get('root_path', None)
3472 login_url = config['login_url']
3473 blobstore_path = config['blobstore_path']
3474 datastore_path = config['datastore_path']
3475 clear_datastore = config['clear_datastore']
3476 prospective_search_path = config.get('prospective_search_path', '')
3477 clear_prospective_search = config.get('clear_prospective_search', False)
3478 use_sqlite = config.get('use_sqlite', False)
3479 auto_id_policy = config.get('auto_id_policy', datastore_stub_util.SEQUENTIAL)
3480 high_replication = config.get('high_replication', False)
3481 require_indexes = config.get('require_indexes', False)
3482 mysql_host = config.get('mysql_host', None)
3483 mysql_port = config.get('mysql_port', 3306)
3484 mysql_user = config.get('mysql_user', None)
3485 mysql_password = config.get('mysql_password', None)
3486 mysql_socket = config.get('mysql_socket', None)
3487 smtp_host = config.get('smtp_host', None)
3488 smtp_port = config.get('smtp_port', 25)
3489 smtp_user = config.get('smtp_user', '')
3490 smtp_password = config.get('smtp_password', '')
3491 enable_sendmail = config.get('enable_sendmail', False)
3492 show_mail_body = config.get('show_mail_body', False)
3493 remove = config.get('remove', os.remove)
3494 disable_task_running = config.get('disable_task_running', False)
3495 task_retry_seconds = config.get('task_retry_seconds', 30)
3496 logs_path = config.get('logs_path', ':memory:')
3497 trusted = config.get('trusted', False)
3498 serve_port = config.get('port', 8080)
3499 serve_address = config.get('address', 'localhost')
3500 clear_search_index = config.get('clear_search_indexes', False)
3501 search_index_path = config.get('search_indexes_path', None)
3502 _use_atexit_for_datastore_stub = config.get('_use_atexit_for_datastore_stub',
3503 False)
3504 port_sqlite_data = config.get('port_sqlite_data', False)
3510 os.environ['APPLICATION_ID'] = app_id
3514 os.environ['REQUEST_ID_HASH'] = ''
3516 if clear_prospective_search and prospective_search_path:
3517 _RemoveFile(prospective_search_path)
3519 if clear_datastore:
3520 _RemoveFile(datastore_path)
3522 if clear_search_index:
3523 _RemoveFile(search_index_path)
3526 if multiprocess.GlobalProcess().MaybeConfigureRemoteDataApis():
3530 apiproxy_stub_map.apiproxy.RegisterStub(
3531 'logservice',
3532 logservice_stub.LogServiceStub(logs_path=':memory:'))
3533 else:
3540 apiproxy_stub_map.apiproxy = apiproxy_stub_map.APIProxyStubMap()
3542 apiproxy_stub_map.apiproxy.RegisterStub(
3543 'app_identity_service',
3544 app_identity_stub.AppIdentityServiceStub())
3546 apiproxy_stub_map.apiproxy.RegisterStub(
3547 'capability_service',
3548 capability_stub.CapabilityServiceStub())
3550 if use_sqlite:
3551 if port_sqlite_data:
3552 try:
3553 PortAllEntities(datastore_path)
3554 except Error:
3555 logging.Error("Porting the data from the datastore file stub failed")
3556 raise
3558 datastore = datastore_sqlite_stub.DatastoreSqliteStub(
3559 app_id, datastore_path, require_indexes=require_indexes,
3560 trusted=trusted, root_path=root_path,
3561 use_atexit=_use_atexit_for_datastore_stub,
3562 auto_id_policy=auto_id_policy)
3563 else:
3564 logging.warning(FILE_STUB_DEPRECATION_MESSAGE)
3565 datastore = datastore_file_stub.DatastoreFileStub(
3566 app_id, datastore_path, require_indexes=require_indexes,
3567 trusted=trusted, root_path=root_path,
3568 use_atexit=_use_atexit_for_datastore_stub,
3569 auto_id_policy=auto_id_policy)
3571 if high_replication:
3572 datastore.SetConsistencyPolicy(
3573 datastore_stub_util.TimeBasedHRConsistencyPolicy())
3574 apiproxy_stub_map.apiproxy.ReplaceStub(
3575 'datastore_v3', datastore)
3577 apiproxy_stub_map.apiproxy.RegisterStub(
3578 'mail',
3579 mail_stub.MailServiceStub(smtp_host,
3580 smtp_port,
3581 smtp_user,
3582 smtp_password,
3583 enable_sendmail=enable_sendmail,
3584 show_mail_body=show_mail_body))
3586 apiproxy_stub_map.apiproxy.RegisterStub(
3587 'memcache',
3588 memcache_stub.MemcacheServiceStub())
3590 apiproxy_stub_map.apiproxy.RegisterStub(
3591 'taskqueue',
3592 taskqueue_stub.TaskQueueServiceStub(
3593 root_path=root_path,
3594 auto_task_running=(not disable_task_running),
3595 task_retry_seconds=task_retry_seconds,
3596 default_http_server='%s:%s' % (serve_address, serve_port)))
3598 urlmatchers_to_fetch_functions = []
3599 urlmatchers_to_fetch_functions.extend(
3600 gcs_dispatcher.URLMATCHERS_TO_FETCH_FUNCTIONS)
3601 apiproxy_stub_map.apiproxy.RegisterStub(
3602 'urlfetch',
3603 urlfetch_stub.URLFetchServiceStub(
3604 urlmatchers_to_fetch_functions=urlmatchers_to_fetch_functions))
3606 apiproxy_stub_map.apiproxy.RegisterStub(
3607 'xmpp',
3608 xmpp_service_stub.XmppServiceStub())
3610 apiproxy_stub_map.apiproxy.RegisterStub(
3611 'logservice',
3612 logservice_stub.LogServiceStub(logs_path=logs_path))
3617 from google.appengine import api
3618 sys.modules['google.appengine.api.rdbms'] = rdbms_mysqldb
3619 api.rdbms = rdbms_mysqldb
3620 rdbms_mysqldb.SetConnectKwargs(host=mysql_host, port=mysql_port,
3621 user=mysql_user, passwd=mysql_password,
3622 unix_socket=mysql_socket)
3624 fixed_login_url = '%s?%s=%%s' % (login_url,
3625 dev_appserver_login.CONTINUE_PARAM)
3626 fixed_logout_url = '%s&%s' % (fixed_login_url,
3627 dev_appserver_login.LOGOUT_PARAM)
3633 apiproxy_stub_map.apiproxy.RegisterStub(
3634 'user',
3635 user_service_stub.UserServiceStub(login_url=fixed_login_url,
3636 logout_url=fixed_logout_url))
3638 apiproxy_stub_map.apiproxy.RegisterStub(
3639 'channel',
3640 channel_service_stub.ChannelServiceStub())
3642 apiproxy_stub_map.apiproxy.RegisterStub(
3643 'matcher',
3644 prospective_search_stub.ProspectiveSearchStub(
3645 prospective_search_path,
3646 apiproxy_stub_map.apiproxy.GetStub('taskqueue')))
3648 apiproxy_stub_map.apiproxy.RegisterStub(
3649 'remote_socket',
3650 _remote_socket_stub.RemoteSocketServiceStub())
3652 apiproxy_stub_map.apiproxy.RegisterStub(
3653 'search',
3654 simple_search_stub.SearchServiceStub(index_file=search_index_path))
3660 try:
3661 from google.appengine.api.images import images_stub
3662 host_prefix = 'http://%s:%d' % (serve_address, serve_port)
3663 apiproxy_stub_map.apiproxy.RegisterStub(
3664 'images',
3665 images_stub.ImagesServiceStub(host_prefix=host_prefix))
3666 except ImportError, e:
3667 logging.warning('Could not initialize images API; you are likely missing '
3668 'the Python "PIL" module. ImportError: %s', e)
3670 from google.appengine.api.images import images_not_implemented_stub
3671 apiproxy_stub_map.apiproxy.RegisterStub(
3672 'images',
3673 images_not_implemented_stub.ImagesNotImplementedServiceStub())
3675 blob_storage = file_blob_storage.FileBlobStorage(blobstore_path, app_id)
3676 apiproxy_stub_map.apiproxy.RegisterStub(
3677 'blobstore',
3678 blobstore_stub.BlobstoreServiceStub(blob_storage))
3680 apiproxy_stub_map.apiproxy.RegisterStub(
3681 'file',
3682 file_service_stub.FileServiceStub(blob_storage))
3684 system_service_stub = system_stub.SystemServiceStub()
3685 multiprocess.GlobalProcess().UpdateSystemStub(system_service_stub)
3686 apiproxy_stub_map.apiproxy.RegisterStub('system', system_service_stub)
3689 def TearDownStubs():
3690 """Clean up any stubs that need cleanup."""
3692 datastore_stub = apiproxy_stub_map.apiproxy.GetStub('datastore_v3')
3695 if isinstance(datastore_stub, datastore_stub_util.BaseTransactionManager):
3696 logging.info('Applying all pending transactions and saving the datastore')
3697 datastore_stub.Write()
3699 search_stub = apiproxy_stub_map.apiproxy.GetStub('search')
3700 if isinstance(search_stub, simple_search_stub.SearchServiceStub):
3701 logging.info('Saving search indexes')
3702 search_stub.Write()
3705 def CreateImplicitMatcher(
3706 config,
3707 module_dict,
3708 root_path,
3709 login_url,
3710 create_path_adjuster=PathAdjuster,
3711 create_local_dispatcher=LocalCGIDispatcher,
3712 create_cgi_dispatcher=CGIDispatcher,
3713 get_blob_storage=dev_appserver_blobstore.GetBlobStorage):
3714 """Creates a URLMatcher instance that handles internal URLs.
3716 Used to facilitate handling user login/logout, debugging, info about the
3717 currently running app, quitting the dev appserver, etc.
3719 Args:
3720 config: AppInfoExternal instance representing the parsed app.yaml file.
3721 module_dict: Dictionary in the form used by sys.modules.
3722 root_path: Path to the root of the application.
3723 login_url: Relative URL which should be used for handling user login/logout.
3724 create_path_adjuster: Used for dependedency injection.
3725 create_local_dispatcher: Used for dependency injection.
3726 create_cgi_dispatcher: Used for dependedency injection.
3727 get_blob_storage: Used for dependency injection.
3729 Returns:
3730 Instance of URLMatcher with appropriate dispatchers.
3732 url_matcher = URLMatcher()
3733 path_adjuster = create_path_adjuster(root_path)
3738 def _HandleQuit():
3739 raise KeyboardInterrupt
3740 quit_dispatcher = create_local_dispatcher(config, sys.modules, path_adjuster,
3741 _HandleQuit)
3742 url_matcher.AddURL('/_ah/quit?',
3743 quit_dispatcher,
3745 False,
3746 False,
3747 appinfo.AUTH_FAIL_ACTION_REDIRECT)
3752 login_dispatcher = create_local_dispatcher(config, sys.modules, path_adjuster,
3753 dev_appserver_login.main)
3754 url_matcher.AddURL(login_url,
3755 login_dispatcher,
3757 False,
3758 False,
3759 appinfo.AUTH_FAIL_ACTION_REDIRECT)
3761 admin_dispatcher = create_cgi_dispatcher(config, module_dict, root_path,
3762 path_adjuster)
3763 url_matcher.AddURL('/_ah/admin(?:/.*)?',
3764 admin_dispatcher,
3765 DEVEL_CONSOLE_PATH,
3766 False,
3767 False,
3768 appinfo.AUTH_FAIL_ACTION_REDIRECT)
3770 upload_dispatcher = dev_appserver_blobstore.CreateUploadDispatcher(
3771 get_blob_storage)
3773 url_matcher.AddURL(dev_appserver_blobstore.UPLOAD_URL_PATTERN,
3774 upload_dispatcher,
3776 False,
3777 False,
3778 appinfo.AUTH_FAIL_ACTION_UNAUTHORIZED)
3780 blobimage_dispatcher = dev_appserver_blobimage.CreateBlobImageDispatcher(
3781 apiproxy_stub_map.apiproxy.GetStub('images'))
3782 url_matcher.AddURL(dev_appserver_blobimage.BLOBIMAGE_URL_PATTERN,
3783 blobimage_dispatcher,
3785 False,
3786 False,
3787 appinfo.AUTH_FAIL_ACTION_UNAUTHORIZED)
3789 oauth_dispatcher = dev_appserver_oauth.CreateOAuthDispatcher()
3791 url_matcher.AddURL(dev_appserver_oauth.OAUTH_URL_PATTERN,
3792 oauth_dispatcher,
3794 False,
3795 False,
3796 appinfo.AUTH_FAIL_ACTION_UNAUTHORIZED)
3798 channel_dispatcher = dev_appserver_channel.CreateChannelDispatcher(
3799 apiproxy_stub_map.apiproxy.GetStub('channel'))
3801 url_matcher.AddURL(dev_appserver_channel.CHANNEL_POLL_PATTERN,
3802 channel_dispatcher,
3804 False,
3805 False,
3806 appinfo.AUTH_FAIL_ACTION_UNAUTHORIZED)
3808 url_matcher.AddURL(dev_appserver_channel.CHANNEL_JSAPI_PATTERN,
3809 channel_dispatcher,
3811 False,
3812 False,
3813 appinfo.AUTH_FAIL_ACTION_UNAUTHORIZED)
3815 apiserver_dispatcher = dev_appserver_apiserver.CreateApiserverDispatcher()
3816 url_matcher.AddURL(dev_appserver_apiserver.API_SERVING_PATTERN,
3817 apiserver_dispatcher,
3819 False,
3820 False,
3821 appinfo.AUTH_FAIL_ACTION_UNAUTHORIZED)
3823 return url_matcher
3826 def FetchAllEntitites():
3827 """Returns all datastore entities from all namespaces as a list."""
3828 ns = list(datastore.Query('__namespace__').Run())
3829 original_ns = namespace_manager.get_namespace()
3830 entities_set = []
3831 for namespace in ns:
3832 namespace_manager.set_namespace(namespace.key().name())
3833 kinds_list = list(datastore.Query('__kind__').Run())
3834 for kind_entity in kinds_list:
3835 ents = list(datastore.Query(kind_entity.key().name()).Run())
3836 for ent in ents:
3837 entities_set.append(ent)
3838 namespace_manager.set_namespace(original_ns)
3839 return entities_set
3842 def PutAllEntities(entities):
3843 """Puts all entities to the current datastore."""
3844 for entity in entities:
3845 datastore.Put(entity)
3848 def PortAllEntities(datastore_path):
3849 """Copies entities from a DatastoreFileStub to an SQLite stub.
3851 Args:
3852 datastore_path: Path to the file to store Datastore file stub data is.
3855 previous_stub = apiproxy_stub_map.apiproxy.GetStub('datastore_v3')
3857 try:
3858 app_id = os.environ['APPLICATION_ID']
3859 apiproxy_stub_map.apiproxy = apiproxy_stub_map.APIProxyStubMap()
3860 datastore_stub = datastore_file_stub.DatastoreFileStub(
3861 app_id, datastore_path, trusted=True)
3862 apiproxy_stub_map.apiproxy.RegisterStub('datastore_v3', datastore_stub)
3864 entities = FetchAllEntitites()
3865 sqlite_datastore_stub = datastore_sqlite_stub.DatastoreSqliteStub(app_id,
3866 datastore_path + '.sqlite', trusted=True)
3867 apiproxy_stub_map.apiproxy.ReplaceStub('datastore_v3',
3868 sqlite_datastore_stub)
3869 PutAllEntities(entities)
3870 sqlite_datastore_stub.Close()
3871 finally:
3872 apiproxy_stub_map.apiproxy.ReplaceStub('datastore_v3', previous_stub)
3874 shutil.copy(datastore_path, datastore_path + '.filestub')
3875 _RemoveFile(datastore_path)
3876 shutil.move(datastore_path + '.sqlite', datastore_path)
3879 def CreateServer(root_path,
3880 login_url,
3881 port,
3882 template_dir=None,
3883 serve_address='',
3884 allow_skipped_files=False,
3885 static_caching=True,
3886 python_path_list=sys.path,
3887 sdk_dir=SDK_ROOT,
3888 default_partition=None,
3889 frontend_port=None,
3890 interactive_console=True):
3891 """Creates a new HTTPServer for an application.
3893 The sdk_dir argument must be specified for the directory storing all code for
3894 the SDK so as to allow for the sandboxing of module access to work for any
3895 and all SDK code. While typically this is where the 'google' package lives,
3896 it can be in another location because of API version support.
3898 Args:
3899 root_path: String containing the path to the root directory of the
3900 application where the app.yaml file is.
3901 login_url: Relative URL which should be used for handling user login/logout.
3902 port: Port to start the application server on.
3903 template_dir: Unused.
3904 serve_address: Address on which the server should serve.
3905 allow_skipped_files: True if skipped files should be accessible.
3906 static_caching: True if browser caching of static files should be allowed.
3907 python_path_list: Used for dependency injection.
3908 sdk_dir: Directory where the SDK is stored.
3909 default_partition: Default partition to use for the appid.
3910 frontend_port: A frontend port (so backends can return an address for a
3911 frontend). If None, port will be used.
3912 interactive_console: Whether to add the interactive console.
3914 Returns:
3915 Instance of BaseHTTPServer.HTTPServer that's ready to start accepting.
3922 absolute_root_path = os.path.realpath(root_path)
3924 FakeFile.SetAllowedPaths(absolute_root_path,
3925 [sdk_dir])
3926 FakeFile.SetAllowSkippedFiles(allow_skipped_files)
3928 handler_class = CreateRequestHandler(absolute_root_path,
3929 login_url,
3930 static_caching,
3931 default_partition,
3932 interactive_console)
3935 if absolute_root_path not in python_path_list:
3938 python_path_list.insert(0, absolute_root_path)
3940 if multiprocess.Enabled():
3941 server = HttpServerWithMultiProcess((serve_address, port), handler_class)
3942 else:
3943 server = HTTPServerWithScheduler((serve_address, port), handler_class)
3947 queue_stub = apiproxy_stub_map.apiproxy.GetStub('taskqueue')
3948 if queue_stub and hasattr(queue_stub, 'StartBackgroundExecution'):
3949 queue_stub.StartBackgroundExecution()
3951 request_info._local_dispatcher = DevAppserverDispatcher(server,
3952 frontend_port or port)
3953 server.frontend_hostport = '%s:%d' % (serve_address or 'localhost',
3954 frontend_port or port)
3956 return server
3959 class HTTPServerWithScheduler(BaseHTTPServer.HTTPServer):
3960 """A BaseHTTPServer subclass that calls a method at a regular interval."""
3962 def __init__(self, server_address, request_handler_class):
3963 """Constructor.
3965 Args:
3966 server_address: the bind address of the server.
3967 request_handler_class: class used to handle requests.
3969 BaseHTTPServer.HTTPServer.__init__(self, server_address,
3970 request_handler_class)
3971 self._events = []
3972 self._stopped = False
3974 def handle_request(self):
3975 """Override the base handle_request call.
3977 Python 2.6 changed the semantics of handle_request() with r61289.
3978 This patches it back to the Python 2.5 version, which has
3979 helpfully been renamed to _handle_request_noblock.
3981 if hasattr(self, "_handle_request_noblock"):
3982 self._handle_request_noblock()
3983 else:
3984 BaseHTTPServer.HTTPServer.handle_request(self)
3986 def get_request(self, time_func=time.time, select_func=select.select):
3987 """Overrides the base get_request call.
3989 Args:
3990 time_func: used for testing.
3991 select_func: used for testing.
3993 Returns:
3994 a (socket_object, address info) tuple.
3996 while True:
3997 if self._events:
3998 current_time = time_func()
3999 next_eta = self._events[0][0]
4000 delay = next_eta - current_time
4001 else:
4002 delay = DEFAULT_SELECT_DELAY
4003 readable, _, _ = select_func([self.socket], [], [], max(delay, 0))
4004 if readable:
4005 return self.socket.accept()
4006 current_time = time_func()
4010 if self._events and current_time >= self._events[0][0]:
4011 runnable = heapq.heappop(self._events)[1]
4012 request_tuple = runnable()
4013 if request_tuple:
4014 return request_tuple
4016 def serve_forever(self):
4017 """Handle one request at a time until told to stop."""
4018 while not self._stopped:
4019 self.handle_request()
4020 self.server_close()
4022 def stop_serving_forever(self):
4023 """Stop the serve_forever() loop.
4025 Stop happens on the next handle_request() loop; it will not stop
4026 immediately. Since dev_appserver.py must run on py2.5 we can't
4027 use newer features of SocketServer (e.g. shutdown(), added in py2.6).
4029 self._stopped = True
4031 def AddEvent(self, eta, runnable, service=None, event_id=None):
4032 """Add a runnable event to be run at the specified time.
4034 Args:
4035 eta: when to run the event, in seconds since epoch.
4036 runnable: a callable object.
4037 service: the service that owns this event. Should be set if id is set.
4038 event_id: optional id of the event. Used for UpdateEvent below.
4040 heapq.heappush(self._events, (eta, runnable, service, event_id))
4042 def UpdateEvent(self, service, event_id, eta):
4043 """Update a runnable event in the heap with a new eta.
4044 TODO: come up with something better than a linear scan to
4045 update items. For the case this is used for now -- updating events to
4046 "time out" channels -- this works fine because those events are always
4047 soon (within seconds) and thus found quickly towards the front of the heap.
4048 One could easily imagine a scenario where this is always called for events
4049 that tend to be at the back of the heap, of course...
4051 Args:
4052 service: the service that owns this event.
4053 event_id: the id of the event.
4054 eta: the new eta of the event.
4056 for id in xrange(len(self._events)):
4057 item = self._events[id]
4058 if item[2] == service and item[3] == event_id:
4059 item = (eta, item[1], item[2], item[3])
4060 del(self._events[id])
4061 heapq.heappush(self._events, item)
4062 break
4065 class HttpServerWithMultiProcess(HTTPServerWithScheduler):
4066 """Class extending HTTPServerWithScheduler with multi-process handling."""
4068 def __init__(self, server_address, request_handler_class):
4069 """Constructor.
4071 Args:
4072 server_address: the bind address of the server.
4073 request_handler_class: class used to handle requests.
4075 HTTPServerWithScheduler.__init__(self, server_address,
4076 request_handler_class)
4077 multiprocess.GlobalProcess().SetHttpServer(self)
4079 def process_request(self, request, client_address):
4080 """Overrides the SocketServer process_request call."""
4081 multiprocess.GlobalProcess().ProcessRequest(request, client_address)
4084 class FakeRequestSocket(object):
4085 """A socket object to fake an HTTP request."""
4087 def __init__(self, method, relative_url, headers, body):
4088 payload = cStringIO.StringIO()
4089 payload.write('%s %s HTTP/1.1\r\n' % (method, relative_url))
4090 payload.write('Content-Length: %d\r\n' % len(body))
4091 for key, value in headers:
4092 payload.write('%s: %s\r\n' % (key, value))
4093 payload.write('\r\n')
4094 payload.write(body)
4095 self.rfile = cStringIO.StringIO(payload.getvalue())
4096 self.wfile = StringIO.StringIO()
4097 self.wfile_close = self.wfile.close
4098 self.wfile.close = self.connection_done
4100 def connection_done(self):
4101 self.wfile_close()
4103 def makefile(self, mode, buffsize):
4104 if mode.startswith('w'):
4105 return self.wfile
4106 else:
4107 return self.rfile
4109 def close(self):
4110 pass
4112 def shutdown(self, how):
4113 pass
4116 class DevAppserverDispatcher(request_info._LocalFakeDispatcher):
4117 """A dev_appserver Dispatcher implementation."""
4119 def __init__(self, server, port):
4120 self._server = server
4121 self._port = port
4123 def add_event(self, runnable, eta, service=None, event_id=None):
4124 """Add a callable to be run at the specified time.
4126 Args:
4127 runnable: A callable object to call at the specified time.
4128 eta: An int containing the time to run the event, in seconds since the
4129 epoch.
4130 service: A str containing the name of the service that owns this event.
4131 This should be set if event_id is set.
4132 event_id: A str containing the id of the event. If set, this can be passed
4133 to update_event to change the time at which the event should run.
4135 self._server.AddEvent(eta, runnable, service, event_id)
4137 def update_event(self, eta, service, event_id):
4138 """Update the eta of a scheduled event.
4140 Args:
4141 eta: An int containing the time to run the event, in seconds since the
4142 epoch.
4143 service: A str containing the name of the service that owns this event.
4144 event_id: A str containing the id of the event to update.
4146 self._server.UpdateEvent(service, event_id, eta)
4148 def add_async_request(self, method, relative_url, headers, body, source_ip,
4149 server_name=None, version=None, instance_id=None):
4150 """Dispatch an HTTP request asynchronously.
4152 Args:
4153 method: A str containing the HTTP method of the request.
4154 relative_url: A str containing path and query string of the request.
4155 headers: A list of (key, value) tuples where key and value are both str.
4156 body: A str containing the request body.
4157 source_ip: The source ip address for the request.
4158 server_name: An optional str containing the server name to service this
4159 request. If unset, the request will be dispatched to the default
4160 server.
4161 version: An optional str containing the version to service this request.
4162 If unset, the request will be dispatched to the default version.
4163 instance_id: An optional str containing the instance_id of the instance to
4164 service this request. If unset, the request will be dispatched to
4165 according to the load-balancing for the server and version.
4167 fake_socket = FakeRequestSocket(method, relative_url, headers, body)
4168 self._server.AddEvent(0, lambda: (fake_socket, (source_ip, self._port)))
4170 def add_request(self, method, relative_url, headers, body, source_ip,
4171 server_name=None, version=None, instance_id=None):
4172 """Process an HTTP request.
4174 Args:
4175 method: A str containing the HTTP method of the request.
4176 relative_url: A str containing path and query string of the request.
4177 headers: A list of (key, value) tuples where key and value are both str.
4178 body: A str containing the request body.
4179 source_ip: The source ip address for the request.
4180 server_name: An optional str containing the server name to service this
4181 request. If unset, the request will be dispatched to the default
4182 server.
4183 version: An optional str containing the version to service this request.
4184 If unset, the request will be dispatched to the default version.
4185 instance_id: An optional str containing the instance_id of the instance to
4186 service this request. If unset, the request will be dispatched to
4187 according to the load-balancing for the server and version.
4189 Returns:
4190 A request_info.ResponseTuple containing the response information for the
4191 HTTP request.
4193 try:
4194 header_dict = wsgiref.headers.Headers(headers)
4195 connection_host = header_dict.get('host')
4196 connection = httplib.HTTPConnection(connection_host)
4199 connection.putrequest(
4200 method, relative_url,
4201 skip_host='host' in header_dict,
4202 skip_accept_encoding='accept-encoding' in header_dict)
4204 for header_key, header_value in headers:
4205 connection.putheader(header_key, header_value)
4206 connection.endheaders()
4207 connection.send(body)
4209 response = connection.getresponse()
4210 response.read()
4211 response.close()
4213 return request_info.ResponseTuple(
4214 '%d %s' % (response.status, response.reason), [], '')
4215 except (httplib.HTTPException, socket.error):
4216 logging.exception(
4217 'An error occured while sending a %s request to "%s%s"',
4218 method, connection_host, relative_url)
4219 return request_info.ResponseTuple('0', [], '')