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