1.9.30 sync.
[gae.git] / python / google / appengine / tools / old_dev_appserver.py
blob5491a59862d471e8e7bcefaa065b0775cb90bc77
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.
17 """Pure-Python application server for testing applications locally.
19 Given a port and the paths to a valid application directory (with an 'app.yaml'
20 file), the external library directory, and a relative URL to use for logins,
21 creates an HTTP server that can be used to test an application locally. Uses
22 stubs instead of actual APIs when SetupStubs() is called first.
24 Example:
25 root_path = '/path/to/application/directory'
26 login_url = '/login'
27 port = 8080
28 server = dev_appserver.CreateServer(root_path, login_url, port)
29 server.serve_forever()
30 """
32 from __future__ import with_statement
37 from google.appengine.tools import os_compat
39 import __builtin__
40 import BaseHTTPServer
41 import base64
42 import binascii
43 import calendar
44 import cStringIO
45 import cgi
46 import cgitb
47 import email.Utils
48 import errno
49 import hashlib
50 import heapq
51 import httplib
52 import imp
53 import inspect
54 import logging
55 import mimetools
56 import mimetypes
57 import os
58 import select
59 import shutil
60 import simplejson
61 import StringIO
62 import struct
63 import tempfile
64 import wsgiref.headers
65 import yaml
72 import re
73 import sre_compile
74 import sre_constants
75 import sre_parse
77 import socket
78 import sys
79 import time
80 import types
81 import urlparse
82 import urllib
83 import zlib
85 import google
89 try:
90 from google.third_party.apphosting.python.webapp2 import v2_3 as tmp
91 sys.path.append(os.path.dirname(tmp.__file__))
92 del tmp
93 except ImportError:
94 pass
96 from google.appengine.api import apiproxy_stub_map
97 from google.appengine.api import appinfo
98 from google.appengine.api import appinfo_includes
99 from google.appengine.api import app_logging
100 from google.appengine.api import blobstore
101 from google.appengine.api import croninfo
102 from google.appengine.api import datastore
103 from google.appengine.api import datastore_file_stub
104 from google.appengine.api import lib_config
105 from google.appengine.api import mail
106 from google.appengine.api import mail_stub
107 from google.appengine.api import namespace_manager
108 from google.appengine.api import request_info
109 from google.appengine.api import urlfetch_stub
110 from google.appengine.api import user_service_stub
111 from google.appengine.api import yaml_errors
112 from google.appengine.api.app_identity import app_identity_stub
113 from google.appengine.api.blobstore import blobstore_stub
114 from google.appengine.api.blobstore import file_blob_storage
115 from google.appengine.api.capabilities import capability_stub
116 from google.appengine.api.channel import channel_service_stub
117 from google.appengine.api.files import file_service_stub
118 from google.appengine.api.logservice import logservice
119 from google.appengine.api.logservice import logservice_stub
120 from google.appengine.api.search import simple_search_stub
121 from google.appengine.api.taskqueue import taskqueue_stub
122 from google.appengine.api.prospective_search import prospective_search_stub
123 from google.appengine.api.remote_socket import _remote_socket_stub
124 from google.appengine.api.memcache import memcache_stub
125 from google.appengine.api import rdbms_mysqldb
127 from google.appengine.api.system import system_stub
128 from google.appengine.api.xmpp import xmpp_service_stub
129 from google.appengine.datastore import datastore_sqlite_stub
130 from google.appengine.datastore import datastore_stub_util
131 from google.appengine.datastore import datastore_v4_stub
132 from google.appengine import dist
134 try:
135 from google.appengine.runtime import request_environment
136 from google.appengine.runtime import runtime
137 except:
139 request_environment = None
140 runtime = None
142 from google.appengine.tools import dev_appserver_apiserver
143 from google.appengine.tools import dev_appserver_blobimage
144 from google.appengine.tools import dev_appserver_blobstore
145 from google.appengine.tools import dev_appserver_channel
146 from google.appengine.tools import dev_appserver_import_hook
147 from google.appengine.tools import dev_appserver_login
148 from google.appengine.tools import dev_appserver_multiprocess as multiprocess
149 from google.appengine.tools import dev_appserver_oauth
150 from google.appengine.tools import dev_appserver_upload
152 from google.storage.speckle.python.api import rdbms
155 CouldNotFindModuleError = dev_appserver_import_hook.CouldNotFindModuleError
156 FakeAccess = dev_appserver_import_hook.FakeAccess
157 FakeFile = dev_appserver_import_hook.FakeFile
158 FakeReadlink = dev_appserver_import_hook.FakeReadlink
159 FakeSetLocale = dev_appserver_import_hook.FakeSetLocale
160 FakeUnlink = dev_appserver_import_hook.FakeUnlink
161 GetSubmoduleName = dev_appserver_import_hook.GetSubmoduleName
162 HardenedModulesHook = dev_appserver_import_hook.HardenedModulesHook
166 SDK_ROOT = dev_appserver_import_hook.SDK_ROOT
169 PYTHON_LIB_VAR = '$PYTHON_LIB'
170 DEVEL_CONSOLE_PATH = PYTHON_LIB_VAR + '/google/appengine/ext/admin'
171 REMOTE_API_PATH = (PYTHON_LIB_VAR +
172 '/google/appengine/ext/remote_api/handler.py')
175 FILE_MISSING_EXCEPTIONS = frozenset([errno.ENOENT, errno.ENOTDIR])
179 MAX_URL_LENGTH = 2047
183 DEFAULT_ENV = {
184 'GATEWAY_INTERFACE': 'CGI/1.1',
185 'AUTH_DOMAIN': 'gmail.com',
186 'USER_ORGANIZATION': '',
187 'TZ': 'UTC',
191 DEFAULT_SELECT_DELAY = 30.0
195 for ext, mime_type in mail.EXTENSION_MIME_MAP.iteritems():
196 mimetypes.add_type(mime_type, '.' + ext)
200 MAX_RUNTIME_RESPONSE_SIZE = 32 << 20
204 MAX_REQUEST_SIZE = 32 * 1024 * 1024
207 COPY_BLOCK_SIZE = 1 << 20
211 API_VERSION = '1'
216 VERSION_FILE = '../../VERSION'
221 DEVEL_PAYLOAD_HEADER = 'HTTP_X_APPENGINE_DEVELOPMENT_PAYLOAD'
222 DEVEL_PAYLOAD_RAW_HEADER = 'X-AppEngine-Development-Payload'
224 DEVEL_FAKE_IS_ADMIN_HEADER = 'HTTP_X_APPENGINE_FAKE_IS_ADMIN'
225 DEVEL_FAKE_IS_ADMIN_RAW_HEADER = 'X-AppEngine-Fake-Is-Admin'
227 FILE_STUB_DEPRECATION_MESSAGE = (
228 """The datastore file stub is deprecated, and
229 will stop being the default in a future release.
230 Append the --use_sqlite flag to use the new SQLite stub.
232 You can port your existing data using the --port_sqlite_data flag or
233 purge your previous test data with --clear_datastore.
234 """)
240 NON_PUBLIC_CACHE_CONTROLS = frozenset(['private', 'no-cache', 'no-store'])
244 class Error(Exception):
245 """Base-class for exceptions in this module."""
248 class InvalidAppConfigError(Error):
249 """The supplied application configuration file is invalid."""
252 class AppConfigNotFoundError(Error):
253 """Application configuration file not found."""
256 class CompileError(Error):
257 """Application could not be compiled."""
258 def __init__(self, text):
259 self.text = text
261 class ExecuteError(Error):
262 """Application could not be executed."""
263 def __init__(self, text, log):
264 self.text = text
265 self.log = log
269 def MonkeyPatchPdb(pdb):
270 """Given a reference to the pdb module, fix its set_trace function.
272 This will allow the standard trick of setting a breakpoint in your
273 code by inserting a call to pdb.set_trace() to work properly, as
274 long as the original stdin and stdout of dev_appserver.py are
275 connected to a console or shell window.
278 def NewSetTrace():
279 """Replacement for set_trace() that uses the original i/o streams.
281 This is necessary because by the time the user code that might
282 invoke pdb.set_trace() runs, the default sys.stdin and sys.stdout
283 are redirected to the HTTP request and response streams instead,
284 so that pdb will encounter garbage (or EOF) in its input, and its
285 output will garble the HTTP response. Fortunately, sys.__stdin__
286 and sys.__stderr__ retain references to the original streams --
287 this is a standard Python feature. Also, fortunately, as of
288 Python 2.5, the Pdb class lets you easily override stdin and
289 stdout. The original set_trace() function does essentially the
290 same thing as the code here except it instantiates Pdb() without
291 arguments.
293 p = pdb.Pdb(stdin=sys.__stdin__, stdout=sys.__stdout__)
294 p.set_trace(sys._getframe().f_back)
296 pdb.set_trace = NewSetTrace
299 def MonkeyPatchThreadingLocal(_threading_local):
300 """Given a reference to the _threading_local module, fix _localbase.__new__.
302 This ensures that using dev_appserver with a Python interpreter older than
303 2.7 will include the fix to the _threading_local._localbase.__new__ method
304 which was introduced in Python 2.7 (http://bugs.python.org/issue1522237).
307 @staticmethod
308 def New(cls, *args, **kw):
309 self = object.__new__(cls)
310 key = '_local__key', 'thread.local.' + str(id(self))
311 object.__setattr__(self, '_local__key', key)
312 object.__setattr__(self, '_local__args', (args, kw))
313 object.__setattr__(self, '_local__lock', _threading_local.RLock())
314 if (args or kw) and (cls.__init__ is object.__init__):
315 raise TypeError('Initialization arguments are not supported')
316 dict = object.__getattribute__(self, '__dict__')
317 _threading_local.current_thread().__dict__[key] = dict
318 return self
320 _threading_local._localbase.__new__ = New
323 def SplitURL(relative_url):
324 """Splits a relative URL into its path and query-string components.
326 Args:
327 relative_url: String containing the relative URL (often starting with '/')
328 to split. Should be properly escaped as www-form-urlencoded data.
330 Returns:
331 Tuple (script_name, query_string) where:
332 script_name: Relative URL of the script that was accessed.
333 query_string: String containing everything after the '?' character.
335 (unused_scheme, unused_netloc, path, query,
336 unused_fragment) = urlparse.urlsplit(relative_url)
337 return path, query
340 def GetFullURL(server_name, server_port, relative_url):
341 """Returns the full, original URL used to access the relative URL.
343 Args:
344 server_name: Name of the local host, or the value of the 'host' header
345 from the request.
346 server_port: Port on which the request was served (string or int).
347 relative_url: Relative URL that was accessed, including query string.
349 Returns:
350 String containing the original URL.
352 if str(server_port) != '80':
353 netloc = '%s:%s' % (server_name, server_port)
354 else:
355 netloc = server_name
356 return 'http://%s%s' % (netloc, relative_url)
358 def CopyStreamPart(source, destination, content_size):
359 """Copy a portion of a stream from one file-like object to another.
361 Args:
362 source: Source stream to copy from.
363 destination: Destination stream to copy to.
364 content_size: Maximum bytes to copy.
366 Returns:
367 Number of bytes actually copied.
369 bytes_copied = 0
370 bytes_left = content_size
371 while bytes_left > 0:
372 bytes = source.read(min(bytes_left, COPY_BLOCK_SIZE))
373 bytes_read = len(bytes)
374 if bytes_read == 0:
375 break
376 destination.write(bytes)
377 bytes_copied += bytes_read
378 bytes_left -= bytes_read
379 return bytes_copied
382 def AppIdWithDefaultPartition(app_id, default_partition):
383 """Add a partition to an application id if necessary."""
384 if not default_partition:
385 return app_id
389 if '~' in app_id:
390 return app_id
392 return default_partition + '~' + app_id
397 class AppServerRequest(object):
398 """Encapsulates app-server request.
400 Object used to hold a full appserver request. Used as a container that is
401 passed through the request forward chain and ultimately sent to the
402 URLDispatcher instances.
404 Attributes:
405 relative_url: String containing the URL accessed.
406 path: Local path of the resource that was matched; back-references will be
407 replaced by values matched in the relative_url. Path may be relative
408 or absolute, depending on the resource being served (e.g., static files
409 will have an absolute path; scripts will be relative).
410 headers: Instance of mimetools.Message with headers from the request.
411 infile: File-like object with input data from the request.
412 force_admin: Allow request admin-only URLs to proceed regardless of whether
413 user is logged in or is an admin.
416 ATTRIBUTES = ['relative_url',
417 'path',
418 'headers',
419 'infile',
420 'force_admin',
423 def __init__(self,
424 relative_url,
425 path,
426 headers,
427 infile,
428 force_admin=False):
429 """Constructor.
431 Args:
432 relative_url: Mapped directly to attribute.
433 path: Mapped directly to attribute.
434 headers: Mapped directly to attribute.
435 infile: Mapped directly to attribute.
436 force_admin: Mapped directly to attribute.
438 self.relative_url = relative_url
439 self.path = path
440 self.headers = headers
441 self.infile = infile
442 self.force_admin = force_admin
443 if (DEVEL_PAYLOAD_RAW_HEADER in self.headers or
444 DEVEL_FAKE_IS_ADMIN_RAW_HEADER in self.headers):
445 self.force_admin = True
447 def __eq__(self, other):
448 """Used mainly for testing.
450 Returns:
451 True if all fields of both requests are equal, else False.
453 if type(self) == type(other):
454 for attribute in self.ATTRIBUTES:
455 if getattr(self, attribute) != getattr(other, attribute):
456 return False
457 return True
459 def __repr__(self):
460 """String representation of request.
462 Used mainly for testing.
464 Returns:
465 String representation of AppServerRequest. Strings of different
466 request objects that have the same values for all fields compare
467 as equal.
469 results = []
470 for attribute in self.ATTRIBUTES:
471 results.append('%s: %s' % (attribute, getattr(self, attribute)))
472 return '<AppServerRequest %s>' % ' '.join(results)
475 class URLDispatcher(object):
476 """Base-class for handling HTTP requests."""
478 def Dispatch(self,
479 request,
480 outfile,
481 base_env_dict=None):
482 """Dispatch and handle an HTTP request.
484 base_env_dict should contain at least these CGI variables:
485 REQUEST_METHOD, REMOTE_ADDR, SERVER_SOFTWARE, SERVER_NAME,
486 SERVER_PROTOCOL, SERVER_PORT
488 Args:
489 request: AppServerRequest instance.
490 outfile: File-like object where output data should be written.
491 base_env_dict: Dictionary of CGI environment parameters if available.
492 Defaults to None.
494 Returns:
495 None if request handling is complete.
496 A new AppServerRequest instance if internal redirect is required.
498 raise NotImplementedError
500 def EndRedirect(self, dispatched_output, original_output):
501 """Process the end of an internal redirect.
503 This method is called after all subsequent dispatch requests have finished.
504 By default the output from the dispatched process is copied to the original.
506 This will not be called on dispatchers that do not return an internal
507 redirect.
509 Args:
510 dispatched_output: StringIO buffer containing the results from the
511 dispatched
512 original_output: The original output file.
514 Returns:
515 None if request handling is complete.
516 A new AppServerRequest instance if internal redirect is required.
518 original_output.write(dispatched_output.read())
521 class URLMatcher(object):
522 """Matches an arbitrary URL using a list of URL patterns from an application.
524 Each URL pattern has an associated URLDispatcher instance and path to the
525 resource's location on disk. See AddURL for more details. The first pattern
526 that matches an inputted URL will have its associated values returned by
527 Match().
530 def __init__(self):
531 """Initializer."""
535 self._url_patterns = []
537 def AddURL(self, regex, dispatcher, path, requires_login, admin_only,
538 auth_fail_action):
539 """Adds a URL pattern to the list of patterns.
541 If the supplied regex starts with a '^' or ends with a '$' an
542 InvalidAppConfigError exception will be raised. Start and end symbols
543 and implicitly added to all regexes, meaning we assume that all regexes
544 consume all input from a URL.
546 Args:
547 regex: String containing the regular expression pattern.
548 dispatcher: Instance of URLDispatcher that should handle requests that
549 match this regex.
550 path: Path on disk for the resource. May contain back-references like
551 r'\1', r'\2', etc, which will be replaced by the corresponding groups
552 matched by the regex if present.
553 requires_login: True if the user must be logged-in before accessing this
554 URL; False if anyone can access this URL.
555 admin_only: True if the user must be a logged-in administrator to
556 access the URL; False if anyone can access the URL.
557 auth_fail_action: either appinfo.AUTH_FAIL_ACTION_REDIRECT (default)
558 which indicates that the server should redirect to the login page when
559 an authentication is needed, or appinfo.AUTH_FAIL_ACTION_UNAUTHORIZED
560 which indicates that the server should just return a 401 Unauthorized
561 message immediately.
563 Raises:
564 TypeError: if dispatcher is not a URLDispatcher sub-class instance.
565 InvalidAppConfigError: if regex isn't valid.
567 if not isinstance(dispatcher, URLDispatcher):
568 raise TypeError('dispatcher must be a URLDispatcher sub-class')
570 if regex.startswith('^') or regex.endswith('$'):
571 raise InvalidAppConfigError('regex starts with "^" or ends with "$"')
573 adjusted_regex = '^%s$' % regex
575 try:
576 url_re = re.compile(adjusted_regex)
577 except re.error, e:
578 raise InvalidAppConfigError('regex invalid: %s' % e)
580 match_tuple = (url_re, dispatcher, path, requires_login, admin_only,
581 auth_fail_action)
582 self._url_patterns.append(match_tuple)
584 def Match(self,
585 relative_url,
586 split_url=SplitURL):
587 """Matches a URL from a request against the list of URL patterns.
589 The supplied relative_url may include the query string (i.e., the '?'
590 character and everything following).
592 Args:
593 relative_url: Relative URL being accessed in a request.
594 split_url: Used for dependency injection.
596 Returns:
597 Tuple (dispatcher, matched_path, requires_login, admin_only,
598 auth_fail_action), which are the corresponding values passed to
599 AddURL when the matching URL pattern was added to this matcher.
600 The matched_path will have back-references replaced using values
601 matched by the URL pattern. If no match was found, dispatcher will
602 be None.
605 adjusted_url, unused_query_string = split_url(relative_url)
607 for url_tuple in self._url_patterns:
608 url_re, dispatcher, path, requires_login, admin_only, auth_fail_action = url_tuple
609 the_match = url_re.match(adjusted_url)
611 if the_match:
612 adjusted_path = the_match.expand(path)
613 return (dispatcher, adjusted_path, requires_login, admin_only,
614 auth_fail_action)
616 return None, None, None, None, None
618 def GetDispatchers(self):
619 """Retrieves the URLDispatcher objects that could be matched.
621 Should only be used in tests.
623 Returns:
624 A set of URLDispatcher objects.
626 return set([url_tuple[1] for url_tuple in self._url_patterns])
631 class MatcherDispatcher(URLDispatcher):
632 """Dispatcher across multiple URLMatcher instances."""
634 def __init__(self,
635 config,
636 login_url,
637 module_manager,
638 url_matchers,
639 get_user_info=dev_appserver_login.GetUserInfo,
640 login_redirect=dev_appserver_login.LoginRedirect):
641 """Initializer.
643 Args:
644 config: AppInfoExternal instance representing the parsed app.yaml file.
645 login_url: Relative URL which should be used for handling user logins.
646 module_manager: ModuleManager instance that is used to detect and reload
647 modules if the matched Dispatcher is dynamic.
648 url_matchers: Sequence of URLMatcher objects.
649 get_user_info: Used for dependency injection.
650 login_redirect: Used for dependency injection.
652 self._config = config
653 self._login_url = login_url
654 self._module_manager = module_manager
655 self._url_matchers = tuple(url_matchers)
656 self._get_user_info = get_user_info
657 self._login_redirect = login_redirect
659 def Dispatch(self,
660 request,
661 outfile,
662 base_env_dict=None):
663 """Dispatches a request to the first matching dispatcher.
665 Matchers are checked in the order they were supplied to the constructor.
666 If no matcher matches, a 404 error will be written to the outfile. The
667 path variable supplied to this method is ignored.
669 The value of request.path is ignored.
671 cookies = ', '.join(request.headers.getheaders('cookie'))
672 email_addr, admin, user_id = self._get_user_info(cookies)
674 for matcher in self._url_matchers:
675 dispatcher, matched_path, requires_login, admin_only, auth_fail_action = matcher.Match(request.relative_url)
676 if dispatcher is None:
677 continue
679 logging.debug('Matched "%s" to %s with path %s',
680 request.relative_url, dispatcher, matched_path)
682 if ((requires_login or admin_only) and
683 not email_addr and
684 not request.force_admin):
685 logging.debug('Login required, redirecting user')
686 if auth_fail_action == appinfo.AUTH_FAIL_ACTION_REDIRECT:
687 self._login_redirect(self._login_url,
688 base_env_dict['SERVER_NAME'],
689 base_env_dict['SERVER_PORT'],
690 request.relative_url,
691 outfile)
692 elif auth_fail_action == appinfo.AUTH_FAIL_ACTION_UNAUTHORIZED:
693 outfile.write('Status: %d Not authorized\r\n'
694 '\r\n'
695 'Login required to view page.'
696 % (httplib.UNAUTHORIZED))
697 elif admin_only and not admin and not request.force_admin:
698 outfile.write('Status: %d Not authorized\r\n'
699 '\r\n'
700 'Current logged in user %s is not '
701 'authorized to view this page.'
702 % (httplib.FORBIDDEN, email_addr))
703 else:
704 request.path = matched_path
710 if (not isinstance(dispatcher, FileDispatcher) and
711 self._module_manager.AreModuleFilesModified()):
712 self._module_manager.ResetModules()
714 forward_request = dispatcher.Dispatch(request,
715 outfile,
716 base_env_dict=base_env_dict)
718 while forward_request:
720 logging.info('Internal redirection to %s',
721 forward_request.relative_url)
722 new_outfile = cStringIO.StringIO()
723 self.Dispatch(forward_request,
724 new_outfile,
725 dict(base_env_dict))
727 new_outfile.seek(0)
728 forward_request = dispatcher.EndRedirect(new_outfile, outfile)
731 return
733 outfile.write('Status: %d URL did not match\r\n'
734 '\r\n'
735 'Not found error: %s did not match any patterns '
736 'in application configuration.'
737 % (httplib.NOT_FOUND, request.relative_url))
743 _IGNORE_REQUEST_HEADERS = frozenset([
744 'accept-encoding',
745 'connection',
746 'keep-alive',
747 'proxy-authorization',
748 'te',
749 'trailer',
750 'transfer-encoding',
753 'content-type',
754 'content-length',
758 _request_id = 0
759 _request_time = 0
762 def _generate_request_id_hash():
763 """Generates a hash of the current request id."""
764 return hashlib.sha1(str(_request_id)).hexdigest()[:8].upper()
767 def _GenerateRequestLogId():
768 """Generates the request log id for the current request."""
769 sec = int(_request_time)
770 usec = int(1000000 * (_request_time - sec))
771 h = hashlib.sha1(str(_request_id)).digest()[:4]
772 packed = struct.Struct('> L L').pack(sec, usec)
773 return binascii.b2a_hex(packed + h)
776 def GetGoogleSqlOAuth2RefreshToken(oauth_file_path):
777 """Reads the user's Google Cloud SQL OAuth2.0 token from disk."""
778 if not os.path.exists(oauth_file_path):
779 return None
780 try:
781 with open(oauth_file_path) as oauth_file:
782 token = simplejson.load(oauth_file)
783 return token['refresh_token']
784 except (IOError, KeyError, simplejson.decoder.JSONDecodeError):
785 logging.exception(
786 'Could not read OAuth2.0 token from %s', oauth_file_path)
787 return None
790 def SetupEnvironment(cgi_path,
791 relative_url,
792 headers,
793 infile,
794 split_url=SplitURL,
795 get_user_info=dev_appserver_login.GetUserInfo):
796 """Sets up environment variables for a CGI.
798 Args:
799 cgi_path: Full file-system path to the CGI being executed.
800 relative_url: Relative URL used to access the CGI.
801 headers: Instance of mimetools.Message containing request headers.
802 infile: File-like object with input data from the request.
803 split_url, get_user_info: Used for dependency injection.
805 Returns:
806 Dictionary containing CGI environment variables.
808 env = DEFAULT_ENV.copy()
810 script_name, query_string = split_url(relative_url)
815 env['_AH_ENCODED_SCRIPT_NAME'] = script_name
816 env['SCRIPT_NAME'] = ''
817 env['QUERY_STRING'] = query_string
818 env['PATH_INFO'] = urllib.unquote(script_name)
819 env['PATH_TRANSLATED'] = cgi_path
820 env['CONTENT_TYPE'] = headers.getheader('content-type',
821 'application/x-www-form-urlencoded')
822 env['CONTENT_LENGTH'] = headers.getheader('content-length', '')
824 cookies = ', '.join(headers.getheaders('cookie'))
825 email_addr, admin, user_id = get_user_info(cookies)
826 env['USER_EMAIL'] = email_addr
827 env['USER_ID'] = user_id
828 if admin:
829 env['USER_IS_ADMIN'] = '1'
830 if env['AUTH_DOMAIN'] == '*':
832 auth_domain = 'gmail.com'
833 parts = email_addr.split('@')
834 if len(parts) == 2 and parts[1]:
835 auth_domain = parts[1]
836 env['AUTH_DOMAIN'] = auth_domain
838 env['REQUEST_LOG_ID'] = _GenerateRequestLogId()
839 env['REQUEST_ID_HASH'] = _generate_request_id_hash()
842 for key in headers:
843 if key in _IGNORE_REQUEST_HEADERS:
844 continue
845 adjusted_name = key.replace('-', '_').upper()
846 env['HTTP_' + adjusted_name] = ', '.join(headers.getheaders(key))
851 if DEVEL_PAYLOAD_HEADER in env:
852 del env[DEVEL_PAYLOAD_HEADER]
853 new_data = base64.standard_b64decode(infile.getvalue())
854 infile.seek(0)
855 infile.truncate()
856 infile.write(new_data)
857 infile.seek(0)
858 env['CONTENT_LENGTH'] = str(len(new_data))
862 if DEVEL_FAKE_IS_ADMIN_HEADER in env:
863 del env[DEVEL_FAKE_IS_ADMIN_HEADER]
865 token = GetGoogleSqlOAuth2RefreshToken(os.path.expanduser(
866 rdbms.OAUTH_CREDENTIALS_PATH))
867 if token:
868 env['GOOGLE_SQL_OAUTH2_REFRESH_TOKEN'] = token
870 return env
873 def NotImplementedFake(*args, **kwargs):
874 """Fake for methods/functions that are not implemented in the production
875 environment.
877 raise NotImplementedError('This class/method is not available.')
880 class NotImplementedFakeClass(object):
881 """Fake class for classes that are not implemented in the production env.
883 __init__ = NotImplementedFake
886 def IsEncodingsModule(module_name):
887 """Determines if the supplied module is related to encodings in any way.
889 Encodings-related modules cannot be reloaded, so they need to be treated
890 specially when sys.modules is modified in any way.
892 Args:
893 module_name: Absolute name of the module regardless of how it is imported
894 into the local namespace (e.g., foo.bar.baz).
896 Returns:
897 True if it's an encodings-related module; False otherwise.
899 if (module_name in ('codecs', 'encodings') or
900 module_name.startswith('encodings.')):
901 return True
902 return False
905 def ClearAllButEncodingsModules(module_dict):
906 """Clear all modules in a module dictionary except for those modules that
907 are in any way related to encodings.
909 Args:
910 module_dict: Dictionary in the form used by sys.modules.
912 for module_name in module_dict.keys():
915 if not IsEncodingsModule(module_name) and module_name != 'sys':
916 del module_dict[module_name]
919 def ConnectAndDisconnectChildModules(old_module_dict, new_module_dict):
920 """Prepares for switching from old_module_dict to new_module_dict.
922 Disconnects child modules going away from parents that remain, and reconnects
923 child modules that are being added back in to old parents. This is needed to
924 satisfy code that follows the getattr() descendant chain rather than looking
925 up the desired module directly in the module dict.
927 Args:
928 old_module_dict: The module dict being replaced, looks like sys.modules.
929 new_module_dict: The module dict takings its place, looks like sys.modules.
931 old_keys = set(old_module_dict.keys())
932 new_keys = set(new_module_dict.keys())
933 for deleted_module_name in old_keys - new_keys:
934 if old_module_dict[deleted_module_name] is None:
935 continue
936 segments = deleted_module_name.rsplit('.', 1)
937 if len(segments) == 2:
938 parent_module = new_module_dict.get(segments[0])
939 if parent_module and hasattr(parent_module, segments[1]):
940 delattr(parent_module, segments[1])
941 for added_module_name in new_keys - old_keys:
942 if new_module_dict[added_module_name] is None:
943 continue
944 segments = added_module_name.rsplit('.', 1)
945 if len(segments) == 2:
946 parent_module = old_module_dict.get(segments[0])
947 child_module = new_module_dict[added_module_name]
948 if (parent_module and
949 getattr(parent_module, segments[1], None) is not child_module):
950 setattr(parent_module, segments[1], child_module)
956 SHARED_MODULE_PREFIXES = set([
957 'google',
958 'logging',
959 'sys',
960 'warnings',
965 're',
966 'sre_compile',
967 'sre_constants',
968 'sre_parse',
971 'email',
976 'wsgiref',
978 'MySQLdb',
986 'decimal',
989 NOT_SHARED_MODULE_PREFIXES = set([
990 'google.appengine.ext',
994 def ModuleNameHasPrefix(module_name, prefix_set):
995 """Determines if a module's name belongs to a set of prefix strings.
997 Args:
998 module_name: String containing the fully qualified module name.
999 prefix_set: Iterable set of module name prefixes to check against.
1001 Returns:
1002 True if the module_name belongs to the prefix set or is a submodule of
1003 any of the modules specified in the prefix_set. Otherwise False.
1005 for prefix in prefix_set:
1006 if prefix == module_name:
1007 return True
1009 if module_name.startswith(prefix + '.'):
1010 return True
1012 return False
1015 def SetupSharedModules(module_dict):
1016 """Creates a module dictionary for the hardened part of the process.
1018 Module dictionary will contain modules that should be shared between the
1019 hardened and unhardened parts of the process.
1021 Args:
1022 module_dict: Module dictionary from which existing modules should be
1023 pulled (usually sys.modules).
1025 Returns:
1026 A new module dictionary.
1028 output_dict = {}
1029 for module_name, module in module_dict.iteritems():
1036 if module is None:
1037 continue
1039 if IsEncodingsModule(module_name):
1040 output_dict[module_name] = module
1041 continue
1043 shared_prefix = ModuleNameHasPrefix(module_name, SHARED_MODULE_PREFIXES)
1044 banned_prefix = ModuleNameHasPrefix(module_name, NOT_SHARED_MODULE_PREFIXES)
1046 if shared_prefix and not banned_prefix:
1047 output_dict[module_name] = module
1049 return output_dict
1055 def ModuleHasValidMainFunction(module):
1056 """Determines if a module has a main function that takes no arguments.
1058 This includes functions that have arguments with defaults that are all
1059 assigned, thus requiring no additional arguments in order to be called.
1061 Args:
1062 module: A types.ModuleType instance.
1064 Returns:
1065 True if the module has a valid, reusable main function; False otherwise.
1067 if hasattr(module, 'main') and type(module.main) is types.FunctionType:
1068 arg_names, var_args, var_kwargs, default_values = inspect.getargspec(
1069 module.main)
1070 if len(arg_names) == 0:
1071 return True
1072 if default_values is not None and len(arg_names) == len(default_values):
1073 return True
1074 return False
1077 def CheckScriptExists(cgi_path, handler_path):
1078 """Check that the given handler_path is a file that exists on disk.
1080 Args:
1081 cgi_path: Absolute path to the CGI script file on disk.
1082 handler_path: CGI path stored in the application configuration (as a path
1083 like 'foo/bar/baz.py'). May contain $PYTHON_LIB references.
1085 Raises:
1086 CouldNotFindModuleError: if the given handler_path is a file and doesn't
1087 have the expected extension.
1089 if handler_path.startswith(PYTHON_LIB_VAR + '/'):
1091 return
1093 if (not os.path.isdir(cgi_path) and
1094 not os.path.isfile(cgi_path) and
1095 os.path.isfile(cgi_path + '.py')):
1096 raise CouldNotFindModuleError(
1097 'Perhaps you meant to have the line "script: %s.py" in your app.yaml' %
1098 handler_path)
1101 def GetScriptModuleName(handler_path):
1102 """Determines the fully-qualified Python module name of a script on disk.
1104 Args:
1105 handler_path: CGI path stored in the application configuration (as a path
1106 like 'foo/bar/baz.py'). May contain $PYTHON_LIB references.
1108 Returns:
1109 String containing the corresponding module name (e.g., 'foo.bar.baz').
1111 if handler_path.startswith(PYTHON_LIB_VAR + '/'):
1112 handler_path = handler_path[len(PYTHON_LIB_VAR):]
1113 handler_path = os.path.normpath(handler_path)
1116 extension_index = handler_path.rfind('.py')
1117 if extension_index != -1:
1118 handler_path = handler_path[:extension_index]
1119 module_fullname = handler_path.replace(os.sep, '.')
1120 module_fullname = module_fullname.strip('.')
1121 module_fullname = re.sub('\.+', '.', module_fullname)
1125 if module_fullname.endswith('.__init__'):
1126 module_fullname = module_fullname[:-len('.__init__')]
1128 return module_fullname
1131 def FindMissingInitFiles(cgi_path, module_fullname, isfile=os.path.isfile):
1132 """Determines which __init__.py files are missing from a module's parent
1133 packages.
1135 Args:
1136 cgi_path: Absolute path of the CGI module file on disk.
1137 module_fullname: Fully qualified Python module name used to import the
1138 cgi_path module.
1139 isfile: Used for testing.
1141 Returns:
1142 List containing the paths to the missing __init__.py files.
1144 missing_init_files = []
1146 if cgi_path.endswith('.py'):
1147 module_base = os.path.dirname(cgi_path)
1148 else:
1149 module_base = cgi_path
1151 depth_count = module_fullname.count('.')
1157 if cgi_path.endswith('__init__.py') or not cgi_path.endswith('.py'):
1158 depth_count += 1
1160 for index in xrange(depth_count):
1163 current_init_file = os.path.abspath(
1164 os.path.join(module_base, '__init__.py'))
1166 if not isfile(current_init_file):
1167 missing_init_files.append(current_init_file)
1169 module_base = os.path.abspath(os.path.join(module_base, os.pardir))
1171 return missing_init_files
1174 def LoadTargetModule(handler_path,
1175 cgi_path,
1176 import_hook,
1177 module_dict=sys.modules):
1178 """Loads a target CGI script by importing it as a Python module.
1180 If the module for the target CGI script has already been loaded before,
1181 the new module will be loaded in its place using the same module object,
1182 possibly overwriting existing module attributes.
1184 Args:
1185 handler_path: CGI path stored in the application configuration (as a path
1186 like 'foo/bar/baz.py'). Should not have $PYTHON_LIB references.
1187 cgi_path: Absolute path to the CGI script file on disk.
1188 import_hook: Instance of HardenedModulesHook to use for module loading.
1189 module_dict: Used for dependency injection.
1191 Returns:
1192 Tuple (module_fullname, script_module, module_code) where:
1193 module_fullname: Fully qualified module name used to import the script.
1194 script_module: The ModuleType object corresponding to the module_fullname.
1195 If the module has not already been loaded, this will be an empty
1196 shell of a module.
1197 module_code: Code object (returned by compile built-in) corresponding
1198 to the cgi_path to run. If the script_module was previously loaded
1199 and has a main() function that can be reused, this will be None.
1201 Raises:
1202 CouldNotFindModuleError if the given handler_path is a file and doesn't have
1203 the expected extension.
1205 CheckScriptExists(cgi_path, handler_path)
1206 module_fullname = GetScriptModuleName(handler_path)
1207 script_module = module_dict.get(module_fullname)
1208 module_code = None
1209 if script_module is not None and ModuleHasValidMainFunction(script_module):
1213 logging.debug('Reusing main() function of module "%s"', module_fullname)
1214 else:
1221 if script_module is None:
1222 script_module = imp.new_module(module_fullname)
1223 script_module.__loader__ = import_hook
1226 try:
1227 module_code = import_hook.get_code(module_fullname)
1228 full_path, search_path, submodule = (
1229 import_hook.GetModuleInfo(module_fullname))
1230 script_module.__file__ = full_path
1231 if search_path is not None:
1232 script_module.__path__ = search_path
1233 except UnicodeDecodeError, e:
1237 error = ('%s please see http://www.python.org/peps'
1238 '/pep-0263.html for details (%s)' % (e, handler_path))
1239 raise SyntaxError(error)
1240 except:
1241 exc_type, exc_value, exc_tb = sys.exc_info()
1242 import_error_message = str(exc_type)
1243 if exc_value:
1244 import_error_message += ': ' + str(exc_value)
1252 logging.exception('Encountered error loading module "%s": %s',
1253 module_fullname, import_error_message)
1254 missing_inits = FindMissingInitFiles(cgi_path, module_fullname)
1255 if missing_inits:
1256 logging.warning('Missing package initialization files: %s',
1257 ', '.join(missing_inits))
1258 else:
1259 logging.error('Parent package initialization files are present, '
1260 'but must be broken')
1263 independent_load_successful = True
1265 if not os.path.isfile(cgi_path):
1270 independent_load_successful = False
1271 else:
1272 try:
1273 source_file = open(cgi_path)
1274 try:
1275 module_code = compile(source_file.read(), cgi_path, 'exec')
1276 script_module.__file__ = cgi_path
1277 finally:
1278 source_file.close()
1280 except OSError:
1284 independent_load_successful = False
1287 if not independent_load_successful:
1288 raise exc_type, exc_value, exc_tb
1293 module_dict[module_fullname] = script_module
1295 return module_fullname, script_module, module_code
1298 def _WriteErrorToOutput(status, message, outfile):
1299 """Writes an error status response to the response outfile.
1301 Args:
1302 status: The status to return, e.g. '411 Length Required'.
1303 message: A human-readable error message.
1304 outfile: Response outfile.
1306 logging.error(message)
1307 outfile.write('Status: %s\r\n\r\n%s' % (status, message))
1310 def GetRequestSize(request, env_dict, outfile):
1311 """Gets the size (content length) of the given request.
1313 On error, this method writes an error message to the response outfile and
1314 returns None. Errors include the request missing a required header and the
1315 request being too large.
1317 Args:
1318 request: AppServerRequest instance.
1319 env_dict: Environment dictionary. May be None.
1320 outfile: Response outfile.
1322 Returns:
1323 The calculated request size, or None on error.
1325 if 'content-length' in request.headers:
1326 request_size = int(request.headers['content-length'])
1327 elif env_dict and env_dict.get('REQUEST_METHOD', '') == 'POST':
1328 _WriteErrorToOutput('%d Length required' % httplib.LENGTH_REQUIRED,
1329 'POST requests require a Content-length header.',
1330 outfile)
1331 return None
1332 else:
1333 request_size = 0
1335 if request_size <= MAX_REQUEST_SIZE:
1336 return request_size
1337 else:
1338 msg = ('HTTP request was too large: %d. The limit is: %d.'
1339 % (request_size, MAX_REQUEST_SIZE))
1340 _WriteErrorToOutput(
1341 '%d Request entity too large' % httplib.REQUEST_ENTITY_TOO_LARGE,
1342 msg, outfile)
1343 return None
1346 def ExecuteOrImportScript(config, handler_path, cgi_path, import_hook):
1347 """Executes a CGI script by importing it as a new module.
1349 This possibly reuses the module's main() function if it is defined and
1350 takes no arguments.
1352 Basic technique lifted from PEP 338 and Python2.5's runpy module. See:
1353 http://www.python.org/dev/peps/pep-0338/
1355 See the section entitled "Import Statements and the Main Module" to understand
1356 why a module named '__main__' cannot do relative imports. To get around this,
1357 the requested module's path could be added to sys.path on each request.
1359 Args:
1360 config: AppInfoExternal instance representing the parsed app.yaml file.
1361 handler_path: CGI path stored in the application configuration (as a path
1362 like 'foo/bar/baz.py'). Should not have $PYTHON_LIB references.
1363 cgi_path: Absolute path to the CGI script file on disk.
1364 import_hook: Instance of HardenedModulesHook to use for module loading.
1366 Returns:
1367 True if the response code had an error status (e.g., 404), or False if it
1368 did not.
1370 Raises:
1371 Any kind of exception that could have been raised when loading the target
1372 module, running a target script, or executing the application code itself.
1374 module_fullname, script_module, module_code = LoadTargetModule(
1375 handler_path, cgi_path, import_hook)
1376 script_module.__name__ = '__main__'
1377 sys.modules['__main__'] = script_module
1378 try:
1380 import pdb
1381 MonkeyPatchPdb(pdb)
1384 if module_code:
1385 exec module_code in script_module.__dict__
1386 else:
1387 script_module.main()
1393 sys.stdout.flush()
1394 sys.stdout.seek(0)
1395 try:
1396 headers = mimetools.Message(sys.stdout)
1397 finally:
1400 sys.stdout.seek(0, 2)
1401 status_header = headers.get('status')
1402 error_response = False
1403 if status_header:
1404 try:
1405 status_code = int(status_header.split(' ', 1)[0])
1406 error_response = status_code >= 400
1407 except ValueError:
1408 error_response = True
1411 if not error_response:
1412 try:
1413 parent_package = import_hook.GetParentPackage(module_fullname)
1414 except Exception:
1415 parent_package = None
1417 if parent_package is not None:
1418 submodule = GetSubmoduleName(module_fullname)
1419 setattr(parent_package, submodule, script_module)
1421 return error_response
1422 finally:
1423 script_module.__name__ = module_fullname
1426 def ExecutePy27Handler(config, handler_path, cgi_path, import_hook):
1427 """Equivalent to ExecuteOrImportScript for Python 2.7 runtime.
1429 This dispatches to google.appengine.runtime.runtime,
1430 which in turn will dispatch to either the cgi or the wsgi module in
1431 the same package, depending on the form of handler_path.
1433 Args:
1434 config: AppInfoExternal instance representing the parsed app.yaml file.
1435 handler_path: handler ("script") from the application configuration;
1436 either a script reference like foo/bar.py, or an object reference
1437 like foo.bar.app.
1438 cgi_path: Absolute path to the CGI script file on disk;
1439 typically the app dir joined with handler_path.
1440 import_hook: Instance of HardenedModulesHook to use for module loading.
1442 Returns:
1443 True if the response code had an error status (e.g., 404), or False if it
1444 did not.
1446 Raises:
1447 Any kind of exception that could have been raised when loading the target
1448 module, running a target script, or executing the application code itself.
1450 if request_environment is None or runtime is None:
1451 raise RuntimeError('Python 2.5 is too old to emulate the Python 2.7 runtime.'
1452 ' Please use Python 2.6 or Python 2.7.')
1455 import os
1457 save_environ = os.environ
1458 save_getenv = os.getenv
1460 env = dict(save_environ)
1463 if env.get('_AH_THREADSAFE'):
1464 env['wsgi.multithread'] = True
1466 url = 'http://%s%s' % (env.get('HTTP_HOST', 'localhost:8080'),
1467 env.get('_AH_ENCODED_SCRIPT_NAME', '/'))
1468 qs = env.get('QUERY_STRING')
1469 if qs:
1470 url += '?' + qs
1473 post_data = sys.stdin.read()
1482 if 'CONTENT_TYPE' in env:
1483 if post_data:
1484 env['HTTP_CONTENT_TYPE'] = env['CONTENT_TYPE']
1485 del env['CONTENT_TYPE']
1486 if 'CONTENT_LENGTH' in env:
1487 if env['CONTENT_LENGTH']:
1488 env['HTTP_CONTENT_LENGTH'] = env['CONTENT_LENGTH']
1489 del env['CONTENT_LENGTH']
1491 if cgi_path.endswith(handler_path):
1492 application_root = cgi_path[:-len(handler_path)]
1493 if application_root.endswith('/') and application_root != '/':
1494 application_root = application_root[:-1]
1495 else:
1496 application_root = ''
1499 try:
1501 import pdb
1502 MonkeyPatchPdb(pdb)
1504 import _threading_local
1505 MonkeyPatchThreadingLocal(_threading_local)
1509 os.environ = request_environment.RequestLocalEnviron(
1510 request_environment.current_request)
1514 os.getenv = os.environ.get
1516 response = runtime.HandleRequest(env, handler_path, url,
1517 post_data, application_root, SDK_ROOT,
1518 import_hook)
1519 finally:
1521 os.environ = save_environ
1522 os.getenv = save_getenv
1526 error = response.get('error')
1527 if error:
1528 status = 500
1529 else:
1530 status = 200
1531 status = response.get('response_code', status)
1532 sys.stdout.write('Status: %s\r\n' % status)
1533 for key, value in response.get('headers', ()):
1536 key = '-'.join(key.split())
1537 value = value.replace('\r', ' ').replace('\n', ' ')
1538 sys.stdout.write('%s: %s\r\n' % (key, value))
1539 sys.stdout.write('\r\n')
1540 body = response.get('body')
1541 if body:
1542 sys.stdout.write(body)
1543 logs = response.get('logs')
1544 if logs:
1545 for timestamp_usec, severity, message in logs:
1547 logging.log(severity*10 + 10, '@%s: %s',
1548 time.ctime(timestamp_usec*1e-6), message)
1549 return error
1552 class LoggingStream(object):
1553 """A stream that writes logs at level error."""
1555 def write(self, message):
1558 logging.getLogger()._log(logging.ERROR, message, ())
1560 def writelines(self, lines):
1561 for line in lines:
1562 logging.getLogger()._log(logging.ERROR, line, ())
1564 def __getattr__(self, key):
1565 return getattr(sys.__stderr__, key)
1568 def ExecuteCGI(config,
1569 root_path,
1570 handler_path,
1571 cgi_path,
1572 env,
1573 infile,
1574 outfile,
1575 module_dict,
1576 exec_script=ExecuteOrImportScript,
1577 exec_py27_handler=ExecutePy27Handler):
1578 """Executes Python file in this process as if it were a CGI.
1580 Does not return an HTTP response line. CGIs should output headers followed by
1581 the body content.
1583 The modules in sys.modules should be the same before and after the CGI is
1584 executed, with the specific exception of encodings-related modules, which
1585 cannot be reloaded and thus must always stay in sys.modules.
1587 Args:
1588 config: AppInfoExternal instance representing the parsed app.yaml file.
1589 root_path: Path to the root of the application.
1590 handler_path: CGI path stored in the application configuration (as a path
1591 like 'foo/bar/baz.py'). May contain $PYTHON_LIB references.
1592 cgi_path: Absolute path to the CGI script file on disk.
1593 env: Dictionary of environment variables to use for the execution.
1594 infile: File-like object to read HTTP request input data from.
1595 outfile: FIle-like object to write HTTP response data to.
1596 module_dict: Dictionary in which application-loaded modules should be
1597 preserved between requests. This removes the need to reload modules that
1598 are reused between requests, significantly increasing load performance.
1599 This dictionary must be separate from the sys.modules dictionary.
1600 exec_script: Used for dependency injection.
1601 exec_py27_handler: Used for dependency injection.
1604 old_module_dict = sys.modules.copy()
1605 old_builtin = __builtin__.__dict__.copy()
1606 old_argv = sys.argv
1607 old_stdin = sys.stdin
1608 old_stdout = sys.stdout
1609 old_stderr = sys.stderr
1610 old_env = os.environ.copy()
1611 old_cwd = os.getcwd()
1612 old_file_type = types.FileType
1613 reset_modules = False
1614 app_log_handler = None
1616 try:
1617 ConnectAndDisconnectChildModules(sys.modules, module_dict)
1618 ClearAllButEncodingsModules(sys.modules)
1619 sys.modules.update(module_dict)
1620 sys.argv = [cgi_path]
1622 sys.stdin = cStringIO.StringIO(infile.getvalue())
1623 sys.stdout = outfile
1627 sys.stderr = LoggingStream()
1629 logservice._global_buffer = logservice.LogsBuffer()
1631 app_log_handler = app_logging.AppLogsHandler()
1632 logging.getLogger().addHandler(app_log_handler)
1634 os.environ.clear()
1635 os.environ.update(env)
1639 cgi_dir = os.path.normpath(os.path.dirname(cgi_path))
1640 root_path = os.path.normpath(os.path.abspath(root_path))
1641 if (cgi_dir.startswith(root_path + os.sep) and
1642 not (config and config.runtime == 'python27')):
1643 os.chdir(cgi_dir)
1644 else:
1645 os.chdir(root_path)
1647 dist.fix_paths(root_path, SDK_ROOT)
1652 hook = HardenedModulesHook(config, sys.modules, root_path)
1653 sys.meta_path = [finder for finder in sys.meta_path
1654 if not isinstance(finder, HardenedModulesHook)]
1655 sys.meta_path.insert(0, hook)
1656 if hasattr(sys, 'path_importer_cache'):
1657 sys.path_importer_cache.clear()
1660 __builtin__.file = FakeFile
1661 __builtin__.open = FakeFile
1662 types.FileType = FakeFile
1664 if not (config and config.runtime == 'python27'):
1666 __builtin__.buffer = NotImplementedFakeClass
1673 sys.modules['__builtin__'] = __builtin__
1675 logging.debug('Executing CGI with env:\n%s', repr(env))
1676 try:
1679 if handler_path and config and config.runtime == 'python27':
1680 reset_modules = exec_py27_handler(config, handler_path, cgi_path, hook)
1681 else:
1682 reset_modules = exec_script(config, handler_path, cgi_path, hook)
1683 except SystemExit, e:
1684 logging.debug('CGI exited with status: %s', e)
1685 except:
1686 reset_modules = True
1687 raise
1689 finally:
1690 sys.path_importer_cache.clear()
1692 _ClearTemplateCache(sys.modules)
1696 module_dict.update(sys.modules)
1697 ConnectAndDisconnectChildModules(sys.modules, old_module_dict)
1698 ClearAllButEncodingsModules(sys.modules)
1699 sys.modules.update(old_module_dict)
1701 __builtin__.__dict__.update(old_builtin)
1702 sys.argv = old_argv
1703 sys.stdin = old_stdin
1704 sys.stdout = old_stdout
1706 sys.stderr = old_stderr
1707 logging.getLogger().removeHandler(app_log_handler)
1709 os.environ.clear()
1710 os.environ.update(old_env)
1711 os.chdir(old_cwd)
1714 types.FileType = old_file_type
1717 class CGIDispatcher(URLDispatcher):
1718 """Dispatcher that executes Python CGI scripts."""
1720 def __init__(self,
1721 config,
1722 module_dict,
1723 root_path,
1724 path_adjuster,
1725 setup_env=SetupEnvironment,
1726 exec_cgi=ExecuteCGI):
1727 """Initializer.
1729 Args:
1730 config: AppInfoExternal instance representing the parsed app.yaml file.
1731 module_dict: Dictionary in which application-loaded modules should be
1732 preserved between requests. This dictionary must be separate from the
1733 sys.modules dictionary.
1734 path_adjuster: Instance of PathAdjuster to use for finding absolute
1735 paths of CGI files on disk.
1736 setup_env, exec_cgi: Used for dependency injection.
1738 self._config = config
1739 self._module_dict = module_dict
1740 self._root_path = root_path
1741 self._path_adjuster = path_adjuster
1742 self._setup_env = setup_env
1743 self._exec_cgi = exec_cgi
1745 def Dispatch(self,
1746 request,
1747 outfile,
1748 base_env_dict=None):
1749 """Dispatches the Python CGI."""
1750 request_size = GetRequestSize(request, base_env_dict, outfile)
1751 if request_size is None:
1752 return
1755 memory_file = cStringIO.StringIO()
1756 CopyStreamPart(request.infile, memory_file, request_size)
1757 memory_file.seek(0)
1759 before_level = logging.root.level
1760 try:
1761 env = {}
1764 if self._config.env_variables:
1765 env.update(self._config.env_variables)
1766 if base_env_dict:
1767 env.update(base_env_dict)
1768 cgi_path = self._path_adjuster.AdjustPath(request.path)
1769 env.update(self._setup_env(cgi_path,
1770 request.relative_url,
1771 request.headers,
1772 memory_file))
1773 self._exec_cgi(self._config,
1774 self._root_path,
1775 request.path,
1776 cgi_path,
1777 env,
1778 memory_file,
1779 outfile,
1780 self._module_dict)
1781 finally:
1782 logging.root.level = before_level
1784 def __str__(self):
1785 """Returns a string representation of this dispatcher."""
1786 return 'CGI dispatcher'
1789 class LocalCGIDispatcher(CGIDispatcher):
1790 """Dispatcher that executes local functions like they're CGIs.
1792 The contents of sys.modules will be preserved for local CGIs running this
1793 dispatcher, but module hardening will still occur for any new imports. Thus,
1794 be sure that any local CGIs have loaded all of their dependent modules
1795 _before_ they are executed.
1798 def __init__(self, config, module_dict, path_adjuster, cgi_func):
1799 """Initializer.
1801 Args:
1802 config: AppInfoExternal instance representing the parsed app.yaml file.
1803 module_dict: Passed to CGIDispatcher.
1804 path_adjuster: Passed to CGIDispatcher.
1805 cgi_func: Callable function taking no parameters that should be
1806 executed in a CGI environment in the current process.
1808 self._cgi_func = cgi_func
1810 def curried_exec_script(*args, **kwargs):
1811 cgi_func()
1812 return False
1814 def curried_exec_cgi(*args, **kwargs):
1815 kwargs['exec_script'] = curried_exec_script
1816 return ExecuteCGI(*args, **kwargs)
1818 CGIDispatcher.__init__(self,
1819 config,
1820 module_dict,
1822 path_adjuster,
1823 exec_cgi=curried_exec_cgi)
1825 def Dispatch(self, *args, **kwargs):
1826 """Preserves sys.modules for CGIDispatcher.Dispatch."""
1827 self._module_dict.update(sys.modules)
1828 CGIDispatcher.Dispatch(self, *args, **kwargs)
1830 def __str__(self):
1831 """Returns a string representation of this dispatcher."""
1832 return 'Local CGI dispatcher for %s' % self._cgi_func
1837 class PathAdjuster(object):
1838 """Adjusts application file paths to paths relative to the application or
1839 external library directories."""
1841 def __init__(self, root_path):
1842 """Initializer.
1844 Args:
1845 root_path: Path to the root of the application running on the server.
1847 self._root_path = os.path.abspath(root_path)
1849 def AdjustPath(self, path):
1850 """Adjusts application file paths to relative to the application.
1852 More precisely this method adjusts application file path to paths
1853 relative to the application or external library directories.
1855 Handler paths that start with $PYTHON_LIB will be converted to paths
1856 relative to the google directory.
1858 Args:
1859 path: File path that should be adjusted.
1861 Returns:
1862 The adjusted path.
1864 if path.startswith(PYTHON_LIB_VAR):
1865 path = os.path.join(SDK_ROOT, path[len(PYTHON_LIB_VAR) + 1:])
1866 else:
1867 path = os.path.join(self._root_path, path)
1869 return path
1874 class StaticFileConfigMatcher(object):
1875 """Keeps track of file/directory specific application configuration.
1877 Specifically:
1878 - Computes mime type based on URLMap and file extension.
1879 - Decides on cache expiration time based on URLMap and default expiration.
1880 - Decides what HTTP headers to add to responses.
1882 To determine the mime type, we first see if there is any mime-type property
1883 on each URLMap entry. If non is specified, we use the mimetypes module to
1884 guess the mime type from the file path extension, and use
1885 application/octet-stream if we can't find the mimetype.
1888 def __init__(self,
1889 url_map_list,
1890 default_expiration):
1891 """Initializer.
1893 Args:
1894 url_map_list: List of appinfo.URLMap objects.
1895 If empty or None, then we always use the mime type chosen by the
1896 mimetypes module.
1897 default_expiration: String describing default expiration time for browser
1898 based caching of static files. If set to None this disallows any
1899 browser caching of static content.
1901 if default_expiration is not None:
1902 self._default_expiration = appinfo.ParseExpiration(default_expiration)
1903 else:
1904 self._default_expiration = None
1907 self._patterns = []
1908 for url_map in url_map_list or []:
1910 handler_type = url_map.GetHandlerType()
1911 if handler_type not in (appinfo.STATIC_FILES, appinfo.STATIC_DIR):
1912 continue
1914 path_re = _StaticFilePathRe(url_map)
1915 try:
1916 self._patterns.append((re.compile(path_re), url_map))
1917 except re.error, e:
1918 raise InvalidAppConfigError('regex %s does not compile: %s' %
1919 (path_re, e))
1921 _DUMMY_URLMAP = appinfo.URLMap()
1923 def _FirstMatch(self, path):
1924 """Returns the first appinfo.URLMap that matches path, or a dummy instance.
1926 A dummy instance is returned when no appinfo.URLMap matches path (see the
1927 URLMap.static_file_path_re property). When a dummy instance is returned, it
1928 is always the same one. The dummy instance is constructed simply by doing
1929 the following:
1931 appinfo.URLMap()
1933 Args:
1934 path: A string containing the file's path relative to the app.
1936 Returns:
1937 The first appinfo.URLMap (in the list that was passed to the constructor)
1938 that matches path. Matching depends on whether URLMap is a static_dir
1939 handler or a static_files handler. In either case, matching is done
1940 according to the URLMap.static_file_path_re property.
1942 for path_re, url_map in self._patterns:
1943 if path_re.match(path):
1944 return url_map
1945 return StaticFileConfigMatcher._DUMMY_URLMAP
1947 def IsStaticFile(self, path):
1948 """Tests if the given path points to a "static" file.
1950 Args:
1951 path: A string containing the file's path relative to the app.
1953 Returns:
1954 Boolean, True if the file was configured to be static.
1956 return self._FirstMatch(path) is not self._DUMMY_URLMAP
1958 def GetMimeType(self, path):
1959 """Returns the mime type that we should use when serving the specified file.
1961 Args:
1962 path: A string containing the file's path relative to the app.
1964 Returns:
1965 String containing the mime type to use. Will be 'application/octet-stream'
1966 if we have no idea what it should be.
1968 url_map = self._FirstMatch(path)
1969 if url_map.mime_type is not None:
1970 return url_map.mime_type
1973 unused_filename, extension = os.path.splitext(path)
1974 return mimetypes.types_map.get(extension, 'application/octet-stream')
1976 def GetExpiration(self, path):
1977 """Returns the cache expiration duration to be users for the given file.
1979 Args:
1980 path: A string containing the file's path relative to the app.
1982 Returns:
1983 Integer number of seconds to be used for browser cache expiration time.
1986 if self._default_expiration is None:
1987 return 0
1989 url_map = self._FirstMatch(path)
1990 if url_map.expiration is None:
1991 return self._default_expiration
1993 return appinfo.ParseExpiration(url_map.expiration)
1995 def GetHttpHeaders(self, path):
1996 """Returns http_headers of the matching appinfo.URLMap, or an empty one.
1998 Args:
1999 path: A string containing the file's path relative to the app.
2001 Returns:
2002 A user-specified HTTP headers to be used in static content response. These
2003 headers are contained in an appinfo.HttpHeadersDict, which maps header
2004 names to values (both strings).
2006 return self._FirstMatch(path).http_headers or appinfo.HttpHeadersDict()
2012 def ReadDataFile(data_path, openfile=file):
2013 """Reads a file on disk, returning a corresponding HTTP status and data.
2015 Args:
2016 data_path: Path to the file on disk to read.
2017 openfile: Used for dependency injection.
2019 Returns:
2020 Tuple (status, data) where status is an HTTP response code, and data is
2021 the data read; will be an empty string if an error occurred or the
2022 file was empty.
2024 status = httplib.INTERNAL_SERVER_ERROR
2025 data = ""
2027 try:
2028 data_file = openfile(data_path, 'rb')
2029 try:
2030 data = data_file.read()
2031 finally:
2032 data_file.close()
2033 status = httplib.OK
2034 except (OSError, IOError), e:
2035 logging.error('Error encountered reading file "%s":\n%s', data_path, e)
2036 if e.errno in FILE_MISSING_EXCEPTIONS:
2037 status = httplib.NOT_FOUND
2038 else:
2039 status = httplib.FORBIDDEN
2041 return status, data
2044 class FileDispatcher(URLDispatcher):
2045 """Dispatcher that reads data files from disk."""
2047 def __init__(self,
2048 config,
2049 path_adjuster,
2050 static_file_config_matcher,
2051 read_data_file=ReadDataFile):
2052 """Initializer.
2054 Args:
2055 config: AppInfoExternal instance representing the parsed app.yaml file.
2056 path_adjuster: Instance of PathAdjuster to use for finding absolute
2057 paths of data files on disk.
2058 static_file_config_matcher: StaticFileConfigMatcher object.
2059 read_data_file: Used for dependency injection.
2061 self._config = config
2062 self._path_adjuster = path_adjuster
2063 self._static_file_config_matcher = static_file_config_matcher
2064 self._read_data_file = read_data_file
2066 def Dispatch(self, request, outfile, base_env_dict=None):
2067 """Reads the file and returns the response status and data."""
2068 full_path = self._path_adjuster.AdjustPath(request.path)
2069 status, data = self._read_data_file(full_path)
2070 content_type = self._static_file_config_matcher.GetMimeType(request.path)
2071 static_file = self._static_file_config_matcher.IsStaticFile(request.path)
2072 expiration = self._static_file_config_matcher.GetExpiration(request.path)
2073 current_etag = self.CreateEtag(data)
2074 if_match_etag = request.headers.get('if-match', None)
2075 if_none_match_etag = request.headers.get('if-none-match', '').split(',')
2077 http_headers = self._static_file_config_matcher.GetHttpHeaders(request.path)
2078 def WriteHeader(name, value):
2079 if http_headers.Get(name) is None:
2080 outfile.write('%s: %s\r\n' % (name, value))
2086 if if_match_etag and not self._CheckETagMatches(if_match_etag.split(','),
2087 current_etag,
2088 False):
2089 outfile.write('Status: %s\r\n' % httplib.PRECONDITION_FAILED)
2090 WriteHeader('ETag', current_etag)
2091 outfile.write('\r\n')
2092 elif self._CheckETagMatches(if_none_match_etag, current_etag, True):
2093 outfile.write('Status: %s\r\n' % httplib.NOT_MODIFIED)
2094 WriteHeader('ETag', current_etag)
2095 outfile.write('\r\n')
2096 else:
2100 outfile.write('Status: %d\r\n' % status)
2102 WriteHeader('Content-Type', content_type)
2105 if expiration:
2106 fmt = email.Utils.formatdate
2107 WriteHeader('Expires', fmt(time.time() + expiration, usegmt=True))
2108 WriteHeader('Cache-Control', 'public, max-age=%i' % expiration)
2111 if static_file:
2112 WriteHeader('ETag', '"%s"' % current_etag)
2114 for header in http_headers.iteritems():
2115 outfile.write('%s: %s\r\n' % header)
2117 outfile.write('\r\n')
2118 outfile.write(data)
2120 def __str__(self):
2121 """Returns a string representation of this dispatcher."""
2122 return 'File dispatcher'
2124 @staticmethod
2125 def CreateEtag(data):
2126 """Returns string of hash of file content, unique per URL."""
2127 data_crc = zlib.crc32(data)
2128 return base64.b64encode(str(data_crc))
2130 @staticmethod
2131 def _CheckETagMatches(supplied_etags, current_etag, allow_weak_match):
2132 """Checks if there is an entity tag match.
2134 Args:
2135 supplied_etags: list of input etags
2136 current_etag: the calculated etag for the entity
2137 allow_weak_match: Allow for weak tag comparison.
2139 Returns:
2140 True if there is a match, False otherwise.
2143 for tag in supplied_etags:
2144 if allow_weak_match and tag.startswith('W/'):
2145 tag = tag[2:]
2146 tag_data = tag.strip('"')
2147 if tag_data == '*' or tag_data == current_etag:
2148 return True
2149 return False
2158 _IGNORE_RESPONSE_HEADERS = frozenset([
2159 'connection',
2160 'content-encoding',
2161 'date',
2162 'keep-alive',
2163 'proxy-authenticate',
2164 'server',
2165 'trailer',
2166 'transfer-encoding',
2167 'upgrade',
2168 blobstore.BLOB_KEY_HEADER
2172 class AppServerResponse(object):
2173 """Development appserver response object.
2175 Object used to hold the full appserver response. Used as a container
2176 that is passed through the request rewrite chain and ultimately sent
2177 to the web client.
2179 Attributes:
2180 status_code: Integer HTTP response status (e.g., 200, 302, 404, 500)
2181 status_message: String containing an informational message about the
2182 response code, possibly derived from the 'status' header, if supplied.
2183 headers: mimetools.Message containing the HTTP headers of the response.
2184 body: File-like object containing the body of the response.
2185 large_response: Indicates that response is permitted to be larger than
2186 MAX_RUNTIME_RESPONSE_SIZE.
2190 __slots__ = ['status_code',
2191 'status_message',
2192 'headers',
2193 'body',
2194 'large_response']
2196 def __init__(self, response_file=None, **kwds):
2197 """Initializer.
2199 Args:
2200 response_file: A file-like object that contains the full response
2201 generated by the user application request handler. If present
2202 the headers and body are set from this value, although the values
2203 may be further overridden by the keyword parameters.
2204 kwds: All keywords are mapped to attributes of AppServerResponse.
2206 self.status_code = 200
2207 self.status_message = 'Good to go'
2208 self.large_response = False
2210 if response_file:
2211 self.SetResponse(response_file)
2212 else:
2213 self.headers = mimetools.Message(cStringIO.StringIO())
2214 self.body = None
2216 for name, value in kwds.iteritems():
2217 setattr(self, name, value)
2219 def SetResponse(self, response_file):
2220 """Sets headers and body from the response file.
2222 Args:
2223 response_file: File like object to set body and headers from.
2225 self.headers = mimetools.Message(response_file)
2226 self.body = response_file
2228 @property
2229 def header_data(self):
2230 """Get header data as a string.
2232 Returns:
2233 String representation of header with line breaks cleaned up.
2236 header_list = []
2237 for header in self.headers.headers:
2238 header = header.rstrip('\n\r')
2239 header_list.append(header)
2240 if not self.headers.getheader('Content-Type'):
2242 header_list.append('Content-Type: text/html')
2244 return '\r\n'.join(header_list) + '\r\n'
2247 def IgnoreHeadersRewriter(response):
2248 """Ignore specific response headers.
2250 Certain response headers cannot be modified by an Application. For a
2251 complete list of these headers please see:
2253 https://developers.google.com/appengine/docs/python/tools/webapp/responseclass#Disallowed_HTTP_Response_Headers
2255 This rewriter simply removes those headers.
2257 for h in _IGNORE_RESPONSE_HEADERS:
2258 if h in response.headers:
2259 del response.headers[h]
2262 def ValidHeadersRewriter(response):
2263 """Remove invalid response headers.
2265 Response headers must be printable ascii characters. This is enforced in
2266 production by http_proto.cc IsValidHeader.
2268 This rewriter will remove headers that contain non ascii characters.
2270 for (key, value) in response.headers.items():
2271 try:
2272 key.decode('ascii')
2273 value.decode('ascii')
2274 except UnicodeDecodeError:
2275 del response.headers[key]
2278 def ParseStatusRewriter(response):
2279 """Parse status header, if it exists.
2281 Handles the server-side 'status' header, which instructs the server to change
2282 the HTTP response code accordingly. Handles the 'location' header, which
2283 issues an HTTP 302 redirect to the client. Also corrects the 'content-length'
2284 header to reflect actual content length in case extra information has been
2285 appended to the response body.
2287 If the 'status' header supplied by the client is invalid, this method will
2288 set the response to a 500 with an error message as content.
2290 location_value = response.headers.getheader('location')
2291 status_value = response.headers.getheader('status')
2292 if status_value:
2293 response_status = status_value
2294 del response.headers['status']
2295 elif location_value:
2296 response_status = '%d Redirecting' % httplib.FOUND
2297 else:
2298 return response
2300 status_parts = response_status.split(' ', 1)
2301 response.status_code, response.status_message = (status_parts + [''])[:2]
2302 try:
2303 response.status_code = int(response.status_code)
2304 except ValueError:
2305 response.status_code = 500
2306 response.body = cStringIO.StringIO(
2307 'Error: Invalid "status" header value returned.')
2310 def GetAllHeaders(message, name):
2311 """Get all headers of a given name in a message.
2313 Args:
2314 message: A mimetools.Message object.
2315 name: The name of the header.
2317 Yields:
2318 A sequence of values of all headers with the given name.
2320 for header_line in message.getallmatchingheaders(name):
2321 yield header_line.split(':', 1)[1].strip()
2324 def CacheRewriter(response):
2325 """Update the cache header."""
2328 if response.status_code == httplib.NOT_MODIFIED:
2329 return
2331 if not 'Cache-Control' in response.headers:
2332 response.headers['Cache-Control'] = 'no-cache'
2333 if not 'Expires' in response.headers:
2334 response.headers['Expires'] = 'Fri, 01 Jan 1990 00:00:00 GMT'
2337 if 'Set-Cookie' in response.headers:
2341 current_date = time.time()
2342 expires = response.headers.get('Expires')
2343 reset_expires = True
2344 if expires:
2345 expires_time = email.Utils.parsedate(expires)
2346 if expires_time:
2347 reset_expires = calendar.timegm(expires_time) >= current_date
2348 if reset_expires:
2349 response.headers['Expires'] = time.strftime('%a, %d %b %Y %H:%M:%S GMT',
2350 time.gmtime(current_date))
2354 cache_directives = []
2355 for header in GetAllHeaders(response.headers, 'Cache-Control'):
2356 cache_directives.extend(v.strip() for v in header.split(','))
2357 cache_directives = [d for d in cache_directives if d != 'public']
2358 if not NON_PUBLIC_CACHE_CONTROLS.intersection(cache_directives):
2359 cache_directives.append('private')
2360 response.headers['Cache-Control'] = ', '.join(cache_directives)
2363 def _RemainingDataSize(input_buffer):
2364 """Computes how much data is remaining in the buffer.
2366 It leaves the buffer in its initial state.
2368 Args:
2369 input_buffer: a file-like object with seek and tell methods.
2371 Returns:
2372 integer representing how much data is remaining in the buffer.
2374 current_position = input_buffer.tell()
2375 input_buffer.seek(0, 2)
2376 remaining_data_size = input_buffer.tell() - current_position
2377 input_buffer.seek(current_position)
2378 return remaining_data_size
2381 def ContentLengthRewriter(response, request_headers, env_dict):
2382 """Rewrite the Content-Length header.
2384 Even though Content-Length is not a user modifiable header, App Engine
2385 sends a correct Content-Length to the user based on the actual response.
2388 if env_dict and env_dict.get('REQUEST_METHOD', '') == 'HEAD':
2389 return
2392 if response.status_code != httplib.NOT_MODIFIED:
2395 response.headers['Content-Length'] = str(_RemainingDataSize(response.body))
2396 elif 'Content-Length' in response.headers:
2397 del response.headers['Content-Length']
2400 def CreateResponseRewritersChain():
2401 """Create the default response rewriter chain.
2403 A response rewriter is the a function that gets a final chance to change part
2404 of the dev_appservers response. A rewriter is not like a dispatcher in that
2405 it is called after every request has been handled by the dispatchers
2406 regardless of which dispatcher was used.
2408 The order in which rewriters are registered will be the order in which they
2409 are used to rewrite the response. Modifications from earlier rewriters
2410 are used as input to later rewriters.
2412 A response rewriter is a function that can rewrite the request in any way.
2413 Thefunction can returned modified values or the original values it was
2414 passed.
2416 A rewriter function has the following parameters and return values:
2418 Args:
2419 status_code: Status code of response from dev_appserver or previous
2420 rewriter.
2421 status_message: Text corresponding to status code.
2422 headers: mimetools.Message instance with parsed headers. NOTE: These
2423 headers can contain its own 'status' field, but the default
2424 dev_appserver implementation will remove this. Future rewriters
2425 should avoid re-introducing the status field and return new codes
2426 instead.
2427 body: File object containing the body of the response. This position of
2428 this file may not be at the start of the file. Any content before the
2429 files position is considered not to be part of the final body.
2431 Returns:
2432 An AppServerResponse instance.
2434 Returns:
2435 List of response rewriters.
2437 rewriters = [ParseStatusRewriter,
2438 dev_appserver_blobstore.DownloadRewriter,
2439 IgnoreHeadersRewriter,
2440 ValidHeadersRewriter,
2441 CacheRewriter,
2442 ContentLengthRewriter,
2444 return rewriters
2448 def RewriteResponse(response_file,
2449 response_rewriters=None,
2450 request_headers=None,
2451 env_dict=None):
2452 """Allows final rewrite of dev_appserver response.
2454 This function receives the unparsed HTTP response from the application
2455 or internal handler, parses out the basic structure and feeds that structure
2456 in to a chain of response rewriters.
2458 It also makes sure the final HTTP headers are properly terminated.
2460 For more about response rewriters, please see documentation for
2461 CreateResponeRewritersChain.
2463 Args:
2464 response_file: File-like object containing the full HTTP response including
2465 the response code, all headers, and the request body.
2466 response_rewriters: A list of response rewriters. If none is provided it
2467 will create a new chain using CreateResponseRewritersChain.
2468 request_headers: Original request headers.
2469 env_dict: Environment dictionary.
2471 Returns:
2472 An AppServerResponse instance configured with the rewritten response.
2474 if response_rewriters is None:
2475 response_rewriters = CreateResponseRewritersChain()
2477 response = AppServerResponse(response_file)
2478 for response_rewriter in response_rewriters:
2481 if response_rewriter.func_code.co_argcount == 1:
2482 response_rewriter(response)
2483 elif response_rewriter.func_code.co_argcount == 2:
2484 response_rewriter(response, request_headers)
2485 else:
2486 response_rewriter(response, request_headers, env_dict)
2488 return response
2493 class ModuleManager(object):
2494 """Manages loaded modules in the runtime.
2496 Responsible for monitoring and reporting about file modification times.
2497 Modules can be loaded from source or precompiled byte-code files. When a
2498 file has source code, the ModuleManager monitors the modification time of
2499 the source file even if the module itself is loaded from byte-code.
2502 def __init__(self, modules):
2503 """Initializer.
2505 Args:
2506 modules: Dictionary containing monitored modules.
2508 self._modules = modules
2510 self._default_modules = self._modules.copy()
2512 self._save_path_hooks = sys.path_hooks[:]
2521 self._modification_times = {}
2524 self._dirty = True
2526 @staticmethod
2527 def GetModuleFile(module, is_file=os.path.isfile):
2528 """Helper method to try to determine modules source file.
2530 Args:
2531 module: Module object to get file for.
2532 is_file: Function used to determine if a given path is a file.
2534 Returns:
2535 Path of the module's corresponding Python source file if it exists, or
2536 just the module's compiled Python file. If the module has an invalid
2537 __file__ attribute, None will be returned.
2539 module_file = getattr(module, '__file__', None)
2540 if module_file is None:
2541 return None
2544 source_file = module_file[:module_file.rfind('py') + 2]
2546 if is_file(source_file):
2547 return source_file
2548 return module.__file__
2550 def AreModuleFilesModified(self):
2551 """Determines if any monitored files have been modified.
2553 Returns:
2554 True if one or more files have been modified, False otherwise.
2556 self._dirty = True
2557 for name, (mtime, fname) in self._modification_times.iteritems():
2559 if name not in self._modules:
2560 continue
2562 module = self._modules[name]
2564 try:
2566 if mtime != os.path.getmtime(fname):
2567 self._dirty = True
2568 return True
2569 except OSError, e:
2571 if e.errno in FILE_MISSING_EXCEPTIONS:
2572 self._dirty = True
2573 return True
2574 raise e
2576 return False
2578 def UpdateModuleFileModificationTimes(self):
2579 """Records the current modification times of all monitored modules."""
2580 if not self._dirty:
2581 return
2583 self._modification_times.clear()
2584 for name, module in self._modules.items():
2585 if not isinstance(module, types.ModuleType):
2586 continue
2587 module_file = self.GetModuleFile(module)
2588 if not module_file:
2589 continue
2590 try:
2591 self._modification_times[name] = (os.path.getmtime(module_file),
2592 module_file)
2593 except OSError, e:
2594 if e.errno not in FILE_MISSING_EXCEPTIONS:
2595 raise e
2597 self._dirty = False
2599 def ResetModules(self):
2600 """Clear modules so that when request is run they are reloaded."""
2601 lib_config._default_registry.reset()
2602 self._modules.clear()
2603 self._modules.update(self._default_modules)
2606 sys.path_hooks[:] = self._save_path_hooks
2609 sys.meta_path = []
2615 apiproxy_stub_map.apiproxy.GetPreCallHooks().Clear()
2616 apiproxy_stub_map.apiproxy.GetPostCallHooks().Clear()
2622 def GetVersionObject(isfile=os.path.isfile, open_fn=open):
2623 """Gets the version of the SDK by parsing the VERSION file.
2625 Args:
2626 isfile: used for testing.
2627 open_fn: Used for testing.
2629 Returns:
2630 A Yaml object or None if the VERSION file does not exist.
2632 version_filename = os.path.join(os.path.dirname(google.appengine.__file__),
2633 VERSION_FILE)
2634 if not isfile(version_filename):
2635 logging.error('Could not find version file at %s', version_filename)
2636 return None
2638 version_fh = open_fn(version_filename, 'r')
2639 try:
2640 version = yaml.safe_load(version_fh)
2641 finally:
2642 version_fh.close()
2644 return version
2649 def _ClearTemplateCache(module_dict=sys.modules):
2650 """Clear template cache in webapp.template module.
2652 Attempts to load template module. Ignores failure. If module loads, the
2653 template cache is cleared.
2655 Args:
2656 module_dict: Used for dependency injection.
2658 template_module = module_dict.get('google.appengine.ext.webapp.template')
2659 if template_module is not None:
2660 template_module.template_cache.clear()
2665 def CreateRequestHandler(root_path,
2666 login_url,
2667 static_caching=True,
2668 default_partition=None,
2669 interactive_console=True):
2670 """Creates a new BaseHTTPRequestHandler sub-class.
2672 This class will be used with the Python BaseHTTPServer module's HTTP server.
2674 Python's built-in HTTP server does not support passing context information
2675 along to instances of its request handlers. This function gets around that
2676 by creating a sub-class of the handler in a closure that has access to
2677 this context information.
2679 Args:
2680 root_path: Path to the root of the application running on the server.
2681 login_url: Relative URL which should be used for handling user logins.
2682 static_caching: True if browser caching of static files should be allowed.
2683 default_partition: Default partition to use in the application id.
2684 interactive_console: Whether to add the interactive console.
2686 Returns:
2687 Sub-class of BaseHTTPRequestHandler.
2709 application_module_dict = SetupSharedModules(sys.modules)
2712 application_config_cache = AppConfigCache()
2714 class DevAppServerRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
2715 """Dispatches URLs using patterns from a URLMatcher.
2717 The URLMatcher is created by loading an application's configuration file.
2718 Executes CGI scripts in the local process so the scripts can use mock
2719 versions of APIs.
2721 HTTP requests that correctly specify a user info cookie
2722 (dev_appserver_login.COOKIE_NAME) will have the 'USER_EMAIL' environment
2723 variable set accordingly. If the user is also an admin, the
2724 'USER_IS_ADMIN' variable will exist and be set to '1'. If the user is not
2725 logged in, 'USER_EMAIL' will be set to the empty string.
2727 On each request, raises an InvalidAppConfigError exception if the
2728 application configuration file in the directory specified by the root_path
2729 argument is invalid.
2731 server_version = 'Development/1.0'
2736 module_dict = application_module_dict
2737 module_manager = ModuleManager(application_module_dict)
2740 config_cache = application_config_cache
2742 rewriter_chain = CreateResponseRewritersChain()
2744 channel_poll_path_re = re.compile(
2745 dev_appserver_channel.CHANNEL_POLL_PATTERN)
2747 def __init__(self, *args, **kwargs):
2748 """Initializer.
2750 Args:
2751 args: Positional arguments passed to the superclass constructor.
2752 kwargs: Keyword arguments passed to the superclass constructor.
2754 self._log_record_writer = apiproxy_stub_map.apiproxy.GetStub('logservice')
2755 BaseHTTPServer.BaseHTTPRequestHandler.__init__(self, *args, **kwargs)
2757 def version_string(self):
2758 """Returns server's version string used for Server HTTP header."""
2760 return self.server_version
2762 def do_GET(self):
2763 """Handle GET requests."""
2764 if self._HasNoBody('GET'):
2765 self._HandleRequest()
2767 def do_POST(self):
2768 """Handles POST requests."""
2769 self._HandleRequest()
2771 def do_PUT(self):
2772 """Handle PUT requests."""
2773 self._HandleRequest()
2775 def do_HEAD(self):
2776 """Handle HEAD requests."""
2777 if self._HasNoBody('HEAD'):
2778 self._HandleRequest()
2780 def do_OPTIONS(self):
2781 """Handles OPTIONS requests."""
2782 self._HandleRequest()
2784 def do_DELETE(self):
2785 """Handle DELETE requests."""
2786 self._HandleRequest()
2788 def do_TRACE(self):
2789 """Handles TRACE requests."""
2790 if self._HasNoBody('TRACE'):
2791 self._HandleRequest()
2793 def _HasNoBody(self, method):
2794 """Check for request body in HTTP methods where no body is permitted.
2796 If a request body is present a 400 (Invalid request) response is sent.
2798 Args:
2799 method: The request method.
2801 Returns:
2802 True if no request body is present, False otherwise.
2806 content_length = int(self.headers.get('content-length', 0))
2807 if content_length:
2808 body = self.rfile.read(content_length)
2809 logging.warning('Request body in %s is not permitted: %s', method, body)
2810 self.send_response(httplib.BAD_REQUEST)
2811 return False
2812 return True
2814 def _Dispatch(self, dispatcher, socket_infile, outfile, env_dict):
2815 """Copy request data from socket and dispatch.
2817 Args:
2818 dispatcher: Dispatcher to handle request (MatcherDispatcher).
2819 socket_infile: Original request file stream.
2820 outfile: Output file to write response to.
2821 env_dict: Environment dictionary.
2825 request_descriptor, request_file_name = tempfile.mkstemp('.tmp',
2826 'request.')
2828 try:
2829 request_file = open(request_file_name, 'wb')
2830 try:
2831 CopyStreamPart(self.rfile,
2832 request_file,
2833 int(self.headers.get('content-length', 0)))
2834 finally:
2835 request_file.close()
2837 request_file = open(request_file_name, 'rb')
2838 try:
2839 app_server_request = AppServerRequest(self.path,
2840 None,
2841 self.headers,
2842 request_file)
2843 dispatcher.Dispatch(app_server_request,
2844 outfile,
2845 base_env_dict=env_dict)
2846 finally:
2847 request_file.close()
2848 finally:
2849 try:
2850 os.close(request_descriptor)
2854 try:
2855 os.remove(request_file_name)
2856 except OSError, err:
2857 if getattr(err, 'winerror', 0) == os_compat.ERROR_SHARING_VIOLATION:
2858 logging.warning('Failed removing %s', request_file_name)
2859 else:
2860 raise
2861 except OSError, err:
2862 if err.errno != errno.ENOENT:
2863 raise
2865 def _HandleRequest(self):
2866 """Handles any type of request and prints exceptions if they occur."""
2870 host_name = self.headers.get('host') or self.server.server_name
2871 server_name = host_name.split(':', 1)[0]
2873 env_dict = {
2874 'REQUEST_METHOD': self.command,
2875 'REMOTE_ADDR': self.client_address[0],
2876 'SERVER_SOFTWARE': self.server_version,
2877 'SERVER_NAME': server_name,
2878 'SERVER_PROTOCOL': self.protocol_version,
2879 'SERVER_PORT': str(self.server.server_port),
2882 full_url = GetFullURL(server_name, self.server.server_port, self.path)
2883 if len(full_url) > MAX_URL_LENGTH:
2884 msg = 'Requested URI too long: %s' % full_url
2885 logging.error(msg)
2886 self.send_response(httplib.REQUEST_URI_TOO_LONG, msg)
2887 return
2889 tbhandler = cgitb.Hook(file=self.wfile).handle
2890 try:
2892 config, explicit_matcher, from_cache = LoadAppConfig(
2893 root_path, self.module_dict, cache=self.config_cache,
2894 static_caching=static_caching, default_partition=default_partition)
2897 if not from_cache:
2898 self.module_manager.ResetModules()
2902 implicit_matcher = CreateImplicitMatcher(config,
2903 self.module_dict,
2904 root_path,
2905 login_url)
2907 if self.path.startswith('/_ah/admin'):
2910 if any((handler.url == '/_ah/datastore_admin.*'
2911 for handler in config.handlers)):
2912 self.headers['X-AppEngine-Datastore-Admin-Enabled'] = 'True'
2913 self.headers['X-AppEngine-Interactive-Console-Enabled'] = str(
2914 interactive_console)
2916 if config.api_version != API_VERSION:
2917 logging.error(
2918 "API versions cannot be switched dynamically: %r != %r",
2919 config.api_version, API_VERSION)
2920 sys.exit(1)
2922 (exclude, service_match) = ReservedPathFilter(
2923 config.inbound_services).ExcludePath(self.path)
2924 if exclude:
2925 logging.warning(
2926 'Request to %s excluded because %s is not enabled '
2927 'in inbound_services in app.yaml' % (self.path, service_match))
2928 self.send_response(httplib.NOT_FOUND)
2929 return
2931 if config.runtime == 'go':
2933 from google.appengine.ext import go
2934 go.APP_CONFIG = config
2936 version = GetVersionObject()
2937 env_dict['SDK_VERSION'] = version['release']
2938 env_dict['CURRENT_VERSION_ID'] = config.version + ".1"
2939 env_dict['APPLICATION_ID'] = config.application
2940 env_dict['DEFAULT_VERSION_HOSTNAME'] = self.server.frontend_hostport
2941 env_dict['APPENGINE_RUNTIME'] = config.runtime
2942 if config.runtime == 'python27' and config.threadsafe:
2943 env_dict['_AH_THREADSAFE'] = '1'
2947 global _request_time
2948 global _request_id
2949 _request_time = time.time()
2950 _request_id += 1
2952 request_id_hash = _generate_request_id_hash()
2953 env_dict['REQUEST_ID_HASH'] = request_id_hash
2954 os.environ['REQUEST_ID_HASH'] = request_id_hash
2957 multiprocess.GlobalProcess().UpdateEnv(env_dict)
2959 cookies = ', '.join(self.headers.getheaders('cookie'))
2960 email_addr, admin, user_id = dev_appserver_login.GetUserInfo(cookies)
2962 self._log_record_writer.start_request(
2963 request_id=None,
2964 user_request_id=_GenerateRequestLogId(),
2965 ip=env_dict['REMOTE_ADDR'],
2966 app_id=env_dict['APPLICATION_ID'],
2967 version_id=env_dict['CURRENT_VERSION_ID'],
2968 nickname=email_addr.split('@')[0],
2969 user_agent=self.headers.get('user-agent', ''),
2970 host=host_name,
2971 method=self.command,
2972 resource=self.path,
2973 http_version=self.request_version)
2975 dispatcher = MatcherDispatcher(config, login_url, self.module_manager,
2976 [implicit_matcher, explicit_matcher])
2981 if multiprocess.GlobalProcess().HandleRequest(self):
2982 return
2984 outfile = cStringIO.StringIO()
2985 try:
2986 self._Dispatch(dispatcher, self.rfile, outfile, env_dict)
2987 finally:
2988 self.module_manager.UpdateModuleFileModificationTimes()
2990 outfile.flush()
2991 outfile.seek(0)
2993 response = RewriteResponse(outfile, self.rewriter_chain, self.headers,
2994 env_dict)
2996 runtime_response_size = _RemainingDataSize(response.body)
2997 if self.command == 'HEAD' and runtime_response_size > 0:
2998 logging.warning('Dropping unexpected body in response to HEAD '
2999 'request')
3000 response.body = cStringIO.StringIO('')
3001 elif (not response.large_response and
3002 runtime_response_size > MAX_RUNTIME_RESPONSE_SIZE):
3003 logging.error('Response too large: %d, max is %d',
3004 runtime_response_size, MAX_RUNTIME_RESPONSE_SIZE)
3007 response.status_code = 500
3008 response.status_message = 'Forbidden'
3010 new_response = ('HTTP response was too large: %d. '
3011 'The limit is: %d.'
3012 % (runtime_response_size,
3013 MAX_RUNTIME_RESPONSE_SIZE))
3014 response.headers['Content-Length'] = str(len(new_response))
3015 response.body = cStringIO.StringIO(new_response)
3018 multiprocess.GlobalProcess().RequestComplete(self, response)
3020 except yaml_errors.EventListenerError, e:
3021 title = 'Fatal error when loading application configuration'
3022 msg = '%s:\n%s' % (title, str(e))
3023 logging.error(msg)
3024 self.send_response(httplib.INTERNAL_SERVER_ERROR, title)
3025 self.wfile.write('Content-Type: text/html\r\n\r\n')
3026 self.wfile.write('<pre>%s</pre>' % cgi.escape(msg))
3027 except KeyboardInterrupt, e:
3031 logging.info('Server interrupted by user, terminating')
3032 self.server.stop_serving_forever()
3033 except CompileError, e:
3034 msg = 'Compile error:\n' + e.text + '\n'
3035 logging.error(msg)
3036 self.send_response(httplib.INTERNAL_SERVER_ERROR, 'Compile error')
3037 self.wfile.write('Content-Type: text/plain; charset=utf-8\r\n\r\n')
3038 self.wfile.write(msg)
3039 except ExecuteError, e:
3040 logging.error(e.text)
3041 self.send_response(httplib.INTERNAL_SERVER_ERROR, 'Execute error')
3042 self.wfile.write('Content-Type: text/html; charset=utf-8\r\n\r\n')
3043 self.wfile.write('<title>App failure</title>\n')
3044 self.wfile.write(e.text + '\n<pre>\n')
3045 for l in e.log:
3046 self.wfile.write(cgi.escape(l))
3047 self.wfile.write('</pre>\n')
3048 except:
3049 msg = 'Exception encountered handling request'
3050 logging.exception(msg)
3051 self.send_response(httplib.INTERNAL_SERVER_ERROR, msg)
3052 tbhandler()
3053 else:
3054 try:
3055 self.send_response(response.status_code, response.status_message)
3056 self.wfile.write(response.header_data)
3057 self.wfile.write('\r\n')
3059 shutil.copyfileobj(response.body, self.wfile, COPY_BLOCK_SIZE)
3060 except (IOError, OSError), e:
3071 if e.errno not in [errno.EPIPE, os_compat.WSAECONNABORTED]:
3072 raise e
3073 except socket.error, e:
3074 if len(e.args) >= 1 and e.args[0] != errno.EPIPE:
3075 raise e
3077 def log_error(self, format, *args):
3078 """Redirect error messages through the logging module."""
3079 logging.error(format, *args)
3081 def log_message(self, format, *args):
3082 """Redirect log messages through the logging module."""
3085 if hasattr(self, 'path') and self.channel_poll_path_re.match(self.path):
3086 logging.debug(format, *args)
3087 else:
3088 logging.info(format, *args)
3090 def log_request(self, code='-', size='-'):
3091 """Indicate that this request has completed."""
3092 BaseHTTPServer.BaseHTTPRequestHandler.log_request(self, code, size)
3093 if code == '-':
3094 code = 0
3095 if size == '-':
3096 size = 0
3099 logservice.logs_buffer().flush()
3100 self._log_record_writer.end_request(None, code, size)
3101 return DevAppServerRequestHandler
3106 def ReadAppConfig(appinfo_path, parse_app_config=appinfo_includes.Parse):
3107 """Reads app.yaml file and returns its app id and list of URLMap instances.
3109 Args:
3110 appinfo_path: String containing the path to the app.yaml file.
3111 parse_app_config: Used for dependency injection.
3113 Returns:
3114 AppInfoExternal instance.
3116 Raises:
3117 If the config file could not be read or the config does not contain any
3118 URLMap instances, this function will raise an InvalidAppConfigError
3119 exception.
3121 try:
3122 appinfo_file = file(appinfo_path, 'r')
3123 except IOError, unused_e:
3124 raise InvalidAppConfigError(
3125 'Application configuration could not be read from "%s"' % appinfo_path)
3126 try:
3129 return parse_app_config(appinfo_file)
3130 finally:
3131 appinfo_file.close()
3134 def _StaticFilePathRe(url_map):
3135 """Returns a regular expression string that matches static file paths.
3137 Args:
3138 url_map: A fully initialized static_files or static_dir appinfo.URLMap
3139 instance.
3141 Returns:
3142 The regular expression matches paths, relative to the application's root
3143 directory, of files that this static handler serves. re.compile should
3144 accept the returned string.
3146 Raises:
3147 AssertionError: The url_map argument was not an URLMap for a static handler.
3149 handler_type = url_map.GetHandlerType()
3152 if handler_type == 'static_files':
3153 return url_map.upload + '$'
3155 elif handler_type == 'static_dir':
3156 path = url_map.static_dir.rstrip(os.path.sep)
3157 return path + re.escape(os.path.sep) + r'(.*)'
3159 assert False, 'This property only applies to static handlers.'
3162 def CreateURLMatcherFromMaps(config,
3163 root_path,
3164 url_map_list,
3165 module_dict,
3166 default_expiration,
3167 create_url_matcher=URLMatcher,
3168 create_cgi_dispatcher=CGIDispatcher,
3169 create_file_dispatcher=FileDispatcher,
3170 create_path_adjuster=PathAdjuster,
3171 normpath=os.path.normpath):
3172 """Creates a URLMatcher instance from URLMap.
3174 Creates all of the correct URLDispatcher instances to handle the various
3175 content types in the application configuration.
3177 Args:
3178 config: AppInfoExternal instance representing the parsed app.yaml file.
3179 root_path: Path to the root of the application running on the server.
3180 url_map_list: List of appinfo.URLMap objects to initialize this
3181 matcher with. Can be an empty list if you would like to add patterns
3182 manually or use config.handlers as a default.
3183 module_dict: Dictionary in which application-loaded modules should be
3184 preserved between requests. This dictionary must be separate from the
3185 sys.modules dictionary.
3186 default_expiration: String describing default expiration time for browser
3187 based caching of static files. If set to None this disallows any
3188 browser caching of static content.
3189 create_url_matcher: Used for dependency injection.
3190 create_cgi_dispatcher: Used for dependency injection.
3191 create_file_dispatcher: Used for dependency injection.
3192 create_path_adjuster: Used for dependency injection.
3193 normpath: Used for dependency injection.
3195 Returns:
3196 Instance of URLMatcher with the supplied URLMap objects properly loaded.
3198 Raises:
3199 InvalidAppConfigError: if a handler is an unknown type.
3201 if config and config.handlers and not url_map_list:
3202 url_map_list = config.handlers
3203 url_matcher = create_url_matcher()
3204 path_adjuster = create_path_adjuster(root_path)
3205 cgi_dispatcher = create_cgi_dispatcher(config, module_dict,
3206 root_path, path_adjuster)
3207 static_file_config_matcher = StaticFileConfigMatcher(url_map_list,
3208 default_expiration)
3209 file_dispatcher = create_file_dispatcher(config, path_adjuster,
3210 static_file_config_matcher)
3212 FakeFile.SetStaticFileConfigMatcher(static_file_config_matcher)
3214 for url_map in url_map_list:
3215 admin_only = url_map.login == appinfo.LOGIN_ADMIN
3216 requires_login = url_map.login == appinfo.LOGIN_REQUIRED or admin_only
3217 auth_fail_action = url_map.auth_fail_action
3219 handler_type = url_map.GetHandlerType()
3220 if handler_type == appinfo.HANDLER_SCRIPT:
3221 dispatcher = cgi_dispatcher
3222 elif handler_type in (appinfo.STATIC_FILES, appinfo.STATIC_DIR):
3223 dispatcher = file_dispatcher
3224 else:
3226 raise InvalidAppConfigError('Unknown handler type "%s"' % handler_type)
3229 regex = url_map.url
3230 path = url_map.GetHandler()
3231 if handler_type == appinfo.STATIC_DIR:
3232 if regex[-1] == r'/':
3233 regex = regex[:-1]
3234 if path[-1] == os.path.sep:
3235 path = path[:-1]
3236 regex = '/'.join((re.escape(regex), '(.*)'))
3237 if os.path.sep == '\\':
3238 backref = r'\\1'
3239 else:
3240 backref = r'\1'
3241 path = (normpath(path).replace('\\', '\\\\') +
3242 os.path.sep + backref)
3244 url_matcher.AddURL(regex,
3245 dispatcher,
3246 path,
3247 requires_login, admin_only, auth_fail_action)
3249 return url_matcher
3252 class AppConfigCache(object):
3253 """Cache used by LoadAppConfig.
3255 If given to LoadAppConfig instances of this class are used to cache contents
3256 of the app config (app.yaml or app.yml) and the Matcher created from it.
3258 Code outside LoadAppConfig should treat instances of this class as opaque
3259 objects and not access its members.
3263 path = None
3268 mtime = None
3270 config = None
3272 matcher = None
3275 def LoadAppConfig(root_path,
3276 module_dict,
3277 cache=None,
3278 static_caching=True,
3279 read_app_config=ReadAppConfig,
3280 create_matcher=CreateURLMatcherFromMaps,
3281 default_partition=None):
3282 """Creates a Matcher instance for an application configuration file.
3284 Raises an InvalidAppConfigError exception if there is anything wrong with
3285 the application configuration file.
3287 Args:
3288 root_path: Path to the root of the application to load.
3289 module_dict: Dictionary in which application-loaded modules should be
3290 preserved between requests. This dictionary must be separate from the
3291 sys.modules dictionary.
3292 cache: Instance of AppConfigCache or None.
3293 static_caching: True if browser caching of static files should be allowed.
3294 read_app_config: Used for dependency injection.
3295 create_matcher: Used for dependency injection.
3296 default_partition: Default partition to use for the appid.
3298 Returns:
3299 tuple: (AppInfoExternal, URLMatcher, from_cache)
3301 Raises:
3302 AppConfigNotFound: if an app.yaml file cannot be found.
3304 for appinfo_path in [os.path.join(root_path, 'app.yaml'),
3305 os.path.join(root_path, 'app.yml')]:
3307 if os.path.isfile(appinfo_path):
3308 if cache is not None:
3310 mtime = os.path.getmtime(appinfo_path)
3311 if cache.path == appinfo_path and cache.mtime == mtime:
3312 return (cache.config, cache.matcher, True)
3315 cache.config = cache.matcher = cache.path = None
3316 cache.mtime = mtime
3318 config = read_app_config(appinfo_path, appinfo_includes.Parse)
3320 if config.application:
3321 config.application = AppIdWithDefaultPartition(config.application,
3322 default_partition)
3323 multiprocess.GlobalProcess().NewAppInfo(config)
3325 if static_caching:
3326 if config.default_expiration:
3327 default_expiration = config.default_expiration
3328 else:
3331 default_expiration = '0'
3332 else:
3334 default_expiration = None
3336 matcher = create_matcher(config,
3337 root_path,
3338 config.handlers,
3339 module_dict,
3340 default_expiration)
3342 FakeFile.SetSkippedFiles(config.skip_files)
3344 if cache is not None:
3345 cache.path = appinfo_path
3346 cache.config = config
3347 cache.matcher = matcher
3349 return config, matcher, False
3351 raise AppConfigNotFoundError(
3352 'Could not find app.yaml in "%s".' % (root_path,))
3355 class ReservedPathFilter():
3356 """Checks a path against a set of inbound_services."""
3358 reserved_paths = {
3359 '/_ah/channel/connect': 'channel_presence',
3360 '/_ah/channel/disconnect': 'channel_presence'
3363 def __init__(self, inbound_services):
3364 self.inbound_services = inbound_services
3366 def ExcludePath(self, path):
3367 """Check to see if this is a service url and matches inbound_services."""
3368 skip = False
3369 for reserved_path in self.reserved_paths.keys():
3370 if path.startswith(reserved_path):
3371 if (not self.inbound_services or
3372 self.reserved_paths[reserved_path] not in self.inbound_services):
3373 return (True, self.reserved_paths[reserved_path])
3375 return (False, None)
3378 def CreateInboundServiceFilter(inbound_services):
3379 return ReservedPathFilter(inbound_services)
3382 def ReadCronConfig(croninfo_path, parse_cron_config=croninfo.LoadSingleCron):
3383 """Reads cron.yaml file and returns a list of CronEntry instances.
3385 Args:
3386 croninfo_path: String containing the path to the cron.yaml file.
3387 parse_cron_config: Used for dependency injection.
3389 Returns:
3390 A CronInfoExternal object.
3392 Raises:
3393 If the config file is unreadable, empty or invalid, this function will
3394 raise an InvalidAppConfigError or a MalformedCronConfiguration exception.
3396 try:
3397 croninfo_file = file(croninfo_path, 'r')
3398 except IOError, e:
3399 raise InvalidAppConfigError(
3400 'Cron configuration could not be read from "%s": %s'
3401 % (croninfo_path, e))
3402 try:
3403 return parse_cron_config(croninfo_file)
3404 finally:
3405 croninfo_file.close()
3410 def _RemoveFile(file_path):
3411 if file_path and os.path.lexists(file_path):
3412 logging.info('Attempting to remove file at %s', file_path)
3413 try:
3414 os.remove(file_path)
3415 except OSError, e:
3416 logging.warning('Removing file failed: %s', e)
3419 def SetupStubs(app_id, **config):
3420 """Sets up testing stubs of APIs.
3422 Args:
3423 app_id: Application ID being served.
3424 config: keyword arguments.
3426 Keywords:
3427 root_path: Root path to the directory of the application which should
3428 contain the app.yaml, index.yaml, and queue.yaml files.
3429 login_url: Relative URL which should be used for handling user login/logout.
3430 blobstore_path: Path to the directory to store Blobstore blobs in.
3431 datastore_path: Path to the file to store Datastore file stub data in.
3432 prospective_search_path: Path to the file to store Prospective Search stub
3433 data in.
3434 use_sqlite: Use the SQLite stub for the datastore.
3435 auto_id_policy: How datastore stub assigns IDs, sequential or scattered.
3436 high_replication: Use the high replication consistency model
3437 history_path: DEPRECATED, No-op.
3438 clear_datastore: If the datastore should be cleared on startup.
3439 smtp_host: SMTP host used for sending test mail.
3440 smtp_port: SMTP port.
3441 smtp_user: SMTP user.
3442 smtp_password: SMTP password.
3443 mysql_host: MySQL host.
3444 mysql_port: MySQL port.
3445 mysql_user: MySQL user.
3446 mysql_password: MySQL password.
3447 mysql_socket: MySQL socket.
3448 appidentity_email_address: Email address for service account substitute.
3449 appidentity_private_key_path: Path to private key for service account sub.
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 appidentity_email_address = config.get('appidentity_email_address', None)
3494 appidentity_private_key_path = config.get('appidentity_private_key_path', None)
3495 remove = config.get('remove', os.remove)
3496 disable_task_running = config.get('disable_task_running', False)
3497 task_retry_seconds = config.get('task_retry_seconds', 30)
3498 logs_path = config.get('logs_path', ':memory:')
3499 trusted = config.get('trusted', False)
3500 serve_port = config.get('port', 8080)
3501 serve_address = config.get('address', 'localhost')
3502 clear_search_index = config.get('clear_search_indexes', False)
3503 search_index_path = config.get('search_indexes_path', None)
3504 _use_atexit_for_datastore_stub = config.get('_use_atexit_for_datastore_stub',
3505 False)
3506 port_sqlite_data = config.get('port_sqlite_data', False)
3512 os.environ['APPLICATION_ID'] = app_id
3516 os.environ['REQUEST_ID_HASH'] = ''
3518 if clear_prospective_search and prospective_search_path:
3519 _RemoveFile(prospective_search_path)
3521 if clear_datastore:
3522 _RemoveFile(datastore_path)
3524 if clear_search_index:
3525 _RemoveFile(search_index_path)
3528 if multiprocess.GlobalProcess().MaybeConfigureRemoteDataApis():
3532 apiproxy_stub_map.apiproxy.RegisterStub(
3533 'logservice',
3534 logservice_stub.LogServiceStub(logs_path=':memory:'))
3535 else:
3542 apiproxy_stub_map.apiproxy = apiproxy_stub_map.APIProxyStubMap()
3544 apiproxy_stub_map.apiproxy.RegisterStub(
3545 'app_identity_service',
3546 app_identity_stub.AppIdentityServiceStub.Create(
3547 email_address=appidentity_email_address,
3548 private_key_path=appidentity_private_key_path))
3550 apiproxy_stub_map.apiproxy.RegisterStub(
3551 'capability_service',
3552 capability_stub.CapabilityServiceStub())
3554 if use_sqlite:
3555 if port_sqlite_data:
3556 try:
3557 PortAllEntities(datastore_path)
3558 except Error:
3559 logging.Error("Porting the data from the datastore file stub failed")
3560 raise
3562 datastore = datastore_sqlite_stub.DatastoreSqliteStub(
3563 app_id, datastore_path, require_indexes=require_indexes,
3564 trusted=trusted, root_path=root_path,
3565 use_atexit=_use_atexit_for_datastore_stub,
3566 auto_id_policy=auto_id_policy)
3567 else:
3568 logging.warning(FILE_STUB_DEPRECATION_MESSAGE)
3569 datastore = datastore_file_stub.DatastoreFileStub(
3570 app_id, datastore_path, require_indexes=require_indexes,
3571 trusted=trusted, root_path=root_path,
3572 use_atexit=_use_atexit_for_datastore_stub,
3573 auto_id_policy=auto_id_policy)
3575 if high_replication:
3576 datastore.SetConsistencyPolicy(
3577 datastore_stub_util.TimeBasedHRConsistencyPolicy())
3578 apiproxy_stub_map.apiproxy.ReplaceStub(
3579 'datastore_v3', datastore)
3581 apiproxy_stub_map.apiproxy.RegisterStub(
3582 'datastore_v4',
3583 datastore_v4_stub.DatastoreV4Stub(app_id))
3585 apiproxy_stub_map.apiproxy.RegisterStub(
3586 'mail',
3587 mail_stub.MailServiceStub(smtp_host,
3588 smtp_port,
3589 smtp_user,
3590 smtp_password,
3591 enable_sendmail=enable_sendmail,
3592 show_mail_body=show_mail_body,
3593 allow_tls=False))
3595 apiproxy_stub_map.apiproxy.RegisterStub(
3596 'memcache',
3597 memcache_stub.MemcacheServiceStub())
3599 apiproxy_stub_map.apiproxy.RegisterStub(
3600 'taskqueue',
3601 taskqueue_stub.TaskQueueServiceStub(
3602 root_path=root_path,
3603 auto_task_running=(not disable_task_running),
3604 task_retry_seconds=task_retry_seconds,
3605 default_http_server='%s:%s' % (serve_address, serve_port)))
3607 apiproxy_stub_map.apiproxy.RegisterStub(
3608 'urlfetch',
3609 urlfetch_stub.URLFetchServiceStub())
3611 apiproxy_stub_map.apiproxy.RegisterStub(
3612 'xmpp',
3613 xmpp_service_stub.XmppServiceStub())
3615 apiproxy_stub_map.apiproxy.RegisterStub(
3616 'logservice',
3617 logservice_stub.LogServiceStub(logs_path=logs_path))
3622 from google.appengine import api
3623 sys.modules['google.appengine.api.rdbms'] = rdbms_mysqldb
3624 api.rdbms = rdbms_mysqldb
3625 rdbms_mysqldb.SetConnectKwargs(host=mysql_host, port=mysql_port,
3626 user=mysql_user, passwd=mysql_password,
3627 unix_socket=mysql_socket)
3629 fixed_login_url = '%s?%s=%%s' % (login_url,
3630 dev_appserver_login.CONTINUE_PARAM)
3631 fixed_logout_url = '%s&%s' % (fixed_login_url,
3632 dev_appserver_login.LOGOUT_PARAM)
3638 apiproxy_stub_map.apiproxy.RegisterStub(
3639 'user',
3640 user_service_stub.UserServiceStub(login_url=fixed_login_url,
3641 logout_url=fixed_logout_url))
3643 apiproxy_stub_map.apiproxy.RegisterStub(
3644 'channel',
3645 channel_service_stub.ChannelServiceStub())
3647 apiproxy_stub_map.apiproxy.RegisterStub(
3648 'matcher',
3649 prospective_search_stub.ProspectiveSearchStub(
3650 prospective_search_path,
3651 apiproxy_stub_map.apiproxy.GetStub('taskqueue')))
3653 apiproxy_stub_map.apiproxy.RegisterStub(
3654 'remote_socket',
3655 _remote_socket_stub.RemoteSocketServiceStub())
3657 apiproxy_stub_map.apiproxy.RegisterStub(
3658 'search',
3659 simple_search_stub.SearchServiceStub(index_file=search_index_path))
3665 try:
3666 from google.appengine.api.images import images_stub
3667 host_prefix = 'http://%s:%d' % (serve_address, serve_port)
3668 apiproxy_stub_map.apiproxy.RegisterStub(
3669 'images',
3670 images_stub.ImagesServiceStub(host_prefix=host_prefix))
3671 except ImportError, e:
3673 from google.appengine.api.images import images_not_implemented_stub
3674 apiproxy_stub_map.apiproxy.RegisterStub(
3675 'images',
3676 images_not_implemented_stub.ImagesNotImplementedServiceStub())
3678 blob_storage = file_blob_storage.FileBlobStorage(blobstore_path, app_id)
3679 apiproxy_stub_map.apiproxy.RegisterStub(
3680 'blobstore',
3681 blobstore_stub.BlobstoreServiceStub(blob_storage))
3683 apiproxy_stub_map.apiproxy.RegisterStub(
3684 'file',
3685 file_service_stub.FileServiceStub(blob_storage))
3687 system_service_stub = system_stub.SystemServiceStub()
3688 multiprocess.GlobalProcess().UpdateSystemStub(system_service_stub)
3689 apiproxy_stub_map.apiproxy.RegisterStub('system', system_service_stub)
3692 def TearDownStubs():
3693 """Clean up any stubs that need cleanup."""
3695 datastore_stub = apiproxy_stub_map.apiproxy.GetStub('datastore_v3')
3698 if isinstance(datastore_stub, datastore_stub_util.BaseTransactionManager):
3699 logging.info('Applying all pending transactions and saving the datastore')
3700 datastore_stub.Write()
3702 search_stub = apiproxy_stub_map.apiproxy.GetStub('search')
3703 if isinstance(search_stub, simple_search_stub.SearchServiceStub):
3704 logging.info('Saving search indexes')
3705 search_stub.Write()
3708 def CreateImplicitMatcher(
3709 config,
3710 module_dict,
3711 root_path,
3712 login_url,
3713 create_path_adjuster=PathAdjuster,
3714 create_local_dispatcher=LocalCGIDispatcher,
3715 create_cgi_dispatcher=CGIDispatcher,
3716 get_blob_storage=dev_appserver_blobstore.GetBlobStorage):
3717 """Creates a URLMatcher instance that handles internal URLs.
3719 Used to facilitate handling user login/logout, debugging, info about the
3720 currently running app, quitting the dev appserver, etc.
3722 Args:
3723 config: AppInfoExternal instance representing the parsed app.yaml file.
3724 module_dict: Dictionary in the form used by sys.modules.
3725 root_path: Path to the root of the application.
3726 login_url: Relative URL which should be used for handling user login/logout.
3727 create_path_adjuster: Used for dependedency injection.
3728 create_local_dispatcher: Used for dependency injection.
3729 create_cgi_dispatcher: Used for dependedency injection.
3730 get_blob_storage: Used for dependency injection.
3732 Returns:
3733 Instance of URLMatcher with appropriate dispatchers.
3735 url_matcher = URLMatcher()
3736 path_adjuster = create_path_adjuster(root_path)
3741 def _HandleQuit():
3742 raise KeyboardInterrupt
3743 quit_dispatcher = create_local_dispatcher(config, sys.modules, path_adjuster,
3744 _HandleQuit)
3745 url_matcher.AddURL('/_ah/quit?',
3746 quit_dispatcher,
3748 False,
3749 False,
3750 appinfo.AUTH_FAIL_ACTION_REDIRECT)
3755 login_dispatcher = create_local_dispatcher(config, sys.modules, path_adjuster,
3756 dev_appserver_login.main)
3757 url_matcher.AddURL(login_url,
3758 login_dispatcher,
3760 False,
3761 False,
3762 appinfo.AUTH_FAIL_ACTION_REDIRECT)
3764 admin_dispatcher = create_cgi_dispatcher(config, module_dict, root_path,
3765 path_adjuster)
3766 url_matcher.AddURL('/_ah/admin(?:/.*)?',
3767 admin_dispatcher,
3768 DEVEL_CONSOLE_PATH,
3769 False,
3770 False,
3771 appinfo.AUTH_FAIL_ACTION_REDIRECT)
3773 upload_dispatcher = dev_appserver_blobstore.CreateUploadDispatcher(
3774 get_blob_storage)
3776 url_matcher.AddURL(dev_appserver_blobstore.UPLOAD_URL_PATTERN,
3777 upload_dispatcher,
3779 False,
3780 False,
3781 appinfo.AUTH_FAIL_ACTION_UNAUTHORIZED)
3783 blobimage_dispatcher = dev_appserver_blobimage.CreateBlobImageDispatcher(
3784 apiproxy_stub_map.apiproxy.GetStub('images'))
3785 url_matcher.AddURL(dev_appserver_blobimage.BLOBIMAGE_URL_PATTERN,
3786 blobimage_dispatcher,
3788 False,
3789 False,
3790 appinfo.AUTH_FAIL_ACTION_UNAUTHORIZED)
3792 oauth_dispatcher = dev_appserver_oauth.CreateOAuthDispatcher()
3794 url_matcher.AddURL(dev_appserver_oauth.OAUTH_URL_PATTERN,
3795 oauth_dispatcher,
3797 False,
3798 False,
3799 appinfo.AUTH_FAIL_ACTION_UNAUTHORIZED)
3801 channel_dispatcher = dev_appserver_channel.CreateChannelDispatcher(
3802 apiproxy_stub_map.apiproxy.GetStub('channel'))
3804 url_matcher.AddURL(dev_appserver_channel.CHANNEL_POLL_PATTERN,
3805 channel_dispatcher,
3807 False,
3808 False,
3809 appinfo.AUTH_FAIL_ACTION_UNAUTHORIZED)
3811 url_matcher.AddURL(dev_appserver_channel.CHANNEL_JSAPI_PATTERN,
3812 channel_dispatcher,
3814 False,
3815 False,
3816 appinfo.AUTH_FAIL_ACTION_UNAUTHORIZED)
3818 apiserver_dispatcher = dev_appserver_apiserver.CreateApiserverDispatcher()
3819 url_matcher.AddURL(dev_appserver_apiserver.API_SERVING_PATTERN,
3820 apiserver_dispatcher,
3822 False,
3823 False,
3824 appinfo.AUTH_FAIL_ACTION_UNAUTHORIZED)
3826 return url_matcher
3829 def FetchAllEntitites():
3830 """Returns all datastore entities from all namespaces as a list."""
3831 ns = list(datastore.Query('__namespace__').Run())
3832 original_ns = namespace_manager.get_namespace()
3833 entities_set = []
3834 for namespace in ns:
3835 namespace_manager.set_namespace(namespace.key().name())
3836 kinds_list = list(datastore.Query('__kind__').Run())
3837 for kind_entity in kinds_list:
3838 ents = list(datastore.Query(kind_entity.key().name()).Run())
3839 for ent in ents:
3840 entities_set.append(ent)
3841 namespace_manager.set_namespace(original_ns)
3842 return entities_set
3845 def PutAllEntities(entities):
3846 """Puts all entities to the current datastore."""
3847 for entity in entities:
3848 datastore.Put(entity)
3851 def PortAllEntities(datastore_path):
3852 """Copies entities from a DatastoreFileStub to an SQLite stub.
3854 Args:
3855 datastore_path: Path to the file to store Datastore file stub data is.
3858 previous_stub = apiproxy_stub_map.apiproxy.GetStub('datastore_v3')
3860 try:
3861 app_id = os.environ['APPLICATION_ID']
3862 apiproxy_stub_map.apiproxy = apiproxy_stub_map.APIProxyStubMap()
3863 datastore_stub = datastore_file_stub.DatastoreFileStub(
3864 app_id, datastore_path, trusted=True)
3865 apiproxy_stub_map.apiproxy.RegisterStub('datastore_v3', datastore_stub)
3867 entities = FetchAllEntitites()
3868 sqlite_datastore_stub = datastore_sqlite_stub.DatastoreSqliteStub(app_id,
3869 datastore_path + '.sqlite', trusted=True)
3870 apiproxy_stub_map.apiproxy.ReplaceStub('datastore_v3',
3871 sqlite_datastore_stub)
3872 PutAllEntities(entities)
3873 sqlite_datastore_stub.Close()
3874 finally:
3875 apiproxy_stub_map.apiproxy.ReplaceStub('datastore_v3', previous_stub)
3877 shutil.copy(datastore_path, datastore_path + '.filestub')
3878 _RemoveFile(datastore_path)
3879 shutil.move(datastore_path + '.sqlite', datastore_path)
3882 def CreateServer(root_path,
3883 login_url,
3884 port,
3885 template_dir=None,
3886 serve_address='',
3887 allow_skipped_files=False,
3888 static_caching=True,
3889 python_path_list=sys.path,
3890 sdk_dir=SDK_ROOT,
3891 default_partition=None,
3892 frontend_port=None,
3893 interactive_console=True):
3894 """Creates a new HTTPServer for an application.
3896 The sdk_dir argument must be specified for the directory storing all code for
3897 the SDK so as to allow for the sandboxing of module access to work for any
3898 and all SDK code. While typically this is where the 'google' package lives,
3899 it can be in another location because of API version support.
3901 Args:
3902 root_path: String containing the path to the root directory of the
3903 application where the app.yaml file is.
3904 login_url: Relative URL which should be used for handling user login/logout.
3905 port: Port to start the application server on.
3906 template_dir: Unused.
3907 serve_address: Address on which the server should serve.
3908 allow_skipped_files: True if skipped files should be accessible.
3909 static_caching: True if browser caching of static files should be allowed.
3910 python_path_list: Used for dependency injection.
3911 sdk_dir: Directory where the SDK is stored.
3912 default_partition: Default partition to use for the appid.
3913 frontend_port: A frontend port (so backends can return an address for a
3914 frontend). If None, port will be used.
3915 interactive_console: Whether to add the interactive console.
3917 Returns:
3918 Instance of BaseHTTPServer.HTTPServer that's ready to start accepting.
3925 absolute_root_path = os.path.realpath(root_path)
3927 FakeFile.SetAllowedPaths(absolute_root_path,
3928 [sdk_dir])
3929 FakeFile.SetAllowSkippedFiles(allow_skipped_files)
3931 handler_class = CreateRequestHandler(absolute_root_path,
3932 login_url,
3933 static_caching,
3934 default_partition,
3935 interactive_console)
3938 if absolute_root_path not in python_path_list:
3941 python_path_list.insert(0, absolute_root_path)
3943 if multiprocess.Enabled():
3944 server = HttpServerWithMultiProcess((serve_address, port), handler_class)
3945 else:
3946 server = HTTPServerWithScheduler((serve_address, port), handler_class)
3950 queue_stub = apiproxy_stub_map.apiproxy.GetStub('taskqueue')
3951 if queue_stub and hasattr(queue_stub, 'StartBackgroundExecution'):
3952 queue_stub.StartBackgroundExecution()
3954 request_info._local_dispatcher = DevAppserverDispatcher(server,
3955 frontend_port or port)
3956 server.frontend_hostport = '%s:%d' % (serve_address or 'localhost',
3957 frontend_port or port)
3959 return server
3962 class HTTPServerWithScheduler(BaseHTTPServer.HTTPServer):
3963 """A BaseHTTPServer subclass that calls a method at a regular interval."""
3965 def __init__(self, server_address, request_handler_class):
3966 """Constructor.
3968 Args:
3969 server_address: the bind address of the server.
3970 request_handler_class: class used to handle requests.
3972 BaseHTTPServer.HTTPServer.__init__(self, server_address,
3973 request_handler_class)
3974 self._events = []
3975 self._stopped = False
3977 def handle_request(self):
3978 """Override the base handle_request call.
3980 Python 2.6 changed the semantics of handle_request() with r61289.
3981 This patches it back to the Python 2.5 version, which has
3982 helpfully been renamed to _handle_request_noblock.
3984 if hasattr(self, "_handle_request_noblock"):
3985 self._handle_request_noblock()
3986 else:
3987 BaseHTTPServer.HTTPServer.handle_request(self)
3989 def get_request(self, time_func=time.time, select_func=select.select):
3990 """Overrides the base get_request call.
3992 Args:
3993 time_func: used for testing.
3994 select_func: used for testing.
3996 Returns:
3997 a (socket_object, address info) tuple.
3999 while True:
4000 if self._events:
4001 current_time = time_func()
4002 next_eta = self._events[0][0]
4003 delay = next_eta - current_time
4004 else:
4005 delay = DEFAULT_SELECT_DELAY
4006 readable, _, _ = select_func([self.socket], [], [], max(delay, 0))
4007 if readable:
4008 return self.socket.accept()
4009 current_time = time_func()
4013 if self._events and current_time >= self._events[0][0]:
4014 runnable = heapq.heappop(self._events)[1]
4015 request_tuple = runnable()
4016 if request_tuple:
4017 return request_tuple
4019 def serve_forever(self):
4020 """Handle one request at a time until told to stop."""
4021 while not self._stopped:
4022 self.handle_request()
4023 self.server_close()
4025 def stop_serving_forever(self):
4026 """Stop the serve_forever() loop.
4028 Stop happens on the next handle_request() loop; it will not stop
4029 immediately. Since dev_appserver.py must run on py2.5 we can't
4030 use newer features of SocketServer (e.g. shutdown(), added in py2.6).
4032 self._stopped = True
4034 def AddEvent(self, eta, runnable, service=None, event_id=None):
4035 """Add a runnable event to be run at the specified time.
4037 Args:
4038 eta: when to run the event, in seconds since epoch.
4039 runnable: a callable object.
4040 service: the service that owns this event. Should be set if id is set.
4041 event_id: optional id of the event. Used for UpdateEvent below.
4043 heapq.heappush(self._events, (eta, runnable, service, event_id))
4045 def UpdateEvent(self, service, event_id, eta):
4046 """Update a runnable event in the heap with a new eta.
4047 TODO: come up with something better than a linear scan to
4048 update items. For the case this is used for now -- updating events to
4049 "time out" channels -- this works fine because those events are always
4050 soon (within seconds) and thus found quickly towards the front of the heap.
4051 One could easily imagine a scenario where this is always called for events
4052 that tend to be at the back of the heap, of course...
4054 Args:
4055 service: the service that owns this event.
4056 event_id: the id of the event.
4057 eta: the new eta of the event.
4059 for id in xrange(len(self._events)):
4060 item = self._events[id]
4061 if item[2] == service and item[3] == event_id:
4062 item = (eta, item[1], item[2], item[3])
4063 del(self._events[id])
4064 heapq.heappush(self._events, item)
4065 break
4068 class HttpServerWithMultiProcess(HTTPServerWithScheduler):
4069 """Class extending HTTPServerWithScheduler with multi-process handling."""
4071 def __init__(self, server_address, request_handler_class):
4072 """Constructor.
4074 Args:
4075 server_address: the bind address of the server.
4076 request_handler_class: class used to handle requests.
4078 HTTPServerWithScheduler.__init__(self, server_address,
4079 request_handler_class)
4080 multiprocess.GlobalProcess().SetHttpServer(self)
4082 def process_request(self, request, client_address):
4083 """Overrides the SocketServer process_request call."""
4084 multiprocess.GlobalProcess().ProcessRequest(request, client_address)
4087 class FakeRequestSocket(object):
4088 """A socket object to fake an HTTP request."""
4090 def __init__(self, method, relative_url, headers, body):
4091 payload = cStringIO.StringIO()
4092 payload.write('%s %s HTTP/1.1\r\n' % (method, relative_url))
4093 payload.write('Content-Length: %d\r\n' % len(body))
4094 for key, value in headers:
4095 payload.write('%s: %s\r\n' % (key, value))
4096 payload.write('\r\n')
4097 payload.write(body)
4098 self.rfile = cStringIO.StringIO(payload.getvalue())
4099 self.wfile = StringIO.StringIO()
4100 self.wfile_close = self.wfile.close
4101 self.wfile.close = self.connection_done
4103 def connection_done(self):
4104 self.wfile_close()
4106 def makefile(self, mode, buffsize):
4107 if mode.startswith('w'):
4108 return self.wfile
4109 else:
4110 return self.rfile
4112 def close(self):
4113 pass
4115 def shutdown(self, how):
4116 pass
4119 class DevAppserverDispatcher(request_info._LocalFakeDispatcher):
4120 """A dev_appserver Dispatcher implementation."""
4122 def __init__(self, server, port):
4123 self._server = server
4124 self._port = port
4126 def add_event(self, runnable, eta, service=None, event_id=None):
4127 """Add a callable to be run at the specified time.
4129 Args:
4130 runnable: A callable object to call at the specified time.
4131 eta: An int containing the time to run the event, in seconds since the
4132 epoch.
4133 service: A str containing the name of the service that owns this event.
4134 This should be set if event_id is set.
4135 event_id: A str containing the id of the event. If set, this can be passed
4136 to update_event to change the time at which the event should run.
4138 self._server.AddEvent(eta, runnable, service, event_id)
4140 def update_event(self, eta, service, event_id):
4141 """Update the eta of a scheduled event.
4143 Args:
4144 eta: An int containing the time to run the event, in seconds since the
4145 epoch.
4146 service: A str containing the name of the service that owns this event.
4147 event_id: A str containing the id of the event to update.
4149 self._server.UpdateEvent(service, event_id, eta)
4151 def add_async_request(self, method, relative_url, headers, body, source_ip,
4152 server_name=None, version=None, instance_id=None):
4153 """Dispatch an HTTP request asynchronously.
4155 Args:
4156 method: A str containing the HTTP method of the request.
4157 relative_url: A str containing path and query string of the request.
4158 headers: A list of (key, value) tuples where key and value are both str.
4159 body: A str containing the request body.
4160 source_ip: The source ip address for the request.
4161 server_name: An optional str containing the server name to service this
4162 request. If unset, the request will be dispatched to the default
4163 server.
4164 version: An optional str containing the version to service this request.
4165 If unset, the request will be dispatched to the default version.
4166 instance_id: An optional str containing the instance_id of the instance to
4167 service this request. If unset, the request will be dispatched to
4168 according to the load-balancing for the server and version.
4170 fake_socket = FakeRequestSocket(method, relative_url, headers, body)
4171 self._server.AddEvent(0, lambda: (fake_socket, (source_ip, self._port)))
4173 def add_request(self, method, relative_url, headers, body, source_ip,
4174 server_name=None, version=None, instance_id=None):
4175 """Process an HTTP request.
4177 Args:
4178 method: A str containing the HTTP method of the request.
4179 relative_url: A str containing path and query string of the request.
4180 headers: A list of (key, value) tuples where key and value are both str.
4181 body: A str containing the request body.
4182 source_ip: The source ip address for the request.
4183 server_name: An optional str containing the server name to service this
4184 request. If unset, the request will be dispatched to the default
4185 server.
4186 version: An optional str containing the version to service this request.
4187 If unset, the request will be dispatched to the default version.
4188 instance_id: An optional str containing the instance_id of the instance to
4189 service this request. If unset, the request will be dispatched to
4190 according to the load-balancing for the server and version.
4192 Returns:
4193 A request_info.ResponseTuple containing the response information for the
4194 HTTP request.
4196 try:
4197 header_dict = wsgiref.headers.Headers(headers)
4198 connection_host = header_dict.get('host')
4199 connection = httplib.HTTPConnection(connection_host)
4202 connection.putrequest(
4203 method, relative_url,
4204 skip_host='host' in header_dict,
4205 skip_accept_encoding='accept-encoding' in header_dict)
4207 for header_key, header_value in headers:
4208 connection.putheader(header_key, header_value)
4209 connection.endheaders()
4210 connection.send(body)
4212 response = connection.getresponse()
4213 response.read()
4214 response.close()
4216 return request_info.ResponseTuple(
4217 '%d %s' % (response.status, response.reason), [], '')
4218 except (httplib.HTTPException, socket.error):
4219 logging.exception(
4220 'An error occured while sending a %s request to "%s%s"',
4221 method, connection_host, relative_url)
4222 return request_info.ResponseTuple('0', [], '')