App Engine Java SDK version 1.9.14
[gae.git] / python / google / appengine / tools / old_dev_appserver.py
blobc8b1e92c726d0fe8234971cf3c6e5392f5ea5ba9
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
36 from google.appengine.tools import os_compat
38 import __builtin__
39 import BaseHTTPServer
40 import base64
41 import binascii
42 import calendar
43 import cStringIO
44 import cgi
45 import cgitb
46 import email.Utils
47 import errno
48 import hashlib
49 import heapq
50 import httplib
51 import imp
52 import inspect
53 import logging
54 import mimetools
55 import mimetypes
56 import os
57 import select
58 import shutil
59 import simplejson
60 import StringIO
61 import struct
62 import tempfile
63 import wsgiref.headers
64 import yaml
71 import re
72 import sre_compile
73 import sre_constants
74 import sre_parse
76 import socket
77 import sys
78 import time
79 import types
80 import urlparse
81 import urllib
82 import zlib
84 import google
88 try:
89 from google.third_party.apphosting.python.webapp2 import v2_3 as tmp
90 sys.path.append(os.path.dirname(tmp.__file__))
91 del tmp
92 except ImportError:
93 pass
95 from google.appengine.api import apiproxy_stub_map
96 from google.appengine.api import appinfo
97 from google.appengine.api import appinfo_includes
98 from google.appengine.api import app_logging
99 from google.appengine.api import blobstore
100 from google.appengine.api import croninfo
101 from google.appengine.api import datastore
102 from google.appengine.api import datastore_file_stub
103 from google.appengine.api import lib_config
104 from google.appengine.api import mail
105 from google.appengine.api import mail_stub
106 from google.appengine.api import namespace_manager
107 from google.appengine.api import request_info
108 from google.appengine.api import urlfetch_stub
109 from google.appengine.api import user_service_stub
110 from google.appengine.api import yaml_errors
111 from google.appengine.api.app_identity import app_identity_stub
112 from google.appengine.api.blobstore import blobstore_stub
113 from google.appengine.api.blobstore import file_blob_storage
114 from google.appengine.api.capabilities import capability_stub
115 from google.appengine.api.channel import channel_service_stub
116 from google.appengine.api.files import file_service_stub
117 from google.appengine.api.logservice import logservice
118 from google.appengine.api.logservice import logservice_stub
119 from google.appengine.api.search import simple_search_stub
120 from google.appengine.api.taskqueue import taskqueue_stub
121 from google.appengine.api.prospective_search import prospective_search_stub
122 from google.appengine.api.remote_socket import _remote_socket_stub
123 from google.appengine.api.memcache import memcache_stub
124 from google.appengine.api import rdbms_mysqldb
126 from google.appengine.api.system import system_stub
127 from google.appengine.api.xmpp import xmpp_service_stub
128 from google.appengine.datastore import datastore_sqlite_stub
129 from google.appengine.datastore import datastore_stub_util
130 from google.appengine.datastore import datastore_v4_stub
131 from google.appengine import dist
133 try:
134 from google.appengine.runtime import request_environment
135 from google.appengine.runtime import runtime
136 except:
138 request_environment = None
139 runtime = None
141 from google.appengine.tools import dev_appserver_apiserver
142 from google.appengine.tools import dev_appserver_blobimage
143 from google.appengine.tools import dev_appserver_blobstore
144 from google.appengine.tools import dev_appserver_channel
145 from google.appengine.tools import dev_appserver_import_hook
146 from google.appengine.tools import dev_appserver_login
147 from google.appengine.tools import dev_appserver_multiprocess as multiprocess
148 from google.appengine.tools import dev_appserver_oauth
149 from google.appengine.tools import dev_appserver_upload
151 from google.storage.speckle.python.api import rdbms
154 CouldNotFindModuleError = dev_appserver_import_hook.CouldNotFindModuleError
155 FakeAccess = dev_appserver_import_hook.FakeAccess
156 FakeFile = dev_appserver_import_hook.FakeFile
157 FakeReadlink = dev_appserver_import_hook.FakeReadlink
158 FakeSetLocale = dev_appserver_import_hook.FakeSetLocale
159 FakeUnlink = dev_appserver_import_hook.FakeUnlink
160 GetSubmoduleName = dev_appserver_import_hook.GetSubmoduleName
161 HardenedModulesHook = dev_appserver_import_hook.HardenedModulesHook
165 SDK_ROOT = dev_appserver_import_hook.SDK_ROOT
168 PYTHON_LIB_VAR = '$PYTHON_LIB'
169 DEVEL_CONSOLE_PATH = PYTHON_LIB_VAR + '/google/appengine/ext/admin'
170 REMOTE_API_PATH = (PYTHON_LIB_VAR +
171 '/google/appengine/ext/remote_api/handler.py')
174 FILE_MISSING_EXCEPTIONS = frozenset([errno.ENOENT, errno.ENOTDIR])
178 MAX_URL_LENGTH = 2047
182 DEFAULT_ENV = {
183 'GATEWAY_INTERFACE': 'CGI/1.1',
184 'AUTH_DOMAIN': 'gmail.com',
185 'USER_ORGANIZATION': '',
186 'TZ': 'UTC',
190 DEFAULT_SELECT_DELAY = 30.0
194 for ext, mime_type in mail.EXTENSION_MIME_MAP.iteritems():
195 mimetypes.add_type(mime_type, '.' + ext)
199 MAX_RUNTIME_RESPONSE_SIZE = 32 << 20
203 MAX_REQUEST_SIZE = 32 * 1024 * 1024
206 COPY_BLOCK_SIZE = 1 << 20
210 API_VERSION = '1'
215 VERSION_FILE = '../../VERSION'
220 DEVEL_PAYLOAD_HEADER = 'HTTP_X_APPENGINE_DEVELOPMENT_PAYLOAD'
221 DEVEL_PAYLOAD_RAW_HEADER = 'X-AppEngine-Development-Payload'
223 DEVEL_FAKE_IS_ADMIN_HEADER = 'HTTP_X_APPENGINE_FAKE_IS_ADMIN'
224 DEVEL_FAKE_IS_ADMIN_RAW_HEADER = 'X-AppEngine-Fake-Is-Admin'
226 FILE_STUB_DEPRECATION_MESSAGE = (
227 """The datastore file stub is deprecated, and
228 will stop being the default in a future release.
229 Append the --use_sqlite flag to use the new SQLite stub.
231 You can port your existing data using the --port_sqlite_data flag or
232 purge your previous test data with --clear_datastore.
233 """)
239 NON_PUBLIC_CACHE_CONTROLS = frozenset(['private', 'no-cache', 'no-store'])
243 class Error(Exception):
244 """Base-class for exceptions in this module."""
247 class InvalidAppConfigError(Error):
248 """The supplied application configuration file is invalid."""
251 class AppConfigNotFoundError(Error):
252 """Application configuration file not found."""
255 class CompileError(Error):
256 """Application could not be compiled."""
257 def __init__(self, text):
258 self.text = text
260 class ExecuteError(Error):
261 """Application could not be executed."""
262 def __init__(self, text, log):
263 self.text = text
264 self.log = log
268 def MonkeyPatchPdb(pdb):
269 """Given a reference to the pdb module, fix its set_trace function.
271 This will allow the standard trick of setting a breakpoint in your
272 code by inserting a call to pdb.set_trace() to work properly, as
273 long as the original stdin and stdout of dev_appserver.py are
274 connected to a console or shell window.
277 def NewSetTrace():
278 """Replacement for set_trace() that uses the original i/o streams.
280 This is necessary because by the time the user code that might
281 invoke pdb.set_trace() runs, the default sys.stdin and sys.stdout
282 are redirected to the HTTP request and response streams instead,
283 so that pdb will encounter garbage (or EOF) in its input, and its
284 output will garble the HTTP response. Fortunately, sys.__stdin__
285 and sys.__stderr__ retain references to the original streams --
286 this is a standard Python feature. Also, fortunately, as of
287 Python 2.5, the Pdb class lets you easily override stdin and
288 stdout. The original set_trace() function does essentially the
289 same thing as the code here except it instantiates Pdb() without
290 arguments.
292 p = pdb.Pdb(stdin=sys.__stdin__, stdout=sys.__stdout__)
293 p.set_trace(sys._getframe().f_back)
295 pdb.set_trace = NewSetTrace
298 def MonkeyPatchThreadingLocal(_threading_local):
299 """Given a reference to the _threading_local module, fix _localbase.__new__.
301 This ensures that using dev_appserver with a Python interpreter older than
302 2.7 will include the fix to the _threading_local._localbase.__new__ method
303 which was introduced in Python 2.7 (http://bugs.python.org/issue1522237).
306 @staticmethod
307 def New(cls, *args, **kw):
308 self = object.__new__(cls)
309 key = '_local__key', 'thread.local.' + str(id(self))
310 object.__setattr__(self, '_local__key', key)
311 object.__setattr__(self, '_local__args', (args, kw))
312 object.__setattr__(self, '_local__lock', _threading_local.RLock())
313 if (args or kw) and (cls.__init__ is object.__init__):
314 raise TypeError('Initialization arguments are not supported')
315 dict = object.__getattribute__(self, '__dict__')
316 _threading_local.current_thread().__dict__[key] = dict
317 return self
319 _threading_local._localbase.__new__ = New
322 def SplitURL(relative_url):
323 """Splits a relative URL into its path and query-string components.
325 Args:
326 relative_url: String containing the relative URL (often starting with '/')
327 to split. Should be properly escaped as www-form-urlencoded data.
329 Returns:
330 Tuple (script_name, query_string) where:
331 script_name: Relative URL of the script that was accessed.
332 query_string: String containing everything after the '?' character.
334 (unused_scheme, unused_netloc, path, query,
335 unused_fragment) = urlparse.urlsplit(relative_url)
336 return path, query
339 def GetFullURL(server_name, server_port, relative_url):
340 """Returns the full, original URL used to access the relative URL.
342 Args:
343 server_name: Name of the local host, or the value of the 'host' header
344 from the request.
345 server_port: Port on which the request was served (string or int).
346 relative_url: Relative URL that was accessed, including query string.
348 Returns:
349 String containing the original URL.
351 if str(server_port) != '80':
352 netloc = '%s:%s' % (server_name, server_port)
353 else:
354 netloc = server_name
355 return 'http://%s%s' % (netloc, relative_url)
357 def CopyStreamPart(source, destination, content_size):
358 """Copy a portion of a stream from one file-like object to another.
360 Args:
361 source: Source stream to copy from.
362 destination: Destination stream to copy to.
363 content_size: Maximum bytes to copy.
365 Returns:
366 Number of bytes actually copied.
368 bytes_copied = 0
369 bytes_left = content_size
370 while bytes_left > 0:
371 bytes = source.read(min(bytes_left, COPY_BLOCK_SIZE))
372 bytes_read = len(bytes)
373 if bytes_read == 0:
374 break
375 destination.write(bytes)
376 bytes_copied += bytes_read
377 bytes_left -= bytes_read
378 return bytes_copied
381 def AppIdWithDefaultPartition(app_id, default_partition):
382 """Add a partition to an application id if necessary."""
383 if not default_partition:
384 return app_id
388 if '~' in app_id:
389 return app_id
391 return default_partition + '~' + app_id
396 class AppServerRequest(object):
397 """Encapsulates app-server request.
399 Object used to hold a full appserver request. Used as a container that is
400 passed through the request forward chain and ultimately sent to the
401 URLDispatcher instances.
403 Attributes:
404 relative_url: String containing the URL accessed.
405 path: Local path of the resource that was matched; back-references will be
406 replaced by values matched in the relative_url. Path may be relative
407 or absolute, depending on the resource being served (e.g., static files
408 will have an absolute path; scripts will be relative).
409 headers: Instance of mimetools.Message with headers from the request.
410 infile: File-like object with input data from the request.
411 force_admin: Allow request admin-only URLs to proceed regardless of whether
412 user is logged in or is an admin.
415 ATTRIBUTES = ['relative_url',
416 'path',
417 'headers',
418 'infile',
419 'force_admin',
422 def __init__(self,
423 relative_url,
424 path,
425 headers,
426 infile,
427 force_admin=False):
428 """Constructor.
430 Args:
431 relative_url: Mapped directly to attribute.
432 path: Mapped directly to attribute.
433 headers: Mapped directly to attribute.
434 infile: Mapped directly to attribute.
435 force_admin: Mapped directly to attribute.
437 self.relative_url = relative_url
438 self.path = path
439 self.headers = headers
440 self.infile = infile
441 self.force_admin = force_admin
442 if (DEVEL_PAYLOAD_RAW_HEADER in self.headers or
443 DEVEL_FAKE_IS_ADMIN_RAW_HEADER in self.headers):
444 self.force_admin = True
446 def __eq__(self, other):
447 """Used mainly for testing.
449 Returns:
450 True if all fields of both requests are equal, else False.
452 if type(self) == type(other):
453 for attribute in self.ATTRIBUTES:
454 if getattr(self, attribute) != getattr(other, attribute):
455 return False
456 return True
458 def __repr__(self):
459 """String representation of request.
461 Used mainly for testing.
463 Returns:
464 String representation of AppServerRequest. Strings of different
465 request objects that have the same values for all fields compare
466 as equal.
468 results = []
469 for attribute in self.ATTRIBUTES:
470 results.append('%s: %s' % (attribute, getattr(self, attribute)))
471 return '<AppServerRequest %s>' % ' '.join(results)
474 class URLDispatcher(object):
475 """Base-class for handling HTTP requests."""
477 def Dispatch(self,
478 request,
479 outfile,
480 base_env_dict=None):
481 """Dispatch and handle an HTTP request.
483 base_env_dict should contain at least these CGI variables:
484 REQUEST_METHOD, REMOTE_ADDR, SERVER_SOFTWARE, SERVER_NAME,
485 SERVER_PROTOCOL, SERVER_PORT
487 Args:
488 request: AppServerRequest instance.
489 outfile: File-like object where output data should be written.
490 base_env_dict: Dictionary of CGI environment parameters if available.
491 Defaults to None.
493 Returns:
494 None if request handling is complete.
495 A new AppServerRequest instance if internal redirect is required.
497 raise NotImplementedError
499 def EndRedirect(self, dispatched_output, original_output):
500 """Process the end of an internal redirect.
502 This method is called after all subsequent dispatch requests have finished.
503 By default the output from the dispatched process is copied to the original.
505 This will not be called on dispatchers that do not return an internal
506 redirect.
508 Args:
509 dispatched_output: StringIO buffer containing the results from the
510 dispatched
511 original_output: The original output file.
513 Returns:
514 None if request handling is complete.
515 A new AppServerRequest instance if internal redirect is required.
517 original_output.write(dispatched_output.read())
520 class URLMatcher(object):
521 """Matches an arbitrary URL using a list of URL patterns from an application.
523 Each URL pattern has an associated URLDispatcher instance and path to the
524 resource's location on disk. See AddURL for more details. The first pattern
525 that matches an inputted URL will have its associated values returned by
526 Match().
529 def __init__(self):
530 """Initializer."""
534 self._url_patterns = []
536 def AddURL(self, regex, dispatcher, path, requires_login, admin_only,
537 auth_fail_action):
538 """Adds a URL pattern to the list of patterns.
540 If the supplied regex starts with a '^' or ends with a '$' an
541 InvalidAppConfigError exception will be raised. Start and end symbols
542 and implicitly added to all regexes, meaning we assume that all regexes
543 consume all input from a URL.
545 Args:
546 regex: String containing the regular expression pattern.
547 dispatcher: Instance of URLDispatcher that should handle requests that
548 match this regex.
549 path: Path on disk for the resource. May contain back-references like
550 r'\1', r'\2', etc, which will be replaced by the corresponding groups
551 matched by the regex if present.
552 requires_login: True if the user must be logged-in before accessing this
553 URL; False if anyone can access this URL.
554 admin_only: True if the user must be a logged-in administrator to
555 access the URL; False if anyone can access the URL.
556 auth_fail_action: either appinfo.AUTH_FAIL_ACTION_REDIRECT (default)
557 which indicates that the server should redirect to the login page when
558 an authentication is needed, or appinfo.AUTH_FAIL_ACTION_UNAUTHORIZED
559 which indicates that the server should just return a 401 Unauthorized
560 message immediately.
562 Raises:
563 TypeError: if dispatcher is not a URLDispatcher sub-class instance.
564 InvalidAppConfigError: if regex isn't valid.
566 if not isinstance(dispatcher, URLDispatcher):
567 raise TypeError('dispatcher must be a URLDispatcher sub-class')
569 if regex.startswith('^') or regex.endswith('$'):
570 raise InvalidAppConfigError('regex starts with "^" or ends with "$"')
572 adjusted_regex = '^%s$' % regex
574 try:
575 url_re = re.compile(adjusted_regex)
576 except re.error, e:
577 raise InvalidAppConfigError('regex invalid: %s' % e)
579 match_tuple = (url_re, dispatcher, path, requires_login, admin_only,
580 auth_fail_action)
581 self._url_patterns.append(match_tuple)
583 def Match(self,
584 relative_url,
585 split_url=SplitURL):
586 """Matches a URL from a request against the list of URL patterns.
588 The supplied relative_url may include the query string (i.e., the '?'
589 character and everything following).
591 Args:
592 relative_url: Relative URL being accessed in a request.
593 split_url: Used for dependency injection.
595 Returns:
596 Tuple (dispatcher, matched_path, requires_login, admin_only,
597 auth_fail_action), which are the corresponding values passed to
598 AddURL when the matching URL pattern was added to this matcher.
599 The matched_path will have back-references replaced using values
600 matched by the URL pattern. If no match was found, dispatcher will
601 be None.
604 adjusted_url, unused_query_string = split_url(relative_url)
606 for url_tuple in self._url_patterns:
607 url_re, dispatcher, path, requires_login, admin_only, auth_fail_action = url_tuple
608 the_match = url_re.match(adjusted_url)
610 if the_match:
611 adjusted_path = the_match.expand(path)
612 return (dispatcher, adjusted_path, requires_login, admin_only,
613 auth_fail_action)
615 return None, None, None, None, None
617 def GetDispatchers(self):
618 """Retrieves the URLDispatcher objects that could be matched.
620 Should only be used in tests.
622 Returns:
623 A set of URLDispatcher objects.
625 return set([url_tuple[1] for url_tuple in self._url_patterns])
630 class MatcherDispatcher(URLDispatcher):
631 """Dispatcher across multiple URLMatcher instances."""
633 def __init__(self,
634 config,
635 login_url,
636 module_manager,
637 url_matchers,
638 get_user_info=dev_appserver_login.GetUserInfo,
639 login_redirect=dev_appserver_login.LoginRedirect):
640 """Initializer.
642 Args:
643 config: AppInfoExternal instance representing the parsed app.yaml file.
644 login_url: Relative URL which should be used for handling user logins.
645 module_manager: ModuleManager instance that is used to detect and reload
646 modules if the matched Dispatcher is dynamic.
647 url_matchers: Sequence of URLMatcher objects.
648 get_user_info: Used for dependency injection.
649 login_redirect: Used for dependency injection.
651 self._config = config
652 self._login_url = login_url
653 self._module_manager = module_manager
654 self._url_matchers = tuple(url_matchers)
655 self._get_user_info = get_user_info
656 self._login_redirect = login_redirect
658 def Dispatch(self,
659 request,
660 outfile,
661 base_env_dict=None):
662 """Dispatches a request to the first matching dispatcher.
664 Matchers are checked in the order they were supplied to the constructor.
665 If no matcher matches, a 404 error will be written to the outfile. The
666 path variable supplied to this method is ignored.
668 The value of request.path is ignored.
670 cookies = ', '.join(request.headers.getheaders('cookie'))
671 email_addr, admin, user_id = self._get_user_info(cookies)
673 for matcher in self._url_matchers:
674 dispatcher, matched_path, requires_login, admin_only, auth_fail_action = matcher.Match(request.relative_url)
675 if dispatcher is None:
676 continue
678 logging.debug('Matched "%s" to %s with path %s',
679 request.relative_url, dispatcher, matched_path)
681 if ((requires_login or admin_only) and
682 not email_addr and
683 not request.force_admin):
684 logging.debug('Login required, redirecting user')
685 if auth_fail_action == appinfo.AUTH_FAIL_ACTION_REDIRECT:
686 self._login_redirect(self._login_url,
687 base_env_dict['SERVER_NAME'],
688 base_env_dict['SERVER_PORT'],
689 request.relative_url,
690 outfile)
691 elif auth_fail_action == appinfo.AUTH_FAIL_ACTION_UNAUTHORIZED:
692 outfile.write('Status: %d Not authorized\r\n'
693 '\r\n'
694 'Login required to view page.'
695 % (httplib.UNAUTHORIZED))
696 elif admin_only and not admin and not request.force_admin:
697 outfile.write('Status: %d Not authorized\r\n'
698 '\r\n'
699 'Current logged in user %s is not '
700 'authorized to view this page.'
701 % (httplib.FORBIDDEN, email_addr))
702 else:
703 request.path = matched_path
709 if (not isinstance(dispatcher, FileDispatcher) and
710 self._module_manager.AreModuleFilesModified()):
711 self._module_manager.ResetModules()
713 forward_request = dispatcher.Dispatch(request,
714 outfile,
715 base_env_dict=base_env_dict)
717 while forward_request:
719 logging.info('Internal redirection to %s',
720 forward_request.relative_url)
721 new_outfile = cStringIO.StringIO()
722 self.Dispatch(forward_request,
723 new_outfile,
724 dict(base_env_dict))
726 new_outfile.seek(0)
727 forward_request = dispatcher.EndRedirect(new_outfile, outfile)
730 return
732 outfile.write('Status: %d URL did not match\r\n'
733 '\r\n'
734 'Not found error: %s did not match any patterns '
735 'in application configuration.'
736 % (httplib.NOT_FOUND, request.relative_url))
742 _IGNORE_REQUEST_HEADERS = frozenset([
743 'accept-encoding',
744 'connection',
745 'keep-alive',
746 'proxy-authorization',
747 'te',
748 'trailer',
749 'transfer-encoding',
752 'content-type',
753 'content-length',
757 _request_id = 0
758 _request_time = 0
761 def _generate_request_id_hash():
762 """Generates a hash of the current request id."""
763 return hashlib.sha1(str(_request_id)).hexdigest()[:8].upper()
766 def _GenerateRequestLogId():
767 """Generates the request log id for the current request."""
768 sec = int(_request_time)
769 usec = int(1000000 * (_request_time - sec))
770 h = hashlib.sha1(str(_request_id)).digest()[:4]
771 packed = struct.Struct('> L L').pack(sec, usec)
772 return binascii.b2a_hex(packed + h)
775 def GetGoogleSqlOAuth2RefreshToken(oauth_file_path):
776 """Reads the user's Google Cloud SQL OAuth2.0 token from disk."""
777 if not os.path.exists(oauth_file_path):
778 return None
779 try:
780 with open(oauth_file_path) as oauth_file:
781 token = simplejson.load(oauth_file)
782 return token['refresh_token']
783 except (IOError, KeyError, simplejson.decoder.JSONDecodeError):
784 logging.exception(
785 'Could not read OAuth2.0 token from %s', oauth_file_path)
786 return None
789 def SetupEnvironment(cgi_path,
790 relative_url,
791 headers,
792 infile,
793 split_url=SplitURL,
794 get_user_info=dev_appserver_login.GetUserInfo):
795 """Sets up environment variables for a CGI.
797 Args:
798 cgi_path: Full file-system path to the CGI being executed.
799 relative_url: Relative URL used to access the CGI.
800 headers: Instance of mimetools.Message containing request headers.
801 infile: File-like object with input data from the request.
802 split_url, get_user_info: Used for dependency injection.
804 Returns:
805 Dictionary containing CGI environment variables.
807 env = DEFAULT_ENV.copy()
809 script_name, query_string = split_url(relative_url)
814 env['_AH_ENCODED_SCRIPT_NAME'] = script_name
815 env['SCRIPT_NAME'] = ''
816 env['QUERY_STRING'] = query_string
817 env['PATH_INFO'] = urllib.unquote(script_name)
818 env['PATH_TRANSLATED'] = cgi_path
819 env['CONTENT_TYPE'] = headers.getheader('content-type',
820 'application/x-www-form-urlencoded')
821 env['CONTENT_LENGTH'] = headers.getheader('content-length', '')
823 cookies = ', '.join(headers.getheaders('cookie'))
824 email_addr, admin, user_id = get_user_info(cookies)
825 env['USER_EMAIL'] = email_addr
826 env['USER_ID'] = user_id
827 if admin:
828 env['USER_IS_ADMIN'] = '1'
829 if env['AUTH_DOMAIN'] == '*':
831 auth_domain = 'gmail.com'
832 parts = email_addr.split('@')
833 if len(parts) == 2 and parts[1]:
834 auth_domain = parts[1]
835 env['AUTH_DOMAIN'] = auth_domain
837 env['REQUEST_LOG_ID'] = _GenerateRequestLogId()
838 env['REQUEST_ID_HASH'] = _generate_request_id_hash()
841 for key in headers:
842 if key in _IGNORE_REQUEST_HEADERS:
843 continue
844 adjusted_name = key.replace('-', '_').upper()
845 env['HTTP_' + adjusted_name] = ', '.join(headers.getheaders(key))
850 if DEVEL_PAYLOAD_HEADER in env:
851 del env[DEVEL_PAYLOAD_HEADER]
852 new_data = base64.standard_b64decode(infile.getvalue())
853 infile.seek(0)
854 infile.truncate()
855 infile.write(new_data)
856 infile.seek(0)
857 env['CONTENT_LENGTH'] = str(len(new_data))
861 if DEVEL_FAKE_IS_ADMIN_HEADER in env:
862 del env[DEVEL_FAKE_IS_ADMIN_HEADER]
864 token = GetGoogleSqlOAuth2RefreshToken(os.path.expanduser(
865 rdbms.OAUTH_CREDENTIALS_PATH))
866 if token:
867 env['GOOGLE_SQL_OAUTH2_REFRESH_TOKEN'] = token
869 return env
872 def NotImplementedFake(*args, **kwargs):
873 """Fake for methods/functions that are not implemented in the production
874 environment.
876 raise NotImplementedError('This class/method is not available.')
879 class NotImplementedFakeClass(object):
880 """Fake class for classes that are not implemented in the production env.
882 __init__ = NotImplementedFake
885 def IsEncodingsModule(module_name):
886 """Determines if the supplied module is related to encodings in any way.
888 Encodings-related modules cannot be reloaded, so they need to be treated
889 specially when sys.modules is modified in any way.
891 Args:
892 module_name: Absolute name of the module regardless of how it is imported
893 into the local namespace (e.g., foo.bar.baz).
895 Returns:
896 True if it's an encodings-related module; False otherwise.
898 if (module_name in ('codecs', 'encodings') or
899 module_name.startswith('encodings.')):
900 return True
901 return False
904 def ClearAllButEncodingsModules(module_dict):
905 """Clear all modules in a module dictionary except for those modules that
906 are in any way related to encodings.
908 Args:
909 module_dict: Dictionary in the form used by sys.modules.
911 for module_name in module_dict.keys():
914 if not IsEncodingsModule(module_name) and module_name != 'sys':
915 del module_dict[module_name]
918 def ConnectAndDisconnectChildModules(old_module_dict, new_module_dict):
919 """Prepares for switching from old_module_dict to new_module_dict.
921 Disconnects child modules going away from parents that remain, and reconnects
922 child modules that are being added back in to old parents. This is needed to
923 satisfy code that follows the getattr() descendant chain rather than looking
924 up the desired module directly in the module dict.
926 Args:
927 old_module_dict: The module dict being replaced, looks like sys.modules.
928 new_module_dict: The module dict takings its place, looks like sys.modules.
930 old_keys = set(old_module_dict.keys())
931 new_keys = set(new_module_dict.keys())
932 for deleted_module_name in old_keys - new_keys:
933 if old_module_dict[deleted_module_name] is None:
934 continue
935 segments = deleted_module_name.rsplit('.', 1)
936 if len(segments) == 2:
937 parent_module = new_module_dict.get(segments[0])
938 if parent_module and hasattr(parent_module, segments[1]):
939 delattr(parent_module, segments[1])
940 for added_module_name in new_keys - old_keys:
941 if new_module_dict[added_module_name] is None:
942 continue
943 segments = added_module_name.rsplit('.', 1)
944 if len(segments) == 2:
945 parent_module = old_module_dict.get(segments[0])
946 child_module = new_module_dict[added_module_name]
947 if (parent_module and
948 getattr(parent_module, segments[1], None) is not child_module):
949 setattr(parent_module, segments[1], child_module)
955 SHARED_MODULE_PREFIXES = set([
956 'google',
957 'logging',
958 'sys',
959 'warnings',
964 're',
965 'sre_compile',
966 'sre_constants',
967 'sre_parse',
970 'email',
975 'wsgiref',
977 'MySQLdb',
985 'decimal',
988 NOT_SHARED_MODULE_PREFIXES = set([
989 'google.appengine.ext',
993 def ModuleNameHasPrefix(module_name, prefix_set):
994 """Determines if a module's name belongs to a set of prefix strings.
996 Args:
997 module_name: String containing the fully qualified module name.
998 prefix_set: Iterable set of module name prefixes to check against.
1000 Returns:
1001 True if the module_name belongs to the prefix set or is a submodule of
1002 any of the modules specified in the prefix_set. Otherwise False.
1004 for prefix in prefix_set:
1005 if prefix == module_name:
1006 return True
1008 if module_name.startswith(prefix + '.'):
1009 return True
1011 return False
1014 def SetupSharedModules(module_dict):
1015 """Creates a module dictionary for the hardened part of the process.
1017 Module dictionary will contain modules that should be shared between the
1018 hardened and unhardened parts of the process.
1020 Args:
1021 module_dict: Module dictionary from which existing modules should be
1022 pulled (usually sys.modules).
1024 Returns:
1025 A new module dictionary.
1027 output_dict = {}
1028 for module_name, module in module_dict.iteritems():
1035 if module is None:
1036 continue
1038 if IsEncodingsModule(module_name):
1039 output_dict[module_name] = module
1040 continue
1042 shared_prefix = ModuleNameHasPrefix(module_name, SHARED_MODULE_PREFIXES)
1043 banned_prefix = ModuleNameHasPrefix(module_name, NOT_SHARED_MODULE_PREFIXES)
1045 if shared_prefix and not banned_prefix:
1046 output_dict[module_name] = module
1048 return output_dict
1054 def ModuleHasValidMainFunction(module):
1055 """Determines if a module has a main function that takes no arguments.
1057 This includes functions that have arguments with defaults that are all
1058 assigned, thus requiring no additional arguments in order to be called.
1060 Args:
1061 module: A types.ModuleType instance.
1063 Returns:
1064 True if the module has a valid, reusable main function; False otherwise.
1066 if hasattr(module, 'main') and type(module.main) is types.FunctionType:
1067 arg_names, var_args, var_kwargs, default_values = inspect.getargspec(
1068 module.main)
1069 if len(arg_names) == 0:
1070 return True
1071 if default_values is not None and len(arg_names) == len(default_values):
1072 return True
1073 return False
1076 def CheckScriptExists(cgi_path, handler_path):
1077 """Check that the given handler_path is a file that exists on disk.
1079 Args:
1080 cgi_path: Absolute path to the CGI script file on disk.
1081 handler_path: CGI path stored in the application configuration (as a path
1082 like 'foo/bar/baz.py'). May contain $PYTHON_LIB references.
1084 Raises:
1085 CouldNotFindModuleError: if the given handler_path is a file and doesn't
1086 have the expected extension.
1088 if handler_path.startswith(PYTHON_LIB_VAR + '/'):
1090 return
1092 if (not os.path.isdir(cgi_path) and
1093 not os.path.isfile(cgi_path) and
1094 os.path.isfile(cgi_path + '.py')):
1095 raise CouldNotFindModuleError(
1096 'Perhaps you meant to have the line "script: %s.py" in your app.yaml' %
1097 handler_path)
1100 def GetScriptModuleName(handler_path):
1101 """Determines the fully-qualified Python module name of a script on disk.
1103 Args:
1104 handler_path: CGI path stored in the application configuration (as a path
1105 like 'foo/bar/baz.py'). May contain $PYTHON_LIB references.
1107 Returns:
1108 String containing the corresponding module name (e.g., 'foo.bar.baz').
1110 if handler_path.startswith(PYTHON_LIB_VAR + '/'):
1111 handler_path = handler_path[len(PYTHON_LIB_VAR):]
1112 handler_path = os.path.normpath(handler_path)
1115 extension_index = handler_path.rfind('.py')
1116 if extension_index != -1:
1117 handler_path = handler_path[:extension_index]
1118 module_fullname = handler_path.replace(os.sep, '.')
1119 module_fullname = module_fullname.strip('.')
1120 module_fullname = re.sub('\.+', '.', module_fullname)
1124 if module_fullname.endswith('.__init__'):
1125 module_fullname = module_fullname[:-len('.__init__')]
1127 return module_fullname
1130 def FindMissingInitFiles(cgi_path, module_fullname, isfile=os.path.isfile):
1131 """Determines which __init__.py files are missing from a module's parent
1132 packages.
1134 Args:
1135 cgi_path: Absolute path of the CGI module file on disk.
1136 module_fullname: Fully qualified Python module name used to import the
1137 cgi_path module.
1138 isfile: Used for testing.
1140 Returns:
1141 List containing the paths to the missing __init__.py files.
1143 missing_init_files = []
1145 if cgi_path.endswith('.py'):
1146 module_base = os.path.dirname(cgi_path)
1147 else:
1148 module_base = cgi_path
1150 depth_count = module_fullname.count('.')
1156 if cgi_path.endswith('__init__.py') or not cgi_path.endswith('.py'):
1157 depth_count += 1
1159 for index in xrange(depth_count):
1162 current_init_file = os.path.abspath(
1163 os.path.join(module_base, '__init__.py'))
1165 if not isfile(current_init_file):
1166 missing_init_files.append(current_init_file)
1168 module_base = os.path.abspath(os.path.join(module_base, os.pardir))
1170 return missing_init_files
1173 def LoadTargetModule(handler_path,
1174 cgi_path,
1175 import_hook,
1176 module_dict=sys.modules):
1177 """Loads a target CGI script by importing it as a Python module.
1179 If the module for the target CGI script has already been loaded before,
1180 the new module will be loaded in its place using the same module object,
1181 possibly overwriting existing module attributes.
1183 Args:
1184 handler_path: CGI path stored in the application configuration (as a path
1185 like 'foo/bar/baz.py'). Should not have $PYTHON_LIB references.
1186 cgi_path: Absolute path to the CGI script file on disk.
1187 import_hook: Instance of HardenedModulesHook to use for module loading.
1188 module_dict: Used for dependency injection.
1190 Returns:
1191 Tuple (module_fullname, script_module, module_code) where:
1192 module_fullname: Fully qualified module name used to import the script.
1193 script_module: The ModuleType object corresponding to the module_fullname.
1194 If the module has not already been loaded, this will be an empty
1195 shell of a module.
1196 module_code: Code object (returned by compile built-in) corresponding
1197 to the cgi_path to run. If the script_module was previously loaded
1198 and has a main() function that can be reused, this will be None.
1200 Raises:
1201 CouldNotFindModuleError if the given handler_path is a file and doesn't have
1202 the expected extension.
1204 CheckScriptExists(cgi_path, handler_path)
1205 module_fullname = GetScriptModuleName(handler_path)
1206 script_module = module_dict.get(module_fullname)
1207 module_code = None
1208 if script_module is not None and ModuleHasValidMainFunction(script_module):
1212 logging.debug('Reusing main() function of module "%s"', module_fullname)
1213 else:
1220 if script_module is None:
1221 script_module = imp.new_module(module_fullname)
1222 script_module.__loader__ = import_hook
1225 try:
1226 module_code = import_hook.get_code(module_fullname)
1227 full_path, search_path, submodule = (
1228 import_hook.GetModuleInfo(module_fullname))
1229 script_module.__file__ = full_path
1230 if search_path is not None:
1231 script_module.__path__ = search_path
1232 except UnicodeDecodeError, e:
1236 error = ('%s please see http://www.python.org/peps'
1237 '/pep-0263.html for details (%s)' % (e, handler_path))
1238 raise SyntaxError(error)
1239 except:
1240 exc_type, exc_value, exc_tb = sys.exc_info()
1241 import_error_message = str(exc_type)
1242 if exc_value:
1243 import_error_message += ': ' + str(exc_value)
1251 logging.exception('Encountered error loading module "%s": %s',
1252 module_fullname, import_error_message)
1253 missing_inits = FindMissingInitFiles(cgi_path, module_fullname)
1254 if missing_inits:
1255 logging.warning('Missing package initialization files: %s',
1256 ', '.join(missing_inits))
1257 else:
1258 logging.error('Parent package initialization files are present, '
1259 'but must be broken')
1262 independent_load_successful = True
1264 if not os.path.isfile(cgi_path):
1269 independent_load_successful = False
1270 else:
1271 try:
1272 source_file = open(cgi_path)
1273 try:
1274 module_code = compile(source_file.read(), cgi_path, 'exec')
1275 script_module.__file__ = cgi_path
1276 finally:
1277 source_file.close()
1279 except OSError:
1283 independent_load_successful = False
1286 if not independent_load_successful:
1287 raise exc_type, exc_value, exc_tb
1292 module_dict[module_fullname] = script_module
1294 return module_fullname, script_module, module_code
1297 def _WriteErrorToOutput(status, message, outfile):
1298 """Writes an error status response to the response outfile.
1300 Args:
1301 status: The status to return, e.g. '411 Length Required'.
1302 message: A human-readable error message.
1303 outfile: Response outfile.
1305 logging.error(message)
1306 outfile.write('Status: %s\r\n\r\n%s' % (status, message))
1309 def GetRequestSize(request, env_dict, outfile):
1310 """Gets the size (content length) of the given request.
1312 On error, this method writes an error message to the response outfile and
1313 returns None. Errors include the request missing a required header and the
1314 request being too large.
1316 Args:
1317 request: AppServerRequest instance.
1318 env_dict: Environment dictionary. May be None.
1319 outfile: Response outfile.
1321 Returns:
1322 The calculated request size, or None on error.
1324 if 'content-length' in request.headers:
1325 request_size = int(request.headers['content-length'])
1326 elif env_dict and env_dict.get('REQUEST_METHOD', '') == 'POST':
1327 _WriteErrorToOutput('%d Length required' % httplib.LENGTH_REQUIRED,
1328 'POST requests require a Content-length header.',
1329 outfile)
1330 return None
1331 else:
1332 request_size = 0
1334 if request_size <= MAX_REQUEST_SIZE:
1335 return request_size
1336 else:
1337 msg = ('HTTP request was too large: %d. The limit is: %d.'
1338 % (request_size, MAX_REQUEST_SIZE))
1339 _WriteErrorToOutput(
1340 '%d Request entity too large' % httplib.REQUEST_ENTITY_TOO_LARGE,
1341 msg, outfile)
1342 return None
1345 def ExecuteOrImportScript(config, handler_path, cgi_path, import_hook):
1346 """Executes a CGI script by importing it as a new module.
1348 This possibly reuses the module's main() function if it is defined and
1349 takes no arguments.
1351 Basic technique lifted from PEP 338 and Python2.5's runpy module. See:
1352 http://www.python.org/dev/peps/pep-0338/
1354 See the section entitled "Import Statements and the Main Module" to understand
1355 why a module named '__main__' cannot do relative imports. To get around this,
1356 the requested module's path could be added to sys.path on each request.
1358 Args:
1359 config: AppInfoExternal instance representing the parsed app.yaml file.
1360 handler_path: CGI path stored in the application configuration (as a path
1361 like 'foo/bar/baz.py'). Should not have $PYTHON_LIB references.
1362 cgi_path: Absolute path to the CGI script file on disk.
1363 import_hook: Instance of HardenedModulesHook to use for module loading.
1365 Returns:
1366 True if the response code had an error status (e.g., 404), or False if it
1367 did not.
1369 Raises:
1370 Any kind of exception that could have been raised when loading the target
1371 module, running a target script, or executing the application code itself.
1373 module_fullname, script_module, module_code = LoadTargetModule(
1374 handler_path, cgi_path, import_hook)
1375 script_module.__name__ = '__main__'
1376 sys.modules['__main__'] = script_module
1377 try:
1379 import pdb
1380 MonkeyPatchPdb(pdb)
1383 if module_code:
1384 exec module_code in script_module.__dict__
1385 else:
1386 script_module.main()
1392 sys.stdout.flush()
1393 sys.stdout.seek(0)
1394 try:
1395 headers = mimetools.Message(sys.stdout)
1396 finally:
1399 sys.stdout.seek(0, 2)
1400 status_header = headers.get('status')
1401 error_response = False
1402 if status_header:
1403 try:
1404 status_code = int(status_header.split(' ', 1)[0])
1405 error_response = status_code >= 400
1406 except ValueError:
1407 error_response = True
1410 if not error_response:
1411 try:
1412 parent_package = import_hook.GetParentPackage(module_fullname)
1413 except Exception:
1414 parent_package = None
1416 if parent_package is not None:
1417 submodule = GetSubmoduleName(module_fullname)
1418 setattr(parent_package, submodule, script_module)
1420 return error_response
1421 finally:
1422 script_module.__name__ = module_fullname
1425 def ExecutePy27Handler(config, handler_path, cgi_path, import_hook):
1426 """Equivalent to ExecuteOrImportScript for Python 2.7 runtime.
1428 This dispatches to google.appengine.runtime.runtime,
1429 which in turn will dispatch to either the cgi or the wsgi module in
1430 the same package, depending on the form of handler_path.
1432 Args:
1433 config: AppInfoExternal instance representing the parsed app.yaml file.
1434 handler_path: handler ("script") from the application configuration;
1435 either a script reference like foo/bar.py, or an object reference
1436 like foo.bar.app.
1437 cgi_path: Absolute path to the CGI script file on disk;
1438 typically the app dir joined with handler_path.
1439 import_hook: Instance of HardenedModulesHook to use for module loading.
1441 Returns:
1442 True if the response code had an error status (e.g., 404), or False if it
1443 did not.
1445 Raises:
1446 Any kind of exception that could have been raised when loading the target
1447 module, running a target script, or executing the application code itself.
1449 if request_environment is None or runtime is None:
1450 raise RuntimeError('Python 2.5 is too old to emulate the Python 2.7 runtime.'
1451 ' Please use Python 2.6 or Python 2.7.')
1454 import os
1456 save_environ = os.environ
1457 save_getenv = os.getenv
1459 env = dict(save_environ)
1462 if env.get('_AH_THREADSAFE'):
1463 env['wsgi.multithread'] = True
1465 url = 'http://%s%s' % (env.get('HTTP_HOST', 'localhost:8080'),
1466 env.get('_AH_ENCODED_SCRIPT_NAME', '/'))
1467 qs = env.get('QUERY_STRING')
1468 if qs:
1469 url += '?' + qs
1472 post_data = sys.stdin.read()
1481 if 'CONTENT_TYPE' in env:
1482 if post_data:
1483 env['HTTP_CONTENT_TYPE'] = env['CONTENT_TYPE']
1484 del env['CONTENT_TYPE']
1485 if 'CONTENT_LENGTH' in env:
1486 if env['CONTENT_LENGTH']:
1487 env['HTTP_CONTENT_LENGTH'] = env['CONTENT_LENGTH']
1488 del env['CONTENT_LENGTH']
1490 if cgi_path.endswith(handler_path):
1491 application_root = cgi_path[:-len(handler_path)]
1492 if application_root.endswith('/') and application_root != '/':
1493 application_root = application_root[:-1]
1494 else:
1495 application_root = ''
1498 try:
1500 import pdb
1501 MonkeyPatchPdb(pdb)
1503 import _threading_local
1504 MonkeyPatchThreadingLocal(_threading_local)
1508 os.environ = request_environment.RequestLocalEnviron(
1509 request_environment.current_request)
1513 os.getenv = os.environ.get
1515 response = runtime.HandleRequest(env, handler_path, url,
1516 post_data, application_root, SDK_ROOT,
1517 import_hook)
1518 finally:
1520 os.environ = save_environ
1521 os.getenv = save_getenv
1525 error = response.get('error')
1526 if error:
1527 status = 500
1528 else:
1529 status = 200
1530 status = response.get('response_code', status)
1531 sys.stdout.write('Status: %s\r\n' % status)
1532 for key, value in response.get('headers', ()):
1535 key = '-'.join(key.split())
1536 value = value.replace('\r', ' ').replace('\n', ' ')
1537 sys.stdout.write('%s: %s\r\n' % (key, value))
1538 sys.stdout.write('\r\n')
1539 body = response.get('body')
1540 if body:
1541 sys.stdout.write(body)
1542 logs = response.get('logs')
1543 if logs:
1544 for timestamp_usec, severity, message in logs:
1546 logging.log(severity*10 + 10, '@%s: %s',
1547 time.ctime(timestamp_usec*1e-6), message)
1548 return error
1551 class LoggingStream(object):
1552 """A stream that writes logs at level error."""
1554 def write(self, message):
1557 logging.getLogger()._log(logging.ERROR, message, ())
1559 def writelines(self, lines):
1560 for line in lines:
1561 logging.getLogger()._log(logging.ERROR, line, ())
1563 def __getattr__(self, key):
1564 return getattr(sys.__stderr__, key)
1567 def ExecuteCGI(config,
1568 root_path,
1569 handler_path,
1570 cgi_path,
1571 env,
1572 infile,
1573 outfile,
1574 module_dict,
1575 exec_script=ExecuteOrImportScript,
1576 exec_py27_handler=ExecutePy27Handler):
1577 """Executes Python file in this process as if it were a CGI.
1579 Does not return an HTTP response line. CGIs should output headers followed by
1580 the body content.
1582 The modules in sys.modules should be the same before and after the CGI is
1583 executed, with the specific exception of encodings-related modules, which
1584 cannot be reloaded and thus must always stay in sys.modules.
1586 Args:
1587 config: AppInfoExternal instance representing the parsed app.yaml file.
1588 root_path: Path to the root of the application.
1589 handler_path: CGI path stored in the application configuration (as a path
1590 like 'foo/bar/baz.py'). May contain $PYTHON_LIB references.
1591 cgi_path: Absolute path to the CGI script file on disk.
1592 env: Dictionary of environment variables to use for the execution.
1593 infile: File-like object to read HTTP request input data from.
1594 outfile: FIle-like object to write HTTP response data to.
1595 module_dict: Dictionary in which application-loaded modules should be
1596 preserved between requests. This removes the need to reload modules that
1597 are reused between requests, significantly increasing load performance.
1598 This dictionary must be separate from the sys.modules dictionary.
1599 exec_script: Used for dependency injection.
1600 exec_py27_handler: Used for dependency injection.
1603 old_module_dict = sys.modules.copy()
1604 old_builtin = __builtin__.__dict__.copy()
1605 old_argv = sys.argv
1606 old_stdin = sys.stdin
1607 old_stdout = sys.stdout
1608 old_stderr = sys.stderr
1609 old_env = os.environ.copy()
1610 old_cwd = os.getcwd()
1611 old_file_type = types.FileType
1612 reset_modules = False
1613 app_log_handler = None
1615 try:
1616 ConnectAndDisconnectChildModules(sys.modules, module_dict)
1617 ClearAllButEncodingsModules(sys.modules)
1618 sys.modules.update(module_dict)
1619 sys.argv = [cgi_path]
1621 sys.stdin = cStringIO.StringIO(infile.getvalue())
1622 sys.stdout = outfile
1626 sys.stderr = LoggingStream()
1628 logservice._global_buffer = logservice.LogsBuffer()
1630 app_log_handler = app_logging.AppLogsHandler()
1631 logging.getLogger().addHandler(app_log_handler)
1633 os.environ.clear()
1634 os.environ.update(env)
1638 cgi_dir = os.path.normpath(os.path.dirname(cgi_path))
1639 root_path = os.path.normpath(os.path.abspath(root_path))
1640 if (cgi_dir.startswith(root_path + os.sep) and
1641 not (config and config.runtime == 'python27')):
1642 os.chdir(cgi_dir)
1643 else:
1644 os.chdir(root_path)
1646 dist.fix_paths(root_path, SDK_ROOT)
1651 hook = HardenedModulesHook(config, sys.modules, root_path)
1652 sys.meta_path = [finder for finder in sys.meta_path
1653 if not isinstance(finder, HardenedModulesHook)]
1654 sys.meta_path.insert(0, hook)
1655 if hasattr(sys, 'path_importer_cache'):
1656 sys.path_importer_cache.clear()
1659 __builtin__.file = FakeFile
1660 __builtin__.open = FakeFile
1661 types.FileType = FakeFile
1663 if not (config and config.runtime == 'python27'):
1665 __builtin__.buffer = NotImplementedFakeClass
1672 sys.modules['__builtin__'] = __builtin__
1674 logging.debug('Executing CGI with env:\n%s', repr(env))
1675 try:
1678 if handler_path and config and config.runtime == 'python27':
1679 reset_modules = exec_py27_handler(config, handler_path, cgi_path, hook)
1680 else:
1681 reset_modules = exec_script(config, handler_path, cgi_path, hook)
1682 except SystemExit, e:
1683 logging.debug('CGI exited with status: %s', e)
1684 except:
1685 reset_modules = True
1686 raise
1688 finally:
1689 sys.path_importer_cache.clear()
1691 _ClearTemplateCache(sys.modules)
1695 module_dict.update(sys.modules)
1696 ConnectAndDisconnectChildModules(sys.modules, old_module_dict)
1697 ClearAllButEncodingsModules(sys.modules)
1698 sys.modules.update(old_module_dict)
1700 __builtin__.__dict__.update(old_builtin)
1701 sys.argv = old_argv
1702 sys.stdin = old_stdin
1703 sys.stdout = old_stdout
1705 sys.stderr = old_stderr
1706 logging.getLogger().removeHandler(app_log_handler)
1708 os.environ.clear()
1709 os.environ.update(old_env)
1710 os.chdir(old_cwd)
1713 types.FileType = old_file_type
1716 class CGIDispatcher(URLDispatcher):
1717 """Dispatcher that executes Python CGI scripts."""
1719 def __init__(self,
1720 config,
1721 module_dict,
1722 root_path,
1723 path_adjuster,
1724 setup_env=SetupEnvironment,
1725 exec_cgi=ExecuteCGI):
1726 """Initializer.
1728 Args:
1729 config: AppInfoExternal instance representing the parsed app.yaml file.
1730 module_dict: Dictionary in which application-loaded modules should be
1731 preserved between requests. This dictionary must be separate from the
1732 sys.modules dictionary.
1733 path_adjuster: Instance of PathAdjuster to use for finding absolute
1734 paths of CGI files on disk.
1735 setup_env, exec_cgi: Used for dependency injection.
1737 self._config = config
1738 self._module_dict = module_dict
1739 self._root_path = root_path
1740 self._path_adjuster = path_adjuster
1741 self._setup_env = setup_env
1742 self._exec_cgi = exec_cgi
1744 def Dispatch(self,
1745 request,
1746 outfile,
1747 base_env_dict=None):
1748 """Dispatches the Python CGI."""
1749 request_size = GetRequestSize(request, base_env_dict, outfile)
1750 if request_size is None:
1751 return
1754 memory_file = cStringIO.StringIO()
1755 CopyStreamPart(request.infile, memory_file, request_size)
1756 memory_file.seek(0)
1758 before_level = logging.root.level
1759 try:
1760 env = {}
1763 if self._config.env_variables:
1764 env.update(self._config.env_variables)
1765 if base_env_dict:
1766 env.update(base_env_dict)
1767 cgi_path = self._path_adjuster.AdjustPath(request.path)
1768 env.update(self._setup_env(cgi_path,
1769 request.relative_url,
1770 request.headers,
1771 memory_file))
1772 self._exec_cgi(self._config,
1773 self._root_path,
1774 request.path,
1775 cgi_path,
1776 env,
1777 memory_file,
1778 outfile,
1779 self._module_dict)
1780 finally:
1781 logging.root.level = before_level
1783 def __str__(self):
1784 """Returns a string representation of this dispatcher."""
1785 return 'CGI dispatcher'
1788 class LocalCGIDispatcher(CGIDispatcher):
1789 """Dispatcher that executes local functions like they're CGIs.
1791 The contents of sys.modules will be preserved for local CGIs running this
1792 dispatcher, but module hardening will still occur for any new imports. Thus,
1793 be sure that any local CGIs have loaded all of their dependent modules
1794 _before_ they are executed.
1797 def __init__(self, config, module_dict, path_adjuster, cgi_func):
1798 """Initializer.
1800 Args:
1801 config: AppInfoExternal instance representing the parsed app.yaml file.
1802 module_dict: Passed to CGIDispatcher.
1803 path_adjuster: Passed to CGIDispatcher.
1804 cgi_func: Callable function taking no parameters that should be
1805 executed in a CGI environment in the current process.
1807 self._cgi_func = cgi_func
1809 def curried_exec_script(*args, **kwargs):
1810 cgi_func()
1811 return False
1813 def curried_exec_cgi(*args, **kwargs):
1814 kwargs['exec_script'] = curried_exec_script
1815 return ExecuteCGI(*args, **kwargs)
1817 CGIDispatcher.__init__(self,
1818 config,
1819 module_dict,
1821 path_adjuster,
1822 exec_cgi=curried_exec_cgi)
1824 def Dispatch(self, *args, **kwargs):
1825 """Preserves sys.modules for CGIDispatcher.Dispatch."""
1826 self._module_dict.update(sys.modules)
1827 CGIDispatcher.Dispatch(self, *args, **kwargs)
1829 def __str__(self):
1830 """Returns a string representation of this dispatcher."""
1831 return 'Local CGI dispatcher for %s' % self._cgi_func
1836 class PathAdjuster(object):
1837 """Adjusts application file paths to paths relative to the application or
1838 external library directories."""
1840 def __init__(self, root_path):
1841 """Initializer.
1843 Args:
1844 root_path: Path to the root of the application running on the server.
1846 self._root_path = os.path.abspath(root_path)
1848 def AdjustPath(self, path):
1849 """Adjusts application file paths to relative to the application.
1851 More precisely this method adjusts application file path to paths
1852 relative to the application or external library directories.
1854 Handler paths that start with $PYTHON_LIB will be converted to paths
1855 relative to the google directory.
1857 Args:
1858 path: File path that should be adjusted.
1860 Returns:
1861 The adjusted path.
1863 if path.startswith(PYTHON_LIB_VAR):
1864 path = os.path.join(SDK_ROOT, path[len(PYTHON_LIB_VAR) + 1:])
1865 else:
1866 path = os.path.join(self._root_path, path)
1868 return path
1873 class StaticFileConfigMatcher(object):
1874 """Keeps track of file/directory specific application configuration.
1876 Specifically:
1877 - Computes mime type based on URLMap and file extension.
1878 - Decides on cache expiration time based on URLMap and default expiration.
1879 - Decides what HTTP headers to add to responses.
1881 To determine the mime type, we first see if there is any mime-type property
1882 on each URLMap entry. If non is specified, we use the mimetypes module to
1883 guess the mime type from the file path extension, and use
1884 application/octet-stream if we can't find the mimetype.
1887 def __init__(self,
1888 url_map_list,
1889 default_expiration):
1890 """Initializer.
1892 Args:
1893 url_map_list: List of appinfo.URLMap objects.
1894 If empty or None, then we always use the mime type chosen by the
1895 mimetypes module.
1896 default_expiration: String describing default expiration time for browser
1897 based caching of static files. If set to None this disallows any
1898 browser caching of static content.
1900 if default_expiration is not None:
1901 self._default_expiration = appinfo.ParseExpiration(default_expiration)
1902 else:
1903 self._default_expiration = None
1906 self._patterns = []
1907 for url_map in url_map_list or []:
1909 handler_type = url_map.GetHandlerType()
1910 if handler_type not in (appinfo.STATIC_FILES, appinfo.STATIC_DIR):
1911 continue
1913 path_re = _StaticFilePathRe(url_map)
1914 try:
1915 self._patterns.append((re.compile(path_re), url_map))
1916 except re.error, e:
1917 raise InvalidAppConfigError('regex %s does not compile: %s' %
1918 (path_re, e))
1920 _DUMMY_URLMAP = appinfo.URLMap()
1922 def _FirstMatch(self, path):
1923 """Returns the first appinfo.URLMap that matches path, or a dummy instance.
1925 A dummy instance is returned when no appinfo.URLMap matches path (see the
1926 URLMap.static_file_path_re property). When a dummy instance is returned, it
1927 is always the same one. The dummy instance is constructed simply by doing
1928 the following:
1930 appinfo.URLMap()
1932 Args:
1933 path: A string containing the file's path relative to the app.
1935 Returns:
1936 The first appinfo.URLMap (in the list that was passed to the constructor)
1937 that matches path. Matching depends on whether URLMap is a static_dir
1938 handler or a static_files handler. In either case, matching is done
1939 according to the URLMap.static_file_path_re property.
1941 for path_re, url_map in self._patterns:
1942 if path_re.match(path):
1943 return url_map
1944 return StaticFileConfigMatcher._DUMMY_URLMAP
1946 def IsStaticFile(self, path):
1947 """Tests if the given path points to a "static" file.
1949 Args:
1950 path: A string containing the file's path relative to the app.
1952 Returns:
1953 Boolean, True if the file was configured to be static.
1955 return self._FirstMatch(path) is not self._DUMMY_URLMAP
1957 def GetMimeType(self, path):
1958 """Returns the mime type that we should use when serving the specified file.
1960 Args:
1961 path: A string containing the file's path relative to the app.
1963 Returns:
1964 String containing the mime type to use. Will be 'application/octet-stream'
1965 if we have no idea what it should be.
1967 url_map = self._FirstMatch(path)
1968 if url_map.mime_type is not None:
1969 return url_map.mime_type
1972 unused_filename, extension = os.path.splitext(path)
1973 return mimetypes.types_map.get(extension, 'application/octet-stream')
1975 def GetExpiration(self, path):
1976 """Returns the cache expiration duration to be users for the given file.
1978 Args:
1979 path: A string containing the file's path relative to the app.
1981 Returns:
1982 Integer number of seconds to be used for browser cache expiration time.
1985 if self._default_expiration is None:
1986 return 0
1988 url_map = self._FirstMatch(path)
1989 if url_map.expiration is None:
1990 return self._default_expiration
1992 return appinfo.ParseExpiration(url_map.expiration)
1994 def GetHttpHeaders(self, path):
1995 """Returns http_headers of the matching appinfo.URLMap, or an empty one.
1997 Args:
1998 path: A string containing the file's path relative to the app.
2000 Returns:
2001 A user-specified HTTP headers to be used in static content response. These
2002 headers are contained in an appinfo.HttpHeadersDict, which maps header
2003 names to values (both strings).
2005 return self._FirstMatch(path).http_headers or appinfo.HttpHeadersDict()
2011 def ReadDataFile(data_path, openfile=file):
2012 """Reads a file on disk, returning a corresponding HTTP status and data.
2014 Args:
2015 data_path: Path to the file on disk to read.
2016 openfile: Used for dependency injection.
2018 Returns:
2019 Tuple (status, data) where status is an HTTP response code, and data is
2020 the data read; will be an empty string if an error occurred or the
2021 file was empty.
2023 status = httplib.INTERNAL_SERVER_ERROR
2024 data = ""
2026 try:
2027 data_file = openfile(data_path, 'rb')
2028 try:
2029 data = data_file.read()
2030 finally:
2031 data_file.close()
2032 status = httplib.OK
2033 except (OSError, IOError), e:
2034 logging.error('Error encountered reading file "%s":\n%s', data_path, e)
2035 if e.errno in FILE_MISSING_EXCEPTIONS:
2036 status = httplib.NOT_FOUND
2037 else:
2038 status = httplib.FORBIDDEN
2040 return status, data
2043 class FileDispatcher(URLDispatcher):
2044 """Dispatcher that reads data files from disk."""
2046 def __init__(self,
2047 config,
2048 path_adjuster,
2049 static_file_config_matcher,
2050 read_data_file=ReadDataFile):
2051 """Initializer.
2053 Args:
2054 config: AppInfoExternal instance representing the parsed app.yaml file.
2055 path_adjuster: Instance of PathAdjuster to use for finding absolute
2056 paths of data files on disk.
2057 static_file_config_matcher: StaticFileConfigMatcher object.
2058 read_data_file: Used for dependency injection.
2060 self._config = config
2061 self._path_adjuster = path_adjuster
2062 self._static_file_config_matcher = static_file_config_matcher
2063 self._read_data_file = read_data_file
2065 def Dispatch(self, request, outfile, base_env_dict=None):
2066 """Reads the file and returns the response status and data."""
2067 full_path = self._path_adjuster.AdjustPath(request.path)
2068 status, data = self._read_data_file(full_path)
2069 content_type = self._static_file_config_matcher.GetMimeType(request.path)
2070 static_file = self._static_file_config_matcher.IsStaticFile(request.path)
2071 expiration = self._static_file_config_matcher.GetExpiration(request.path)
2072 current_etag = self.CreateEtag(data)
2073 if_match_etag = request.headers.get('if-match', None)
2074 if_none_match_etag = request.headers.get('if-none-match', '').split(',')
2076 http_headers = self._static_file_config_matcher.GetHttpHeaders(request.path)
2077 def WriteHeader(name, value):
2078 if http_headers.Get(name) is None:
2079 outfile.write('%s: %s\r\n' % (name, value))
2085 if if_match_etag and not self._CheckETagMatches(if_match_etag.split(','),
2086 current_etag,
2087 False):
2088 outfile.write('Status: %s\r\n' % httplib.PRECONDITION_FAILED)
2089 WriteHeader('ETag', current_etag)
2090 outfile.write('\r\n')
2091 elif self._CheckETagMatches(if_none_match_etag, current_etag, True):
2092 outfile.write('Status: %s\r\n' % httplib.NOT_MODIFIED)
2093 WriteHeader('ETag', current_etag)
2094 outfile.write('\r\n')
2095 else:
2099 outfile.write('Status: %d\r\n' % status)
2101 WriteHeader('Content-Type', content_type)
2104 if expiration:
2105 fmt = email.Utils.formatdate
2106 WriteHeader('Expires', fmt(time.time() + expiration, usegmt=True))
2107 WriteHeader('Cache-Control', 'public, max-age=%i' % expiration)
2110 if static_file:
2111 WriteHeader('ETag', '"%s"' % current_etag)
2113 for header in http_headers.iteritems():
2114 outfile.write('%s: %s\r\n' % header)
2116 outfile.write('\r\n')
2117 outfile.write(data)
2119 def __str__(self):
2120 """Returns a string representation of this dispatcher."""
2121 return 'File dispatcher'
2123 @staticmethod
2124 def CreateEtag(data):
2125 """Returns string of hash of file content, unique per URL."""
2126 data_crc = zlib.crc32(data)
2127 return base64.b64encode(str(data_crc))
2129 @staticmethod
2130 def _CheckETagMatches(supplied_etags, current_etag, allow_weak_match):
2131 """Checks if there is an entity tag match.
2133 Args:
2134 supplied_etags: list of input etags
2135 current_etag: the calculated etag for the entity
2136 allow_weak_match: Allow for weak tag comparison.
2138 Returns:
2139 True if there is a match, False otherwise.
2142 for tag in supplied_etags:
2143 if allow_weak_match and tag.startswith('W/'):
2144 tag = tag[2:]
2145 tag_data = tag.strip('"')
2146 if tag_data == '*' or tag_data == current_etag:
2147 return True
2148 return False
2157 _IGNORE_RESPONSE_HEADERS = frozenset([
2158 'connection',
2159 'content-encoding',
2160 'date',
2161 'keep-alive',
2162 'proxy-authenticate',
2163 'server',
2164 'trailer',
2165 'transfer-encoding',
2166 'upgrade',
2167 blobstore.BLOB_KEY_HEADER
2171 class AppServerResponse(object):
2172 """Development appserver response object.
2174 Object used to hold the full appserver response. Used as a container
2175 that is passed through the request rewrite chain and ultimately sent
2176 to the web client.
2178 Attributes:
2179 status_code: Integer HTTP response status (e.g., 200, 302, 404, 500)
2180 status_message: String containing an informational message about the
2181 response code, possibly derived from the 'status' header, if supplied.
2182 headers: mimetools.Message containing the HTTP headers of the response.
2183 body: File-like object containing the body of the response.
2184 large_response: Indicates that response is permitted to be larger than
2185 MAX_RUNTIME_RESPONSE_SIZE.
2189 __slots__ = ['status_code',
2190 'status_message',
2191 'headers',
2192 'body',
2193 'large_response']
2195 def __init__(self, response_file=None, **kwds):
2196 """Initializer.
2198 Args:
2199 response_file: A file-like object that contains the full response
2200 generated by the user application request handler. If present
2201 the headers and body are set from this value, although the values
2202 may be further overridden by the keyword parameters.
2203 kwds: All keywords are mapped to attributes of AppServerResponse.
2205 self.status_code = 200
2206 self.status_message = 'Good to go'
2207 self.large_response = False
2209 if response_file:
2210 self.SetResponse(response_file)
2211 else:
2212 self.headers = mimetools.Message(cStringIO.StringIO())
2213 self.body = None
2215 for name, value in kwds.iteritems():
2216 setattr(self, name, value)
2218 def SetResponse(self, response_file):
2219 """Sets headers and body from the response file.
2221 Args:
2222 response_file: File like object to set body and headers from.
2224 self.headers = mimetools.Message(response_file)
2225 self.body = response_file
2227 @property
2228 def header_data(self):
2229 """Get header data as a string.
2231 Returns:
2232 String representation of header with line breaks cleaned up.
2235 header_list = []
2236 for header in self.headers.headers:
2237 header = header.rstrip('\n\r')
2238 header_list.append(header)
2239 if not self.headers.getheader('Content-Type'):
2241 header_list.append('Content-Type: text/html')
2243 return '\r\n'.join(header_list) + '\r\n'
2246 def IgnoreHeadersRewriter(response):
2247 """Ignore specific response headers.
2249 Certain response headers cannot be modified by an Application. For a
2250 complete list of these headers please see:
2252 https://developers.google.com/appengine/docs/python/tools/webapp/responseclass#Disallowed_HTTP_Response_Headers
2254 This rewriter simply removes those headers.
2256 for h in _IGNORE_RESPONSE_HEADERS:
2257 if h in response.headers:
2258 del response.headers[h]
2261 def ValidHeadersRewriter(response):
2262 """Remove invalid response headers.
2264 Response headers must be printable ascii characters. This is enforced in
2265 production by http_proto.cc IsValidHeader.
2267 This rewriter will remove headers that contain non ascii characters.
2269 for (key, value) in response.headers.items():
2270 try:
2271 key.decode('ascii')
2272 value.decode('ascii')
2273 except UnicodeDecodeError:
2274 del response.headers[key]
2277 def ParseStatusRewriter(response):
2278 """Parse status header, if it exists.
2280 Handles the server-side 'status' header, which instructs the server to change
2281 the HTTP response code accordingly. Handles the 'location' header, which
2282 issues an HTTP 302 redirect to the client. Also corrects the 'content-length'
2283 header to reflect actual content length in case extra information has been
2284 appended to the response body.
2286 If the 'status' header supplied by the client is invalid, this method will
2287 set the response to a 500 with an error message as content.
2289 location_value = response.headers.getheader('location')
2290 status_value = response.headers.getheader('status')
2291 if status_value:
2292 response_status = status_value
2293 del response.headers['status']
2294 elif location_value:
2295 response_status = '%d Redirecting' % httplib.FOUND
2296 else:
2297 return response
2299 status_parts = response_status.split(' ', 1)
2300 response.status_code, response.status_message = (status_parts + [''])[:2]
2301 try:
2302 response.status_code = int(response.status_code)
2303 except ValueError:
2304 response.status_code = 500
2305 response.body = cStringIO.StringIO(
2306 'Error: Invalid "status" header value returned.')
2309 def GetAllHeaders(message, name):
2310 """Get all headers of a given name in a message.
2312 Args:
2313 message: A mimetools.Message object.
2314 name: The name of the header.
2316 Yields:
2317 A sequence of values of all headers with the given name.
2319 for header_line in message.getallmatchingheaders(name):
2320 yield header_line.split(':', 1)[1].strip()
2323 def CacheRewriter(response):
2324 """Update the cache header."""
2327 if response.status_code == httplib.NOT_MODIFIED:
2328 return
2330 if not 'Cache-Control' in response.headers:
2331 response.headers['Cache-Control'] = 'no-cache'
2332 if not 'Expires' in response.headers:
2333 response.headers['Expires'] = 'Fri, 01 Jan 1990 00:00:00 GMT'
2336 if 'Set-Cookie' in response.headers:
2340 current_date = time.time()
2341 expires = response.headers.get('Expires')
2342 reset_expires = True
2343 if expires:
2344 expires_time = email.Utils.parsedate(expires)
2345 if expires_time:
2346 reset_expires = calendar.timegm(expires_time) >= current_date
2347 if reset_expires:
2348 response.headers['Expires'] = time.strftime('%a, %d %b %Y %H:%M:%S GMT',
2349 time.gmtime(current_date))
2353 cache_directives = []
2354 for header in GetAllHeaders(response.headers, 'Cache-Control'):
2355 cache_directives.extend(v.strip() for v in header.split(','))
2356 cache_directives = [d for d in cache_directives if d != 'public']
2357 if not NON_PUBLIC_CACHE_CONTROLS.intersection(cache_directives):
2358 cache_directives.append('private')
2359 response.headers['Cache-Control'] = ', '.join(cache_directives)
2362 def _RemainingDataSize(input_buffer):
2363 """Computes how much data is remaining in the buffer.
2365 It leaves the buffer in its initial state.
2367 Args:
2368 input_buffer: a file-like object with seek and tell methods.
2370 Returns:
2371 integer representing how much data is remaining in the buffer.
2373 current_position = input_buffer.tell()
2374 input_buffer.seek(0, 2)
2375 remaining_data_size = input_buffer.tell() - current_position
2376 input_buffer.seek(current_position)
2377 return remaining_data_size
2380 def ContentLengthRewriter(response, request_headers, env_dict):
2381 """Rewrite the Content-Length header.
2383 Even though Content-Length is not a user modifiable header, App Engine
2384 sends a correct Content-Length to the user based on the actual response.
2387 if env_dict and env_dict.get('REQUEST_METHOD', '') == 'HEAD':
2388 return
2391 if response.status_code != httplib.NOT_MODIFIED:
2394 response.headers['Content-Length'] = str(_RemainingDataSize(response.body))
2395 elif 'Content-Length' in response.headers:
2396 del response.headers['Content-Length']
2399 def CreateResponseRewritersChain():
2400 """Create the default response rewriter chain.
2402 A response rewriter is the a function that gets a final chance to change part
2403 of the dev_appservers response. A rewriter is not like a dispatcher in that
2404 it is called after every request has been handled by the dispatchers
2405 regardless of which dispatcher was used.
2407 The order in which rewriters are registered will be the order in which they
2408 are used to rewrite the response. Modifications from earlier rewriters
2409 are used as input to later rewriters.
2411 A response rewriter is a function that can rewrite the request in any way.
2412 Thefunction can returned modified values or the original values it was
2413 passed.
2415 A rewriter function has the following parameters and return values:
2417 Args:
2418 status_code: Status code of response from dev_appserver or previous
2419 rewriter.
2420 status_message: Text corresponding to status code.
2421 headers: mimetools.Message instance with parsed headers. NOTE: These
2422 headers can contain its own 'status' field, but the default
2423 dev_appserver implementation will remove this. Future rewriters
2424 should avoid re-introducing the status field and return new codes
2425 instead.
2426 body: File object containing the body of the response. This position of
2427 this file may not be at the start of the file. Any content before the
2428 files position is considered not to be part of the final body.
2430 Returns:
2431 An AppServerResponse instance.
2433 Returns:
2434 List of response rewriters.
2436 rewriters = [ParseStatusRewriter,
2437 dev_appserver_blobstore.DownloadRewriter,
2438 IgnoreHeadersRewriter,
2439 ValidHeadersRewriter,
2440 CacheRewriter,
2441 ContentLengthRewriter,
2443 return rewriters
2447 def RewriteResponse(response_file,
2448 response_rewriters=None,
2449 request_headers=None,
2450 env_dict=None):
2451 """Allows final rewrite of dev_appserver response.
2453 This function receives the unparsed HTTP response from the application
2454 or internal handler, parses out the basic structure and feeds that structure
2455 in to a chain of response rewriters.
2457 It also makes sure the final HTTP headers are properly terminated.
2459 For more about response rewriters, please see documentation for
2460 CreateResponeRewritersChain.
2462 Args:
2463 response_file: File-like object containing the full HTTP response including
2464 the response code, all headers, and the request body.
2465 response_rewriters: A list of response rewriters. If none is provided it
2466 will create a new chain using CreateResponseRewritersChain.
2467 request_headers: Original request headers.
2468 env_dict: Environment dictionary.
2470 Returns:
2471 An AppServerResponse instance configured with the rewritten response.
2473 if response_rewriters is None:
2474 response_rewriters = CreateResponseRewritersChain()
2476 response = AppServerResponse(response_file)
2477 for response_rewriter in response_rewriters:
2480 if response_rewriter.func_code.co_argcount == 1:
2481 response_rewriter(response)
2482 elif response_rewriter.func_code.co_argcount == 2:
2483 response_rewriter(response, request_headers)
2484 else:
2485 response_rewriter(response, request_headers, env_dict)
2487 return response
2492 class ModuleManager(object):
2493 """Manages loaded modules in the runtime.
2495 Responsible for monitoring and reporting about file modification times.
2496 Modules can be loaded from source or precompiled byte-code files. When a
2497 file has source code, the ModuleManager monitors the modification time of
2498 the source file even if the module itself is loaded from byte-code.
2501 def __init__(self, modules):
2502 """Initializer.
2504 Args:
2505 modules: Dictionary containing monitored modules.
2507 self._modules = modules
2509 self._default_modules = self._modules.copy()
2511 self._save_path_hooks = sys.path_hooks[:]
2520 self._modification_times = {}
2523 self._dirty = True
2525 @staticmethod
2526 def GetModuleFile(module, is_file=os.path.isfile):
2527 """Helper method to try to determine modules source file.
2529 Args:
2530 module: Module object to get file for.
2531 is_file: Function used to determine if a given path is a file.
2533 Returns:
2534 Path of the module's corresponding Python source file if it exists, or
2535 just the module's compiled Python file. If the module has an invalid
2536 __file__ attribute, None will be returned.
2538 module_file = getattr(module, '__file__', None)
2539 if module_file is None:
2540 return None
2543 source_file = module_file[:module_file.rfind('py') + 2]
2545 if is_file(source_file):
2546 return source_file
2547 return module.__file__
2549 def AreModuleFilesModified(self):
2550 """Determines if any monitored files have been modified.
2552 Returns:
2553 True if one or more files have been modified, False otherwise.
2555 self._dirty = True
2556 for name, (mtime, fname) in self._modification_times.iteritems():
2558 if name not in self._modules:
2559 continue
2561 module = self._modules[name]
2563 try:
2565 if mtime != os.path.getmtime(fname):
2566 self._dirty = True
2567 return True
2568 except OSError, e:
2570 if e.errno in FILE_MISSING_EXCEPTIONS:
2571 self._dirty = True
2572 return True
2573 raise e
2575 return False
2577 def UpdateModuleFileModificationTimes(self):
2578 """Records the current modification times of all monitored modules."""
2579 if not self._dirty:
2580 return
2582 self._modification_times.clear()
2583 for name, module in self._modules.items():
2584 if not isinstance(module, types.ModuleType):
2585 continue
2586 module_file = self.GetModuleFile(module)
2587 if not module_file:
2588 continue
2589 try:
2590 self._modification_times[name] = (os.path.getmtime(module_file),
2591 module_file)
2592 except OSError, e:
2593 if e.errno not in FILE_MISSING_EXCEPTIONS:
2594 raise e
2596 self._dirty = False
2598 def ResetModules(self):
2599 """Clear modules so that when request is run they are reloaded."""
2600 lib_config._default_registry.reset()
2601 self._modules.clear()
2602 self._modules.update(self._default_modules)
2605 sys.path_hooks[:] = self._save_path_hooks
2608 sys.meta_path = []
2614 apiproxy_stub_map.apiproxy.GetPreCallHooks().Clear()
2615 apiproxy_stub_map.apiproxy.GetPostCallHooks().Clear()
2621 def GetVersionObject(isfile=os.path.isfile, open_fn=open):
2622 """Gets the version of the SDK by parsing the VERSION file.
2624 Args:
2625 isfile: used for testing.
2626 open_fn: Used for testing.
2628 Returns:
2629 A Yaml object or None if the VERSION file does not exist.
2631 version_filename = os.path.join(os.path.dirname(google.appengine.__file__),
2632 VERSION_FILE)
2633 if not isfile(version_filename):
2634 logging.error('Could not find version file at %s', version_filename)
2635 return None
2637 version_fh = open_fn(version_filename, 'r')
2638 try:
2639 version = yaml.safe_load(version_fh)
2640 finally:
2641 version_fh.close()
2643 return version
2648 def _ClearTemplateCache(module_dict=sys.modules):
2649 """Clear template cache in webapp.template module.
2651 Attempts to load template module. Ignores failure. If module loads, the
2652 template cache is cleared.
2654 Args:
2655 module_dict: Used for dependency injection.
2657 template_module = module_dict.get('google.appengine.ext.webapp.template')
2658 if template_module is not None:
2659 template_module.template_cache.clear()
2664 def CreateRequestHandler(root_path,
2665 login_url,
2666 static_caching=True,
2667 default_partition=None,
2668 interactive_console=True):
2669 """Creates a new BaseHTTPRequestHandler sub-class.
2671 This class will be used with the Python BaseHTTPServer module's HTTP server.
2673 Python's built-in HTTP server does not support passing context information
2674 along to instances of its request handlers. This function gets around that
2675 by creating a sub-class of the handler in a closure that has access to
2676 this context information.
2678 Args:
2679 root_path: Path to the root of the application running on the server.
2680 login_url: Relative URL which should be used for handling user logins.
2681 static_caching: True if browser caching of static files should be allowed.
2682 default_partition: Default partition to use in the application id.
2683 interactive_console: Whether to add the interactive console.
2685 Returns:
2686 Sub-class of BaseHTTPRequestHandler.
2708 application_module_dict = SetupSharedModules(sys.modules)
2711 application_config_cache = AppConfigCache()
2713 class DevAppServerRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
2714 """Dispatches URLs using patterns from a URLMatcher.
2716 The URLMatcher is created by loading an application's configuration file.
2717 Executes CGI scripts in the local process so the scripts can use mock
2718 versions of APIs.
2720 HTTP requests that correctly specify a user info cookie
2721 (dev_appserver_login.COOKIE_NAME) will have the 'USER_EMAIL' environment
2722 variable set accordingly. If the user is also an admin, the
2723 'USER_IS_ADMIN' variable will exist and be set to '1'. If the user is not
2724 logged in, 'USER_EMAIL' will be set to the empty string.
2726 On each request, raises an InvalidAppConfigError exception if the
2727 application configuration file in the directory specified by the root_path
2728 argument is invalid.
2730 server_version = 'Development/1.0'
2735 module_dict = application_module_dict
2736 module_manager = ModuleManager(application_module_dict)
2739 config_cache = application_config_cache
2741 rewriter_chain = CreateResponseRewritersChain()
2743 channel_poll_path_re = re.compile(
2744 dev_appserver_channel.CHANNEL_POLL_PATTERN)
2746 def __init__(self, *args, **kwargs):
2747 """Initializer.
2749 Args:
2750 args: Positional arguments passed to the superclass constructor.
2751 kwargs: Keyword arguments passed to the superclass constructor.
2753 self._log_record_writer = apiproxy_stub_map.apiproxy.GetStub('logservice')
2754 BaseHTTPServer.BaseHTTPRequestHandler.__init__(self, *args, **kwargs)
2756 def version_string(self):
2757 """Returns server's version string used for Server HTTP header."""
2759 return self.server_version
2761 def do_GET(self):
2762 """Handle GET requests."""
2763 if self._HasNoBody('GET'):
2764 self._HandleRequest()
2766 def do_POST(self):
2767 """Handles POST requests."""
2768 self._HandleRequest()
2770 def do_PUT(self):
2771 """Handle PUT requests."""
2772 self._HandleRequest()
2774 def do_HEAD(self):
2775 """Handle HEAD requests."""
2776 if self._HasNoBody('HEAD'):
2777 self._HandleRequest()
2779 def do_OPTIONS(self):
2780 """Handles OPTIONS requests."""
2781 self._HandleRequest()
2783 def do_DELETE(self):
2784 """Handle DELETE requests."""
2785 self._HandleRequest()
2787 def do_TRACE(self):
2788 """Handles TRACE requests."""
2789 if self._HasNoBody('TRACE'):
2790 self._HandleRequest()
2792 def _HasNoBody(self, method):
2793 """Check for request body in HTTP methods where no body is permitted.
2795 If a request body is present a 400 (Invalid request) response is sent.
2797 Args:
2798 method: The request method.
2800 Returns:
2801 True if no request body is present, False otherwise.
2805 content_length = int(self.headers.get('content-length', 0))
2806 if content_length:
2807 body = self.rfile.read(content_length)
2808 logging.warning('Request body in %s is not permitted: %s', method, body)
2809 self.send_response(httplib.BAD_REQUEST)
2810 return False
2811 return True
2813 def _Dispatch(self, dispatcher, socket_infile, outfile, env_dict):
2814 """Copy request data from socket and dispatch.
2816 Args:
2817 dispatcher: Dispatcher to handle request (MatcherDispatcher).
2818 socket_infile: Original request file stream.
2819 outfile: Output file to write response to.
2820 env_dict: Environment dictionary.
2824 request_descriptor, request_file_name = tempfile.mkstemp('.tmp',
2825 'request.')
2827 try:
2828 request_file = open(request_file_name, 'wb')
2829 try:
2830 CopyStreamPart(self.rfile,
2831 request_file,
2832 int(self.headers.get('content-length', 0)))
2833 finally:
2834 request_file.close()
2836 request_file = open(request_file_name, 'rb')
2837 try:
2838 app_server_request = AppServerRequest(self.path,
2839 None,
2840 self.headers,
2841 request_file)
2842 dispatcher.Dispatch(app_server_request,
2843 outfile,
2844 base_env_dict=env_dict)
2845 finally:
2846 request_file.close()
2847 finally:
2848 try:
2849 os.close(request_descriptor)
2853 try:
2854 os.remove(request_file_name)
2855 except OSError, err:
2856 if getattr(err, 'winerror', 0) == os_compat.ERROR_SHARING_VIOLATION:
2857 logging.warning('Failed removing %s', request_file_name)
2858 else:
2859 raise
2860 except OSError, err:
2861 if err.errno != errno.ENOENT:
2862 raise
2864 def _HandleRequest(self):
2865 """Handles any type of request and prints exceptions if they occur."""
2869 host_name = self.headers.get('host') or self.server.server_name
2870 server_name = host_name.split(':', 1)[0]
2872 env_dict = {
2873 'REQUEST_METHOD': self.command,
2874 'REMOTE_ADDR': self.client_address[0],
2875 'SERVER_SOFTWARE': self.server_version,
2876 'SERVER_NAME': server_name,
2877 'SERVER_PROTOCOL': self.protocol_version,
2878 'SERVER_PORT': str(self.server.server_port),
2881 full_url = GetFullURL(server_name, self.server.server_port, self.path)
2882 if len(full_url) > MAX_URL_LENGTH:
2883 msg = 'Requested URI too long: %s' % full_url
2884 logging.error(msg)
2885 self.send_response(httplib.REQUEST_URI_TOO_LONG, msg)
2886 return
2888 tbhandler = cgitb.Hook(file=self.wfile).handle
2889 try:
2891 config, explicit_matcher, from_cache = LoadAppConfig(
2892 root_path, self.module_dict, cache=self.config_cache,
2893 static_caching=static_caching, default_partition=default_partition)
2896 if not from_cache:
2897 self.module_manager.ResetModules()
2901 implicit_matcher = CreateImplicitMatcher(config,
2902 self.module_dict,
2903 root_path,
2904 login_url)
2906 if self.path.startswith('/_ah/admin'):
2909 if any((handler.url == '/_ah/datastore_admin.*'
2910 for handler in config.handlers)):
2911 self.headers['X-AppEngine-Datastore-Admin-Enabled'] = 'True'
2912 self.headers['X-AppEngine-Interactive-Console-Enabled'] = str(
2913 interactive_console)
2915 if config.api_version != API_VERSION:
2916 logging.error(
2917 "API versions cannot be switched dynamically: %r != %r",
2918 config.api_version, API_VERSION)
2919 sys.exit(1)
2921 (exclude, service_match) = ReservedPathFilter(
2922 config.inbound_services).ExcludePath(self.path)
2923 if exclude:
2924 logging.warning(
2925 'Request to %s excluded because %s is not enabled '
2926 'in inbound_services in app.yaml' % (self.path, service_match))
2927 self.send_response(httplib.NOT_FOUND)
2928 return
2930 if config.runtime == 'go':
2932 from google.appengine.ext import go
2933 go.APP_CONFIG = config
2935 version = GetVersionObject()
2936 env_dict['SDK_VERSION'] = version['release']
2937 env_dict['CURRENT_VERSION_ID'] = config.version + ".1"
2938 env_dict['APPLICATION_ID'] = config.application
2939 env_dict['DEFAULT_VERSION_HOSTNAME'] = self.server.frontend_hostport
2940 env_dict['APPENGINE_RUNTIME'] = config.runtime
2941 if config.runtime == 'python27' and config.threadsafe:
2942 env_dict['_AH_THREADSAFE'] = '1'
2946 global _request_time
2947 global _request_id
2948 _request_time = time.time()
2949 _request_id += 1
2951 request_id_hash = _generate_request_id_hash()
2952 env_dict['REQUEST_ID_HASH'] = request_id_hash
2953 os.environ['REQUEST_ID_HASH'] = request_id_hash
2956 multiprocess.GlobalProcess().UpdateEnv(env_dict)
2958 cookies = ', '.join(self.headers.getheaders('cookie'))
2959 email_addr, admin, user_id = dev_appserver_login.GetUserInfo(cookies)
2961 self._log_record_writer.start_request(
2962 request_id=None,
2963 user_request_id=_GenerateRequestLogId(),
2964 ip=env_dict['REMOTE_ADDR'],
2965 app_id=env_dict['APPLICATION_ID'],
2966 version_id=env_dict['CURRENT_VERSION_ID'],
2967 nickname=email_addr.split('@')[0],
2968 user_agent=self.headers.get('user-agent', ''),
2969 host=host_name,
2970 method=self.command,
2971 resource=self.path,
2972 http_version=self.request_version)
2974 dispatcher = MatcherDispatcher(config, login_url, self.module_manager,
2975 [implicit_matcher, explicit_matcher])
2980 if multiprocess.GlobalProcess().HandleRequest(self):
2981 return
2983 outfile = cStringIO.StringIO()
2984 try:
2985 self._Dispatch(dispatcher, self.rfile, outfile, env_dict)
2986 finally:
2987 self.module_manager.UpdateModuleFileModificationTimes()
2989 outfile.flush()
2990 outfile.seek(0)
2992 response = RewriteResponse(outfile, self.rewriter_chain, self.headers,
2993 env_dict)
2995 runtime_response_size = _RemainingDataSize(response.body)
2996 if self.command == 'HEAD' and runtime_response_size > 0:
2997 logging.warning('Dropping unexpected body in response to HEAD '
2998 'request')
2999 response.body = cStringIO.StringIO('')
3000 elif (not response.large_response and
3001 runtime_response_size > MAX_RUNTIME_RESPONSE_SIZE):
3002 logging.error('Response too large: %d, max is %d',
3003 runtime_response_size, MAX_RUNTIME_RESPONSE_SIZE)
3006 response.status_code = 500
3007 response.status_message = 'Forbidden'
3009 new_response = ('HTTP response was too large: %d. '
3010 'The limit is: %d.'
3011 % (runtime_response_size,
3012 MAX_RUNTIME_RESPONSE_SIZE))
3013 response.headers['Content-Length'] = str(len(new_response))
3014 response.body = cStringIO.StringIO(new_response)
3017 multiprocess.GlobalProcess().RequestComplete(self, response)
3019 except yaml_errors.EventListenerError, e:
3020 title = 'Fatal error when loading application configuration'
3021 msg = '%s:\n%s' % (title, str(e))
3022 logging.error(msg)
3023 self.send_response(httplib.INTERNAL_SERVER_ERROR, title)
3024 self.wfile.write('Content-Type: text/html\r\n\r\n')
3025 self.wfile.write('<pre>%s</pre>' % cgi.escape(msg))
3026 except KeyboardInterrupt, e:
3030 logging.info('Server interrupted by user, terminating')
3031 self.server.stop_serving_forever()
3032 except CompileError, e:
3033 msg = 'Compile error:\n' + e.text + '\n'
3034 logging.error(msg)
3035 self.send_response(httplib.INTERNAL_SERVER_ERROR, 'Compile error')
3036 self.wfile.write('Content-Type: text/plain; charset=utf-8\r\n\r\n')
3037 self.wfile.write(msg)
3038 except ExecuteError, e:
3039 logging.error(e.text)
3040 self.send_response(httplib.INTERNAL_SERVER_ERROR, 'Execute error')
3041 self.wfile.write('Content-Type: text/html; charset=utf-8\r\n\r\n')
3042 self.wfile.write('<title>App failure</title>\n')
3043 self.wfile.write(e.text + '\n<pre>\n')
3044 for l in e.log:
3045 self.wfile.write(cgi.escape(l))
3046 self.wfile.write('</pre>\n')
3047 except:
3048 msg = 'Exception encountered handling request'
3049 logging.exception(msg)
3050 self.send_response(httplib.INTERNAL_SERVER_ERROR, msg)
3051 tbhandler()
3052 else:
3053 try:
3054 self.send_response(response.status_code, response.status_message)
3055 self.wfile.write(response.header_data)
3056 self.wfile.write('\r\n')
3058 shutil.copyfileobj(response.body, self.wfile, COPY_BLOCK_SIZE)
3059 except (IOError, OSError), e:
3070 if e.errno not in [errno.EPIPE, os_compat.WSAECONNABORTED]:
3071 raise e
3072 except socket.error, e:
3073 if len(e.args) >= 1 and e.args[0] != errno.EPIPE:
3074 raise e
3076 def log_error(self, format, *args):
3077 """Redirect error messages through the logging module."""
3078 logging.error(format, *args)
3080 def log_message(self, format, *args):
3081 """Redirect log messages through the logging module."""
3084 if hasattr(self, 'path') and self.channel_poll_path_re.match(self.path):
3085 logging.debug(format, *args)
3086 else:
3087 logging.info(format, *args)
3089 def log_request(self, code='-', size='-'):
3090 """Indicate that this request has completed."""
3091 BaseHTTPServer.BaseHTTPRequestHandler.log_request(self, code, size)
3092 if code == '-':
3093 code = 0
3094 if size == '-':
3095 size = 0
3098 logservice.logs_buffer().flush()
3099 self._log_record_writer.end_request(None, code, size)
3100 return DevAppServerRequestHandler
3105 def ReadAppConfig(appinfo_path, parse_app_config=appinfo_includes.Parse):
3106 """Reads app.yaml file and returns its app id and list of URLMap instances.
3108 Args:
3109 appinfo_path: String containing the path to the app.yaml file.
3110 parse_app_config: Used for dependency injection.
3112 Returns:
3113 AppInfoExternal instance.
3115 Raises:
3116 If the config file could not be read or the config does not contain any
3117 URLMap instances, this function will raise an InvalidAppConfigError
3118 exception.
3120 try:
3121 appinfo_file = file(appinfo_path, 'r')
3122 except IOError, unused_e:
3123 raise InvalidAppConfigError(
3124 'Application configuration could not be read from "%s"' % appinfo_path)
3125 try:
3128 return parse_app_config(appinfo_file)
3129 finally:
3130 appinfo_file.close()
3133 def _StaticFilePathRe(url_map):
3134 """Returns a regular expression string that matches static file paths.
3136 Args:
3137 url_map: A fully initialized static_files or static_dir appinfo.URLMap
3138 instance.
3140 Returns:
3141 The regular expression matches paths, relative to the application's root
3142 directory, of files that this static handler serves. re.compile should
3143 accept the returned string.
3145 Raises:
3146 AssertionError: The url_map argument was not an URLMap for a static handler.
3148 handler_type = url_map.GetHandlerType()
3151 if handler_type == 'static_files':
3152 return url_map.upload + '$'
3154 elif handler_type == 'static_dir':
3155 path = url_map.static_dir.rstrip(os.path.sep)
3156 return path + re.escape(os.path.sep) + r'(.*)'
3158 assert False, 'This property only applies to static handlers.'
3161 def CreateURLMatcherFromMaps(config,
3162 root_path,
3163 url_map_list,
3164 module_dict,
3165 default_expiration,
3166 create_url_matcher=URLMatcher,
3167 create_cgi_dispatcher=CGIDispatcher,
3168 create_file_dispatcher=FileDispatcher,
3169 create_path_adjuster=PathAdjuster,
3170 normpath=os.path.normpath):
3171 """Creates a URLMatcher instance from URLMap.
3173 Creates all of the correct URLDispatcher instances to handle the various
3174 content types in the application configuration.
3176 Args:
3177 config: AppInfoExternal instance representing the parsed app.yaml file.
3178 root_path: Path to the root of the application running on the server.
3179 url_map_list: List of appinfo.URLMap objects to initialize this
3180 matcher with. Can be an empty list if you would like to add patterns
3181 manually or use config.handlers as a default.
3182 module_dict: Dictionary in which application-loaded modules should be
3183 preserved between requests. This dictionary must be separate from the
3184 sys.modules dictionary.
3185 default_expiration: String describing default expiration time for browser
3186 based caching of static files. If set to None this disallows any
3187 browser caching of static content.
3188 create_url_matcher: Used for dependency injection.
3189 create_cgi_dispatcher: Used for dependency injection.
3190 create_file_dispatcher: Used for dependency injection.
3191 create_path_adjuster: Used for dependency injection.
3192 normpath: Used for dependency injection.
3194 Returns:
3195 Instance of URLMatcher with the supplied URLMap objects properly loaded.
3197 Raises:
3198 InvalidAppConfigError: if a handler is an unknown type.
3200 if config and config.handlers and not url_map_list:
3201 url_map_list = config.handlers
3202 url_matcher = create_url_matcher()
3203 path_adjuster = create_path_adjuster(root_path)
3204 cgi_dispatcher = create_cgi_dispatcher(config, module_dict,
3205 root_path, path_adjuster)
3206 static_file_config_matcher = StaticFileConfigMatcher(url_map_list,
3207 default_expiration)
3208 file_dispatcher = create_file_dispatcher(config, path_adjuster,
3209 static_file_config_matcher)
3211 FakeFile.SetStaticFileConfigMatcher(static_file_config_matcher)
3213 for url_map in url_map_list:
3214 admin_only = url_map.login == appinfo.LOGIN_ADMIN
3215 requires_login = url_map.login == appinfo.LOGIN_REQUIRED or admin_only
3216 auth_fail_action = url_map.auth_fail_action
3218 handler_type = url_map.GetHandlerType()
3219 if handler_type == appinfo.HANDLER_SCRIPT:
3220 dispatcher = cgi_dispatcher
3221 elif handler_type in (appinfo.STATIC_FILES, appinfo.STATIC_DIR):
3222 dispatcher = file_dispatcher
3223 else:
3225 raise InvalidAppConfigError('Unknown handler type "%s"' % handler_type)
3228 regex = url_map.url
3229 path = url_map.GetHandler()
3230 if handler_type == appinfo.STATIC_DIR:
3231 if regex[-1] == r'/':
3232 regex = regex[:-1]
3233 if path[-1] == os.path.sep:
3234 path = path[:-1]
3235 regex = '/'.join((re.escape(regex), '(.*)'))
3236 if os.path.sep == '\\':
3237 backref = r'\\1'
3238 else:
3239 backref = r'\1'
3240 path = (normpath(path).replace('\\', '\\\\') +
3241 os.path.sep + backref)
3243 url_matcher.AddURL(regex,
3244 dispatcher,
3245 path,
3246 requires_login, admin_only, auth_fail_action)
3248 return url_matcher
3251 class AppConfigCache(object):
3252 """Cache used by LoadAppConfig.
3254 If given to LoadAppConfig instances of this class are used to cache contents
3255 of the app config (app.yaml or app.yml) and the Matcher created from it.
3257 Code outside LoadAppConfig should treat instances of this class as opaque
3258 objects and not access its members.
3262 path = None
3267 mtime = None
3269 config = None
3271 matcher = None
3274 def LoadAppConfig(root_path,
3275 module_dict,
3276 cache=None,
3277 static_caching=True,
3278 read_app_config=ReadAppConfig,
3279 create_matcher=CreateURLMatcherFromMaps,
3280 default_partition=None):
3281 """Creates a Matcher instance for an application configuration file.
3283 Raises an InvalidAppConfigError exception if there is anything wrong with
3284 the application configuration file.
3286 Args:
3287 root_path: Path to the root of the application to load.
3288 module_dict: Dictionary in which application-loaded modules should be
3289 preserved between requests. This dictionary must be separate from the
3290 sys.modules dictionary.
3291 cache: Instance of AppConfigCache or None.
3292 static_caching: True if browser caching of static files should be allowed.
3293 read_app_config: Used for dependency injection.
3294 create_matcher: Used for dependency injection.
3295 default_partition: Default partition to use for the appid.
3297 Returns:
3298 tuple: (AppInfoExternal, URLMatcher, from_cache)
3300 Raises:
3301 AppConfigNotFound: if an app.yaml file cannot be found.
3303 for appinfo_path in [os.path.join(root_path, 'app.yaml'),
3304 os.path.join(root_path, 'app.yml')]:
3306 if os.path.isfile(appinfo_path):
3307 if cache is not None:
3309 mtime = os.path.getmtime(appinfo_path)
3310 if cache.path == appinfo_path and cache.mtime == mtime:
3311 return (cache.config, cache.matcher, True)
3314 cache.config = cache.matcher = cache.path = None
3315 cache.mtime = mtime
3317 config = read_app_config(appinfo_path, appinfo_includes.Parse)
3319 if config.application:
3320 config.application = AppIdWithDefaultPartition(config.application,
3321 default_partition)
3322 multiprocess.GlobalProcess().NewAppInfo(config)
3324 if static_caching:
3325 if config.default_expiration:
3326 default_expiration = config.default_expiration
3327 else:
3330 default_expiration = '0'
3331 else:
3333 default_expiration = None
3335 matcher = create_matcher(config,
3336 root_path,
3337 config.handlers,
3338 module_dict,
3339 default_expiration)
3341 FakeFile.SetSkippedFiles(config.skip_files)
3343 if cache is not None:
3344 cache.path = appinfo_path
3345 cache.config = config
3346 cache.matcher = matcher
3348 return config, matcher, False
3350 raise AppConfigNotFoundError(
3351 'Could not find app.yaml in "%s".' % (root_path,))
3354 class ReservedPathFilter():
3355 """Checks a path against a set of inbound_services."""
3357 reserved_paths = {
3358 '/_ah/channel/connect': 'channel_presence',
3359 '/_ah/channel/disconnect': 'channel_presence'
3362 def __init__(self, inbound_services):
3363 self.inbound_services = inbound_services
3365 def ExcludePath(self, path):
3366 """Check to see if this is a service url and matches inbound_services."""
3367 skip = False
3368 for reserved_path in self.reserved_paths.keys():
3369 if path.startswith(reserved_path):
3370 if (not self.inbound_services or
3371 self.reserved_paths[reserved_path] not in self.inbound_services):
3372 return (True, self.reserved_paths[reserved_path])
3374 return (False, None)
3377 def CreateInboundServiceFilter(inbound_services):
3378 return ReservedPathFilter(inbound_services)
3381 def ReadCronConfig(croninfo_path, parse_cron_config=croninfo.LoadSingleCron):
3382 """Reads cron.yaml file and returns a list of CronEntry instances.
3384 Args:
3385 croninfo_path: String containing the path to the cron.yaml file.
3386 parse_cron_config: Used for dependency injection.
3388 Returns:
3389 A CronInfoExternal object.
3391 Raises:
3392 If the config file is unreadable, empty or invalid, this function will
3393 raise an InvalidAppConfigError or a MalformedCronConfiguration exception.
3395 try:
3396 croninfo_file = file(croninfo_path, 'r')
3397 except IOError, e:
3398 raise InvalidAppConfigError(
3399 'Cron configuration could not be read from "%s": %s'
3400 % (croninfo_path, e))
3401 try:
3402 return parse_cron_config(croninfo_file)
3403 finally:
3404 croninfo_file.close()
3409 def _RemoveFile(file_path):
3410 if file_path and os.path.lexists(file_path):
3411 logging.info('Attempting to remove file at %s', file_path)
3412 try:
3413 os.remove(file_path)
3414 except OSError, e:
3415 logging.warning('Removing file failed: %s', e)
3418 def SetupStubs(app_id, **config):
3419 """Sets up testing stubs of APIs.
3421 Args:
3422 app_id: Application ID being served.
3423 config: keyword arguments.
3425 Keywords:
3426 root_path: Root path to the directory of the application which should
3427 contain the app.yaml, index.yaml, and queue.yaml files.
3428 login_url: Relative URL which should be used for handling user login/logout.
3429 blobstore_path: Path to the directory to store Blobstore blobs in.
3430 datastore_path: Path to the file to store Datastore file stub data in.
3431 prospective_search_path: Path to the file to store Prospective Search stub
3432 data in.
3433 use_sqlite: Use the SQLite stub for the datastore.
3434 auto_id_policy: How datastore stub assigns IDs, sequential or scattered.
3435 high_replication: Use the high replication consistency model
3436 history_path: DEPRECATED, No-op.
3437 clear_datastore: If the datastore should be cleared on startup.
3438 smtp_host: SMTP host used for sending test mail.
3439 smtp_port: SMTP port.
3440 smtp_user: SMTP user.
3441 smtp_password: SMTP password.
3442 mysql_host: MySQL host.
3443 mysql_port: MySQL port.
3444 mysql_user: MySQL user.
3445 mysql_password: MySQL password.
3446 mysql_socket: MySQL socket.
3447 appidentity_email_address: Email address for service account substitute.
3448 appidentity_private_key_path: Path to private key for service account sub.
3449 enable_sendmail: Whether to use sendmail as an alternative to SMTP.
3450 show_mail_body: Whether to log the body of emails.
3451 remove: Used for dependency injection.
3452 disable_task_running: True if tasks should not automatically run after
3453 they are enqueued.
3454 task_retry_seconds: How long to wait after an auto-running task before it
3455 is tried again.
3456 logs_path: Path to the file to store the logs data in.
3457 trusted: True if this app can access data belonging to other apps. This
3458 behavior is different from the real app server and should be left False
3459 except for advanced uses of dev_appserver.
3460 port: The port that this dev_appserver is bound to. Defaults to 8080
3461 address: The host that this dev_appsever is running on. Defaults to
3462 localhost.
3463 search_index_path: Path to the file to store search indexes in.
3464 clear_search_index: If the search indices should be cleared on startup.
3470 root_path = config.get('root_path', None)
3471 login_url = config['login_url']
3472 blobstore_path = config['blobstore_path']
3473 datastore_path = config['datastore_path']
3474 clear_datastore = config['clear_datastore']
3475 prospective_search_path = config.get('prospective_search_path', '')
3476 clear_prospective_search = config.get('clear_prospective_search', False)
3477 use_sqlite = config.get('use_sqlite', False)
3478 auto_id_policy = config.get('auto_id_policy', datastore_stub_util.SEQUENTIAL)
3479 high_replication = config.get('high_replication', False)
3480 require_indexes = config.get('require_indexes', False)
3481 mysql_host = config.get('mysql_host', None)
3482 mysql_port = config.get('mysql_port', 3306)
3483 mysql_user = config.get('mysql_user', None)
3484 mysql_password = config.get('mysql_password', None)
3485 mysql_socket = config.get('mysql_socket', None)
3486 smtp_host = config.get('smtp_host', None)
3487 smtp_port = config.get('smtp_port', 25)
3488 smtp_user = config.get('smtp_user', '')
3489 smtp_password = config.get('smtp_password', '')
3490 enable_sendmail = config.get('enable_sendmail', False)
3491 show_mail_body = config.get('show_mail_body', False)
3492 appidentity_email_address = config.get('appidentity_email_address', None)
3493 appidentity_private_key_path = config.get('appidentity_private_key_path', None)
3494 remove = config.get('remove', os.remove)
3495 disable_task_running = config.get('disable_task_running', False)
3496 task_retry_seconds = config.get('task_retry_seconds', 30)
3497 logs_path = config.get('logs_path', ':memory:')
3498 trusted = config.get('trusted', False)
3499 serve_port = config.get('port', 8080)
3500 serve_address = config.get('address', 'localhost')
3501 clear_search_index = config.get('clear_search_indexes', False)
3502 search_index_path = config.get('search_indexes_path', None)
3503 _use_atexit_for_datastore_stub = config.get('_use_atexit_for_datastore_stub',
3504 False)
3505 port_sqlite_data = config.get('port_sqlite_data', False)
3511 os.environ['APPLICATION_ID'] = app_id
3515 os.environ['REQUEST_ID_HASH'] = ''
3517 if clear_prospective_search and prospective_search_path:
3518 _RemoveFile(prospective_search_path)
3520 if clear_datastore:
3521 _RemoveFile(datastore_path)
3523 if clear_search_index:
3524 _RemoveFile(search_index_path)
3527 if multiprocess.GlobalProcess().MaybeConfigureRemoteDataApis():
3531 apiproxy_stub_map.apiproxy.RegisterStub(
3532 'logservice',
3533 logservice_stub.LogServiceStub(logs_path=':memory:'))
3534 else:
3541 apiproxy_stub_map.apiproxy = apiproxy_stub_map.APIProxyStubMap()
3543 apiproxy_stub_map.apiproxy.RegisterStub(
3544 'app_identity_service',
3545 app_identity_stub.AppIdentityServiceStub.Create(
3546 email_address=appidentity_email_address,
3547 private_key_path=appidentity_private_key_path))
3549 apiproxy_stub_map.apiproxy.RegisterStub(
3550 'capability_service',
3551 capability_stub.CapabilityServiceStub())
3553 if use_sqlite:
3554 if port_sqlite_data:
3555 try:
3556 PortAllEntities(datastore_path)
3557 except Error:
3558 logging.Error("Porting the data from the datastore file stub failed")
3559 raise
3561 datastore = datastore_sqlite_stub.DatastoreSqliteStub(
3562 app_id, datastore_path, require_indexes=require_indexes,
3563 trusted=trusted, root_path=root_path,
3564 use_atexit=_use_atexit_for_datastore_stub,
3565 auto_id_policy=auto_id_policy)
3566 else:
3567 logging.warning(FILE_STUB_DEPRECATION_MESSAGE)
3568 datastore = datastore_file_stub.DatastoreFileStub(
3569 app_id, datastore_path, require_indexes=require_indexes,
3570 trusted=trusted, root_path=root_path,
3571 use_atexit=_use_atexit_for_datastore_stub,
3572 auto_id_policy=auto_id_policy)
3574 if high_replication:
3575 datastore.SetConsistencyPolicy(
3576 datastore_stub_util.TimeBasedHRConsistencyPolicy())
3577 apiproxy_stub_map.apiproxy.ReplaceStub(
3578 'datastore_v3', datastore)
3580 apiproxy_stub_map.apiproxy.RegisterStub(
3581 'datastore_v4',
3582 datastore_v4_stub.DatastoreV4Stub(app_id))
3584 apiproxy_stub_map.apiproxy.RegisterStub(
3585 'mail',
3586 mail_stub.MailServiceStub(smtp_host,
3587 smtp_port,
3588 smtp_user,
3589 smtp_password,
3590 enable_sendmail=enable_sendmail,
3591 show_mail_body=show_mail_body,
3592 allow_tls=False))
3594 apiproxy_stub_map.apiproxy.RegisterStub(
3595 'memcache',
3596 memcache_stub.MemcacheServiceStub())
3598 apiproxy_stub_map.apiproxy.RegisterStub(
3599 'taskqueue',
3600 taskqueue_stub.TaskQueueServiceStub(
3601 root_path=root_path,
3602 auto_task_running=(not disable_task_running),
3603 task_retry_seconds=task_retry_seconds,
3604 default_http_server='%s:%s' % (serve_address, serve_port)))
3606 apiproxy_stub_map.apiproxy.RegisterStub(
3607 'urlfetch',
3608 urlfetch_stub.URLFetchServiceStub())
3610 apiproxy_stub_map.apiproxy.RegisterStub(
3611 'xmpp',
3612 xmpp_service_stub.XmppServiceStub())
3614 apiproxy_stub_map.apiproxy.RegisterStub(
3615 'logservice',
3616 logservice_stub.LogServiceStub(logs_path=logs_path))
3621 from google.appengine import api
3622 sys.modules['google.appengine.api.rdbms'] = rdbms_mysqldb
3623 api.rdbms = rdbms_mysqldb
3624 rdbms_mysqldb.SetConnectKwargs(host=mysql_host, port=mysql_port,
3625 user=mysql_user, passwd=mysql_password,
3626 unix_socket=mysql_socket)
3628 fixed_login_url = '%s?%s=%%s' % (login_url,
3629 dev_appserver_login.CONTINUE_PARAM)
3630 fixed_logout_url = '%s&%s' % (fixed_login_url,
3631 dev_appserver_login.LOGOUT_PARAM)
3637 apiproxy_stub_map.apiproxy.RegisterStub(
3638 'user',
3639 user_service_stub.UserServiceStub(login_url=fixed_login_url,
3640 logout_url=fixed_logout_url))
3642 apiproxy_stub_map.apiproxy.RegisterStub(
3643 'channel',
3644 channel_service_stub.ChannelServiceStub())
3646 apiproxy_stub_map.apiproxy.RegisterStub(
3647 'matcher',
3648 prospective_search_stub.ProspectiveSearchStub(
3649 prospective_search_path,
3650 apiproxy_stub_map.apiproxy.GetStub('taskqueue')))
3652 apiproxy_stub_map.apiproxy.RegisterStub(
3653 'remote_socket',
3654 _remote_socket_stub.RemoteSocketServiceStub())
3656 apiproxy_stub_map.apiproxy.RegisterStub(
3657 'search',
3658 simple_search_stub.SearchServiceStub(index_file=search_index_path))
3664 try:
3665 from google.appengine.api.images import images_stub
3666 host_prefix = 'http://%s:%d' % (serve_address, serve_port)
3667 apiproxy_stub_map.apiproxy.RegisterStub(
3668 'images',
3669 images_stub.ImagesServiceStub(host_prefix=host_prefix))
3670 except ImportError, e:
3671 logging.warning('Could not initialize images API; you are likely missing '
3672 'the Python "PIL" module. ImportError: %s', e)
3674 from google.appengine.api.images import images_not_implemented_stub
3675 apiproxy_stub_map.apiproxy.RegisterStub(
3676 'images',
3677 images_not_implemented_stub.ImagesNotImplementedServiceStub())
3679 blob_storage = file_blob_storage.FileBlobStorage(blobstore_path, app_id)
3680 apiproxy_stub_map.apiproxy.RegisterStub(
3681 'blobstore',
3682 blobstore_stub.BlobstoreServiceStub(blob_storage))
3684 apiproxy_stub_map.apiproxy.RegisterStub(
3685 'file',
3686 file_service_stub.FileServiceStub(blob_storage))
3688 system_service_stub = system_stub.SystemServiceStub()
3689 multiprocess.GlobalProcess().UpdateSystemStub(system_service_stub)
3690 apiproxy_stub_map.apiproxy.RegisterStub('system', system_service_stub)
3693 def TearDownStubs():
3694 """Clean up any stubs that need cleanup."""
3696 datastore_stub = apiproxy_stub_map.apiproxy.GetStub('datastore_v3')
3699 if isinstance(datastore_stub, datastore_stub_util.BaseTransactionManager):
3700 logging.info('Applying all pending transactions and saving the datastore')
3701 datastore_stub.Write()
3703 search_stub = apiproxy_stub_map.apiproxy.GetStub('search')
3704 if isinstance(search_stub, simple_search_stub.SearchServiceStub):
3705 logging.info('Saving search indexes')
3706 search_stub.Write()
3709 def CreateImplicitMatcher(
3710 config,
3711 module_dict,
3712 root_path,
3713 login_url,
3714 create_path_adjuster=PathAdjuster,
3715 create_local_dispatcher=LocalCGIDispatcher,
3716 create_cgi_dispatcher=CGIDispatcher,
3717 get_blob_storage=dev_appserver_blobstore.GetBlobStorage):
3718 """Creates a URLMatcher instance that handles internal URLs.
3720 Used to facilitate handling user login/logout, debugging, info about the
3721 currently running app, quitting the dev appserver, etc.
3723 Args:
3724 config: AppInfoExternal instance representing the parsed app.yaml file.
3725 module_dict: Dictionary in the form used by sys.modules.
3726 root_path: Path to the root of the application.
3727 login_url: Relative URL which should be used for handling user login/logout.
3728 create_path_adjuster: Used for dependedency injection.
3729 create_local_dispatcher: Used for dependency injection.
3730 create_cgi_dispatcher: Used for dependedency injection.
3731 get_blob_storage: Used for dependency injection.
3733 Returns:
3734 Instance of URLMatcher with appropriate dispatchers.
3736 url_matcher = URLMatcher()
3737 path_adjuster = create_path_adjuster(root_path)
3742 def _HandleQuit():
3743 raise KeyboardInterrupt
3744 quit_dispatcher = create_local_dispatcher(config, sys.modules, path_adjuster,
3745 _HandleQuit)
3746 url_matcher.AddURL('/_ah/quit?',
3747 quit_dispatcher,
3749 False,
3750 False,
3751 appinfo.AUTH_FAIL_ACTION_REDIRECT)
3756 login_dispatcher = create_local_dispatcher(config, sys.modules, path_adjuster,
3757 dev_appserver_login.main)
3758 url_matcher.AddURL(login_url,
3759 login_dispatcher,
3761 False,
3762 False,
3763 appinfo.AUTH_FAIL_ACTION_REDIRECT)
3765 admin_dispatcher = create_cgi_dispatcher(config, module_dict, root_path,
3766 path_adjuster)
3767 url_matcher.AddURL('/_ah/admin(?:/.*)?',
3768 admin_dispatcher,
3769 DEVEL_CONSOLE_PATH,
3770 False,
3771 False,
3772 appinfo.AUTH_FAIL_ACTION_REDIRECT)
3774 upload_dispatcher = dev_appserver_blobstore.CreateUploadDispatcher(
3775 get_blob_storage)
3777 url_matcher.AddURL(dev_appserver_blobstore.UPLOAD_URL_PATTERN,
3778 upload_dispatcher,
3780 False,
3781 False,
3782 appinfo.AUTH_FAIL_ACTION_UNAUTHORIZED)
3784 blobimage_dispatcher = dev_appserver_blobimage.CreateBlobImageDispatcher(
3785 apiproxy_stub_map.apiproxy.GetStub('images'))
3786 url_matcher.AddURL(dev_appserver_blobimage.BLOBIMAGE_URL_PATTERN,
3787 blobimage_dispatcher,
3789 False,
3790 False,
3791 appinfo.AUTH_FAIL_ACTION_UNAUTHORIZED)
3793 oauth_dispatcher = dev_appserver_oauth.CreateOAuthDispatcher()
3795 url_matcher.AddURL(dev_appserver_oauth.OAUTH_URL_PATTERN,
3796 oauth_dispatcher,
3798 False,
3799 False,
3800 appinfo.AUTH_FAIL_ACTION_UNAUTHORIZED)
3802 channel_dispatcher = dev_appserver_channel.CreateChannelDispatcher(
3803 apiproxy_stub_map.apiproxy.GetStub('channel'))
3805 url_matcher.AddURL(dev_appserver_channel.CHANNEL_POLL_PATTERN,
3806 channel_dispatcher,
3808 False,
3809 False,
3810 appinfo.AUTH_FAIL_ACTION_UNAUTHORIZED)
3812 url_matcher.AddURL(dev_appserver_channel.CHANNEL_JSAPI_PATTERN,
3813 channel_dispatcher,
3815 False,
3816 False,
3817 appinfo.AUTH_FAIL_ACTION_UNAUTHORIZED)
3819 apiserver_dispatcher = dev_appserver_apiserver.CreateApiserverDispatcher()
3820 url_matcher.AddURL(dev_appserver_apiserver.API_SERVING_PATTERN,
3821 apiserver_dispatcher,
3823 False,
3824 False,
3825 appinfo.AUTH_FAIL_ACTION_UNAUTHORIZED)
3827 return url_matcher
3830 def FetchAllEntitites():
3831 """Returns all datastore entities from all namespaces as a list."""
3832 ns = list(datastore.Query('__namespace__').Run())
3833 original_ns = namespace_manager.get_namespace()
3834 entities_set = []
3835 for namespace in ns:
3836 namespace_manager.set_namespace(namespace.key().name())
3837 kinds_list = list(datastore.Query('__kind__').Run())
3838 for kind_entity in kinds_list:
3839 ents = list(datastore.Query(kind_entity.key().name()).Run())
3840 for ent in ents:
3841 entities_set.append(ent)
3842 namespace_manager.set_namespace(original_ns)
3843 return entities_set
3846 def PutAllEntities(entities):
3847 """Puts all entities to the current datastore."""
3848 for entity in entities:
3849 datastore.Put(entity)
3852 def PortAllEntities(datastore_path):
3853 """Copies entities from a DatastoreFileStub to an SQLite stub.
3855 Args:
3856 datastore_path: Path to the file to store Datastore file stub data is.
3859 previous_stub = apiproxy_stub_map.apiproxy.GetStub('datastore_v3')
3861 try:
3862 app_id = os.environ['APPLICATION_ID']
3863 apiproxy_stub_map.apiproxy = apiproxy_stub_map.APIProxyStubMap()
3864 datastore_stub = datastore_file_stub.DatastoreFileStub(
3865 app_id, datastore_path, trusted=True)
3866 apiproxy_stub_map.apiproxy.RegisterStub('datastore_v3', datastore_stub)
3868 entities = FetchAllEntitites()
3869 sqlite_datastore_stub = datastore_sqlite_stub.DatastoreSqliteStub(app_id,
3870 datastore_path + '.sqlite', trusted=True)
3871 apiproxy_stub_map.apiproxy.ReplaceStub('datastore_v3',
3872 sqlite_datastore_stub)
3873 PutAllEntities(entities)
3874 sqlite_datastore_stub.Close()
3875 finally:
3876 apiproxy_stub_map.apiproxy.ReplaceStub('datastore_v3', previous_stub)
3878 shutil.copy(datastore_path, datastore_path + '.filestub')
3879 _RemoveFile(datastore_path)
3880 shutil.move(datastore_path + '.sqlite', datastore_path)
3883 def CreateServer(root_path,
3884 login_url,
3885 port,
3886 template_dir=None,
3887 serve_address='',
3888 allow_skipped_files=False,
3889 static_caching=True,
3890 python_path_list=sys.path,
3891 sdk_dir=SDK_ROOT,
3892 default_partition=None,
3893 frontend_port=None,
3894 interactive_console=True):
3895 """Creates a new HTTPServer for an application.
3897 The sdk_dir argument must be specified for the directory storing all code for
3898 the SDK so as to allow for the sandboxing of module access to work for any
3899 and all SDK code. While typically this is where the 'google' package lives,
3900 it can be in another location because of API version support.
3902 Args:
3903 root_path: String containing the path to the root directory of the
3904 application where the app.yaml file is.
3905 login_url: Relative URL which should be used for handling user login/logout.
3906 port: Port to start the application server on.
3907 template_dir: Unused.
3908 serve_address: Address on which the server should serve.
3909 allow_skipped_files: True if skipped files should be accessible.
3910 static_caching: True if browser caching of static files should be allowed.
3911 python_path_list: Used for dependency injection.
3912 sdk_dir: Directory where the SDK is stored.
3913 default_partition: Default partition to use for the appid.
3914 frontend_port: A frontend port (so backends can return an address for a
3915 frontend). If None, port will be used.
3916 interactive_console: Whether to add the interactive console.
3918 Returns:
3919 Instance of BaseHTTPServer.HTTPServer that's ready to start accepting.
3926 absolute_root_path = os.path.realpath(root_path)
3928 FakeFile.SetAllowedPaths(absolute_root_path,
3929 [sdk_dir])
3930 FakeFile.SetAllowSkippedFiles(allow_skipped_files)
3932 handler_class = CreateRequestHandler(absolute_root_path,
3933 login_url,
3934 static_caching,
3935 default_partition,
3936 interactive_console)
3939 if absolute_root_path not in python_path_list:
3942 python_path_list.insert(0, absolute_root_path)
3944 if multiprocess.Enabled():
3945 server = HttpServerWithMultiProcess((serve_address, port), handler_class)
3946 else:
3947 server = HTTPServerWithScheduler((serve_address, port), handler_class)
3951 queue_stub = apiproxy_stub_map.apiproxy.GetStub('taskqueue')
3952 if queue_stub and hasattr(queue_stub, 'StartBackgroundExecution'):
3953 queue_stub.StartBackgroundExecution()
3955 request_info._local_dispatcher = DevAppserverDispatcher(server,
3956 frontend_port or port)
3957 server.frontend_hostport = '%s:%d' % (serve_address or 'localhost',
3958 frontend_port or port)
3960 return server
3963 class HTTPServerWithScheduler(BaseHTTPServer.HTTPServer):
3964 """A BaseHTTPServer subclass that calls a method at a regular interval."""
3966 def __init__(self, server_address, request_handler_class):
3967 """Constructor.
3969 Args:
3970 server_address: the bind address of the server.
3971 request_handler_class: class used to handle requests.
3973 BaseHTTPServer.HTTPServer.__init__(self, server_address,
3974 request_handler_class)
3975 self._events = []
3976 self._stopped = False
3978 def handle_request(self):
3979 """Override the base handle_request call.
3981 Python 2.6 changed the semantics of handle_request() with r61289.
3982 This patches it back to the Python 2.5 version, which has
3983 helpfully been renamed to _handle_request_noblock.
3985 if hasattr(self, "_handle_request_noblock"):
3986 self._handle_request_noblock()
3987 else:
3988 BaseHTTPServer.HTTPServer.handle_request(self)
3990 def get_request(self, time_func=time.time, select_func=select.select):
3991 """Overrides the base get_request call.
3993 Args:
3994 time_func: used for testing.
3995 select_func: used for testing.
3997 Returns:
3998 a (socket_object, address info) tuple.
4000 while True:
4001 if self._events:
4002 current_time = time_func()
4003 next_eta = self._events[0][0]
4004 delay = next_eta - current_time
4005 else:
4006 delay = DEFAULT_SELECT_DELAY
4007 readable, _, _ = select_func([self.socket], [], [], max(delay, 0))
4008 if readable:
4009 return self.socket.accept()
4010 current_time = time_func()
4014 if self._events and current_time >= self._events[0][0]:
4015 runnable = heapq.heappop(self._events)[1]
4016 request_tuple = runnable()
4017 if request_tuple:
4018 return request_tuple
4020 def serve_forever(self):
4021 """Handle one request at a time until told to stop."""
4022 while not self._stopped:
4023 self.handle_request()
4024 self.server_close()
4026 def stop_serving_forever(self):
4027 """Stop the serve_forever() loop.
4029 Stop happens on the next handle_request() loop; it will not stop
4030 immediately. Since dev_appserver.py must run on py2.5 we can't
4031 use newer features of SocketServer (e.g. shutdown(), added in py2.6).
4033 self._stopped = True
4035 def AddEvent(self, eta, runnable, service=None, event_id=None):
4036 """Add a runnable event to be run at the specified time.
4038 Args:
4039 eta: when to run the event, in seconds since epoch.
4040 runnable: a callable object.
4041 service: the service that owns this event. Should be set if id is set.
4042 event_id: optional id of the event. Used for UpdateEvent below.
4044 heapq.heappush(self._events, (eta, runnable, service, event_id))
4046 def UpdateEvent(self, service, event_id, eta):
4047 """Update a runnable event in the heap with a new eta.
4048 TODO: come up with something better than a linear scan to
4049 update items. For the case this is used for now -- updating events to
4050 "time out" channels -- this works fine because those events are always
4051 soon (within seconds) and thus found quickly towards the front of the heap.
4052 One could easily imagine a scenario where this is always called for events
4053 that tend to be at the back of the heap, of course...
4055 Args:
4056 service: the service that owns this event.
4057 event_id: the id of the event.
4058 eta: the new eta of the event.
4060 for id in xrange(len(self._events)):
4061 item = self._events[id]
4062 if item[2] == service and item[3] == event_id:
4063 item = (eta, item[1], item[2], item[3])
4064 del(self._events[id])
4065 heapq.heappush(self._events, item)
4066 break
4069 class HttpServerWithMultiProcess(HTTPServerWithScheduler):
4070 """Class extending HTTPServerWithScheduler with multi-process handling."""
4072 def __init__(self, server_address, request_handler_class):
4073 """Constructor.
4075 Args:
4076 server_address: the bind address of the server.
4077 request_handler_class: class used to handle requests.
4079 HTTPServerWithScheduler.__init__(self, server_address,
4080 request_handler_class)
4081 multiprocess.GlobalProcess().SetHttpServer(self)
4083 def process_request(self, request, client_address):
4084 """Overrides the SocketServer process_request call."""
4085 multiprocess.GlobalProcess().ProcessRequest(request, client_address)
4088 class FakeRequestSocket(object):
4089 """A socket object to fake an HTTP request."""
4091 def __init__(self, method, relative_url, headers, body):
4092 payload = cStringIO.StringIO()
4093 payload.write('%s %s HTTP/1.1\r\n' % (method, relative_url))
4094 payload.write('Content-Length: %d\r\n' % len(body))
4095 for key, value in headers:
4096 payload.write('%s: %s\r\n' % (key, value))
4097 payload.write('\r\n')
4098 payload.write(body)
4099 self.rfile = cStringIO.StringIO(payload.getvalue())
4100 self.wfile = StringIO.StringIO()
4101 self.wfile_close = self.wfile.close
4102 self.wfile.close = self.connection_done
4104 def connection_done(self):
4105 self.wfile_close()
4107 def makefile(self, mode, buffsize):
4108 if mode.startswith('w'):
4109 return self.wfile
4110 else:
4111 return self.rfile
4113 def close(self):
4114 pass
4116 def shutdown(self, how):
4117 pass
4120 class DevAppserverDispatcher(request_info._LocalFakeDispatcher):
4121 """A dev_appserver Dispatcher implementation."""
4123 def __init__(self, server, port):
4124 self._server = server
4125 self._port = port
4127 def add_event(self, runnable, eta, service=None, event_id=None):
4128 """Add a callable to be run at the specified time.
4130 Args:
4131 runnable: A callable object to call at the specified time.
4132 eta: An int containing the time to run the event, in seconds since the
4133 epoch.
4134 service: A str containing the name of the service that owns this event.
4135 This should be set if event_id is set.
4136 event_id: A str containing the id of the event. If set, this can be passed
4137 to update_event to change the time at which the event should run.
4139 self._server.AddEvent(eta, runnable, service, event_id)
4141 def update_event(self, eta, service, event_id):
4142 """Update the eta of a scheduled event.
4144 Args:
4145 eta: An int containing the time to run the event, in seconds since the
4146 epoch.
4147 service: A str containing the name of the service that owns this event.
4148 event_id: A str containing the id of the event to update.
4150 self._server.UpdateEvent(service, event_id, eta)
4152 def add_async_request(self, method, relative_url, headers, body, source_ip,
4153 server_name=None, version=None, instance_id=None):
4154 """Dispatch an HTTP request asynchronously.
4156 Args:
4157 method: A str containing the HTTP method of the request.
4158 relative_url: A str containing path and query string of the request.
4159 headers: A list of (key, value) tuples where key and value are both str.
4160 body: A str containing the request body.
4161 source_ip: The source ip address for the request.
4162 server_name: An optional str containing the server name to service this
4163 request. If unset, the request will be dispatched to the default
4164 server.
4165 version: An optional str containing the version to service this request.
4166 If unset, the request will be dispatched to the default version.
4167 instance_id: An optional str containing the instance_id of the instance to
4168 service this request. If unset, the request will be dispatched to
4169 according to the load-balancing for the server and version.
4171 fake_socket = FakeRequestSocket(method, relative_url, headers, body)
4172 self._server.AddEvent(0, lambda: (fake_socket, (source_ip, self._port)))
4174 def add_request(self, method, relative_url, headers, body, source_ip,
4175 server_name=None, version=None, instance_id=None):
4176 """Process an HTTP request.
4178 Args:
4179 method: A str containing the HTTP method of the request.
4180 relative_url: A str containing path and query string of the request.
4181 headers: A list of (key, value) tuples where key and value are both str.
4182 body: A str containing the request body.
4183 source_ip: The source ip address for the request.
4184 server_name: An optional str containing the server name to service this
4185 request. If unset, the request will be dispatched to the default
4186 server.
4187 version: An optional str containing the version to service this request.
4188 If unset, the request will be dispatched to the default version.
4189 instance_id: An optional str containing the instance_id of the instance to
4190 service this request. If unset, the request will be dispatched to
4191 according to the load-balancing for the server and version.
4193 Returns:
4194 A request_info.ResponseTuple containing the response information for the
4195 HTTP request.
4197 try:
4198 header_dict = wsgiref.headers.Headers(headers)
4199 connection_host = header_dict.get('host')
4200 connection = httplib.HTTPConnection(connection_host)
4203 connection.putrequest(
4204 method, relative_url,
4205 skip_host='host' in header_dict,
4206 skip_accept_encoding='accept-encoding' in header_dict)
4208 for header_key, header_value in headers:
4209 connection.putheader(header_key, header_value)
4210 connection.endheaders()
4211 connection.send(body)
4213 response = connection.getresponse()
4214 response.read()
4215 response.close()
4217 return request_info.ResponseTuple(
4218 '%d %s' % (response.status, response.reason), [], '')
4219 except (httplib.HTTPException, socket.error):
4220 logging.exception(
4221 'An error occured while sending a %s request to "%s%s"',
4222 method, connection_host, relative_url)
4223 return request_info.ResponseTuple('0', [], '')