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