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