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