changeset 1278 | a7766286a7be |
parent 828 | f5fd65cc3bf3 |
child 2172 | ac7bd3b467ff |
1277:5c931bd3dc1e | 1278:a7766286a7be |
---|---|
81 from google.appengine.api import user_service_stub |
81 from google.appengine.api import user_service_stub |
82 from google.appengine.api import yaml_errors |
82 from google.appengine.api import yaml_errors |
83 from google.appengine.api.capabilities import capability_stub |
83 from google.appengine.api.capabilities import capability_stub |
84 from google.appengine.api.memcache import memcache_stub |
84 from google.appengine.api.memcache import memcache_stub |
85 |
85 |
86 from google.appengine import dist |
|
87 |
|
86 from google.appengine.tools import dev_appserver_index |
88 from google.appengine.tools import dev_appserver_index |
87 from google.appengine.tools import dev_appserver_login |
89 from google.appengine.tools import dev_appserver_login |
88 |
90 |
89 |
91 |
90 PYTHON_LIB_VAR = '$PYTHON_LIB' |
92 PYTHON_LIB_VAR = '$PYTHON_LIB' |
111 ('.rss', 'application/rss+xml'), |
113 ('.rss', 'application/rss+xml'), |
112 ('.text', 'text/plain'), |
114 ('.text', 'text/plain'), |
113 ('.wbmp', 'image/vnd.wap.wbmp')): |
115 ('.wbmp', 'image/vnd.wap.wbmp')): |
114 mimetypes.add_type(mime_type, ext) |
116 mimetypes.add_type(mime_type, ext) |
115 |
117 |
116 MAX_RUNTIME_RESPONSE_SIZE = 1 << 20 |
118 MAX_RUNTIME_RESPONSE_SIZE = 10 << 20 |
117 |
119 |
118 MAX_REQUEST_SIZE = 10 * 1024 * 1024 |
120 MAX_REQUEST_SIZE = 10 * 1024 * 1024 |
121 |
|
122 API_VERSION = '1' |
|
119 |
123 |
120 |
124 |
121 class Error(Exception): |
125 class Error(Exception): |
122 """Base-class for exceptions in this module.""" |
126 """Base-class for exceptions in this module.""" |
123 |
127 |
191 headers: Instance of mimetools.Message with headers from the request. |
195 headers: Instance of mimetools.Message with headers from the request. |
192 infile: File-like object with input data from the request. |
196 infile: File-like object with input data from the request. |
193 outfile: File-like object where output data should be written. |
197 outfile: File-like object where output data should be written. |
194 base_env_dict: Dictionary of CGI environment parameters if available. |
198 base_env_dict: Dictionary of CGI environment parameters if available. |
195 Defaults to None. |
199 Defaults to None. |
200 |
|
201 Returns: |
|
202 None if request handling is complete. |
|
203 Tuple (path, headers, input_file) for an internal redirect: |
|
204 path: Path of URL to redirect to. |
|
205 headers: Headers to send to other dispatcher. |
|
206 input_file: New input to send to new dispatcher. |
|
196 """ |
207 """ |
197 raise NotImplementedError |
208 raise NotImplementedError |
209 |
|
210 def EndRedirect(self, dispatched_output, original_output): |
|
211 """Process the end of an internal redirect. |
|
212 |
|
213 This method is called after all subsequent dispatch requests have finished. |
|
214 By default the output from the dispatched process is copied to the original. |
|
215 |
|
216 This will not be called on dispatchers that do not return an internal |
|
217 redirect. |
|
218 |
|
219 Args: |
|
220 dispatched_output: StringIO buffer containing the results from the |
|
221 dispatched |
|
222 """ |
|
223 original_output.write(dispatched_output.read()) |
|
198 |
224 |
199 |
225 |
200 class URLMatcher(object): |
226 class URLMatcher(object): |
201 """Matches an arbitrary URL using a list of URL patterns from an application. |
227 """Matches an arbitrary URL using a list of URL patterns from an application. |
202 |
228 |
344 '\r\n' |
370 '\r\n' |
345 'Current logged in user %s is not ' |
371 'Current logged in user %s is not ' |
346 'authorized to view this page.' |
372 'authorized to view this page.' |
347 % (httplib.FORBIDDEN, email)) |
373 % (httplib.FORBIDDEN, email)) |
348 else: |
374 else: |
349 dispatcher.Dispatch(relative_url, |
375 forward = dispatcher.Dispatch(relative_url, |
350 matched_path, |
376 matched_path, |
351 headers, |
377 headers, |
352 infile, |
378 infile, |
353 outfile, |
379 outfile, |
354 base_env_dict=base_env_dict) |
380 base_env_dict=base_env_dict) |
381 |
|
382 if forward: |
|
383 new_path, new_headers, new_input = forward |
|
384 logging.info('Internal redirection to %s' % new_path) |
|
385 new_outfile = cStringIO.StringIO() |
|
386 self.Dispatch(new_path, |
|
387 None, |
|
388 new_headers, |
|
389 new_input, |
|
390 new_outfile, |
|
391 dict(base_env_dict)) |
|
392 new_outfile.seek(0) |
|
393 dispatcher.EndRedirect(new_outfile, outfile) |
|
355 |
394 |
356 return |
395 return |
357 |
396 |
358 outfile.write('Status: %d URL did not match\r\n' |
397 outfile.write('Status: %d URL did not match\r\n' |
359 '\r\n' |
398 '\r\n' |
512 env['HTTP_' + adjusted_name] = ', '.join(headers.getheaders(key)) |
551 env['HTTP_' + adjusted_name] = ', '.join(headers.getheaders(key)) |
513 |
552 |
514 return env |
553 return env |
515 |
554 |
516 |
555 |
517 def FakeTemporaryFile(*args, **kwargs): |
|
518 """Fake for tempfile.TemporaryFile that just uses StringIO.""" |
|
519 return cStringIO.StringIO() |
|
520 |
|
521 |
|
522 def NotImplementedFake(*args, **kwargs): |
556 def NotImplementedFake(*args, **kwargs): |
523 """Fake for methods/functions that are not implemented in the production |
557 """Fake for methods/functions that are not implemented in the production |
524 environment. |
558 environment. |
525 """ |
559 """ |
526 raise NotImplementedError("This class/method is not available.") |
560 raise NotImplementedError("This class/method is not available.") |
575 def FakeUname(): |
609 def FakeUname(): |
576 """Fake version of os.uname.""" |
610 """Fake version of os.uname.""" |
577 return ('Linux', '', '', '', '') |
611 return ('Linux', '', '', '', '') |
578 |
612 |
579 |
613 |
614 def FakeUnlink(path): |
|
615 """Fake version of os.unlink.""" |
|
616 if os.path.isdir(path): |
|
617 raise OSError(2, "Is a directory", path) |
|
618 else: |
|
619 raise OSError(1, "Operation not permitted", path) |
|
620 |
|
621 |
|
622 def FakeReadlink(path): |
|
623 """Fake version of os.readlink.""" |
|
624 raise OSError(22, "Invalid argument", path) |
|
625 |
|
626 |
|
627 def FakeAccess(path, mode): |
|
628 """Fake version of os.access where only reads are supported.""" |
|
629 if not os.path.exists(path) or mode != os.R_OK: |
|
630 return False |
|
631 else: |
|
632 return True |
|
633 |
|
634 |
|
580 def FakeSetLocale(category, value=None, original_setlocale=locale.setlocale): |
635 def FakeSetLocale(category, value=None, original_setlocale=locale.setlocale): |
581 """Fake version of locale.setlocale that only supports the default.""" |
636 """Fake version of locale.setlocale that only supports the default.""" |
582 if value not in (None, '', 'C', 'POSIX'): |
637 if value not in (None, '', 'C', 'POSIX'): |
583 raise locale.Error, 'locale emulation only supports "C" locale' |
638 raise locale.Error, 'locale emulation only supports "C" locale' |
584 return original_setlocale(category, 'C') |
639 return original_setlocale(category, 'C') |
713 os.path.dirname(os.__file__), 'site-packages', path))) |
768 os.path.dirname(os.__file__), 'site-packages', path))) |
714 for path in [ |
769 for path in [ |
715 |
770 |
716 ]) |
771 ]) |
717 |
772 |
773 _original_file = file |
|
774 |
|
775 _root_path = None |
|
718 _application_paths = None |
776 _application_paths = None |
719 _original_file = file |
777 _skip_files = None |
778 _static_file_config_matcher = None |
|
779 |
|
780 _availability_cache = {} |
|
720 |
781 |
721 @staticmethod |
782 @staticmethod |
722 def SetAllowedPaths(application_paths): |
783 def SetAllowedPaths(root_path, application_paths): |
723 """Sets the root path of the application that is currently running. |
784 """Configures which paths are allowed to be accessed. |
724 |
785 |
725 Must be called at least once before any file objects are created in the |
786 Must be called at least once before any file objects are created in the |
726 hardened environment. |
787 hardened environment. |
727 |
788 |
728 Args: |
789 Args: |
729 root_path: Path to the root of the application. |
790 root_path: Absolute path to the root of the application. |
791 application_paths: List of additional paths that the application may |
|
792 access, this must include the App Engine runtime but |
|
793 not the Python library directories. |
|
730 """ |
794 """ |
731 FakeFile._application_paths = (set(os.path.realpath(path) |
795 FakeFile._application_paths = (set(os.path.realpath(path) |
732 for path in application_paths) | |
796 for path in application_paths) | |
733 set(os.path.abspath(path) |
797 set(os.path.abspath(path) |
734 for path in application_paths)) |
798 for path in application_paths)) |
799 FakeFile._application_paths.add(root_path) |
|
800 |
|
801 FakeFile._root_path = os.path.join(root_path, '') |
|
802 |
|
803 FakeFile._availability_cache = {} |
|
804 |
|
805 @staticmethod |
|
806 def SetSkippedFiles(skip_files): |
|
807 """Sets which files in the application directory are to be ignored. |
|
808 |
|
809 Must be called at least once before any file objects are created in the |
|
810 hardened environment. |
|
811 |
|
812 Must be called whenever the configuration was updated. |
|
813 |
|
814 Args: |
|
815 skip_files: Object with .match() method (e.g. compiled regexp). |
|
816 """ |
|
817 FakeFile._skip_files = skip_files |
|
818 FakeFile._availability_cache = {} |
|
819 |
|
820 @staticmethod |
|
821 def SetStaticFileConfigMatcher(static_file_config_matcher): |
|
822 """Sets StaticFileConfigMatcher instance for checking if a file is static. |
|
823 |
|
824 Must be called at least once before any file objects are created in the |
|
825 hardened environment. |
|
826 |
|
827 Must be called whenever the configuration was updated. |
|
828 |
|
829 Args: |
|
830 static_file_config_matcher: StaticFileConfigMatcher instance. |
|
831 """ |
|
832 FakeFile._static_file_config_matcher = static_file_config_matcher |
|
833 FakeFile._availability_cache = {} |
|
735 |
834 |
736 @staticmethod |
835 @staticmethod |
737 def IsFileAccessible(filename, normcase=os.path.normcase): |
836 def IsFileAccessible(filename, normcase=os.path.normcase): |
738 """Determines if a file's path is accessible. |
837 """Determines if a file's path is accessible. |
739 |
838 |
740 SetAllowedPaths() must be called before this method or else all file |
839 SetAllowedPaths(), SetSkippedFiles() and SetStaticFileConfigMatcher() must |
741 accesses will raise an error. |
840 be called before this method or else all file accesses will raise an error. |
742 |
841 |
743 Args: |
842 Args: |
744 filename: Path of the file to check (relative or absolute). May be a |
843 filename: Path of the file to check (relative or absolute). May be a |
745 directory, in which case access for files inside that directory will |
844 directory, in which case access for files inside that directory will |
746 be checked. |
845 be checked. |
751 """ |
850 """ |
752 logical_filename = normcase(os.path.abspath(filename)) |
851 logical_filename = normcase(os.path.abspath(filename)) |
753 |
852 |
754 if os.path.isdir(logical_filename): |
853 if os.path.isdir(logical_filename): |
755 logical_filename = os.path.join(logical_filename, 'foo') |
854 logical_filename = os.path.join(logical_filename, 'foo') |
855 |
|
856 result = FakeFile._availability_cache.get(logical_filename) |
|
857 if result is None: |
|
858 result = FakeFile._IsFileAccessibleNoCache(logical_filename, |
|
859 normcase=normcase) |
|
860 FakeFile._availability_cache[logical_filename] = result |
|
861 return result |
|
862 |
|
863 @staticmethod |
|
864 def _IsFileAccessibleNoCache(logical_filename, normcase=os.path.normcase): |
|
865 """Determines if a file's path is accessible. |
|
866 |
|
867 This is an internal part of the IsFileAccessible implementation. |
|
868 |
|
869 Args: |
|
870 logical_filename: Absolute path of the file to check. |
|
871 normcase: Used for dependency injection. |
|
872 |
|
873 Returns: |
|
874 True if the file is accessible, False otherwise. |
|
875 """ |
|
876 if IsPathInSubdirectories(logical_filename, [FakeFile._root_path], |
|
877 normcase=normcase): |
|
878 relative_filename = logical_filename[len(FakeFile._root_path):] |
|
879 |
|
880 if FakeFile._skip_files.match(relative_filename): |
|
881 logging.warning('Blocking access to skipped file "%s"', |
|
882 logical_filename) |
|
883 return False |
|
884 |
|
885 if FakeFile._static_file_config_matcher.IsStaticFile(relative_filename): |
|
886 logging.warning('Blocking access to static file "%s"', |
|
887 logical_filename) |
|
888 return False |
|
756 |
889 |
757 if logical_filename in FakeFile.ALLOWED_FILES: |
890 if logical_filename in FakeFile.ALLOWED_FILES: |
758 return True |
891 return True |
759 |
892 |
760 if IsPathInSubdirectories(logical_filename, |
893 if IsPathInSubdirectories(logical_filename, |
884 args: Positional format parameters for the logging message. |
1017 args: Positional format parameters for the logging message. |
885 """ |
1018 """ |
886 if HardenedModulesHook.ENABLE_LOGGING: |
1019 if HardenedModulesHook.ENABLE_LOGGING: |
887 indent = self._indent_level * ' ' |
1020 indent = self._indent_level * ' ' |
888 print >>sys.stderr, indent + (message % args) |
1021 print >>sys.stderr, indent + (message % args) |
889 |
|
890 EMPTY_MODULE_FILE = '<empty module>' |
|
891 |
1022 |
892 _WHITE_LIST_C_MODULES = [ |
1023 _WHITE_LIST_C_MODULES = [ |
893 'array', |
1024 'array', |
894 'binascii', |
1025 'binascii', |
895 'bz2', |
1026 'bz2', |
957 ], |
1088 ], |
958 |
1089 |
959 |
1090 |
960 |
1091 |
961 'os': [ |
1092 'os': [ |
1093 'access', |
|
962 'altsep', |
1094 'altsep', |
963 'curdir', |
1095 'curdir', |
964 'defpath', |
1096 'defpath', |
965 'devnull', |
1097 'devnull', |
966 'environ', |
1098 'environ', |
1005 'O_WRONLY', |
1137 'O_WRONLY', |
1006 'pardir', |
1138 'pardir', |
1007 'path', |
1139 'path', |
1008 'pathsep', |
1140 'pathsep', |
1009 'R_OK', |
1141 'R_OK', |
1142 'readlink', |
|
1143 'remove', |
|
1010 'SEEK_CUR', |
1144 'SEEK_CUR', |
1011 'SEEK_END', |
1145 'SEEK_END', |
1012 'SEEK_SET', |
1146 'SEEK_SET', |
1013 'sep', |
1147 'sep', |
1014 'stat', |
1148 'stat', |
1015 'stat_float_times', |
1149 'stat_float_times', |
1016 'stat_result', |
1150 'stat_result', |
1017 'strerror', |
1151 'strerror', |
1018 'TMP_MAX', |
1152 'TMP_MAX', |
1153 'unlink', |
|
1019 'urandom', |
1154 'urandom', |
1020 'walk', |
1155 'walk', |
1021 'WCOREDUMP', |
1156 'WCOREDUMP', |
1022 'WEXITSTATUS', |
1157 'WEXITSTATUS', |
1023 'WIFEXITED', |
1158 'WIFEXITED', |
1030 'W_OK', |
1165 'W_OK', |
1031 'X_OK', |
1166 'X_OK', |
1032 ], |
1167 ], |
1033 } |
1168 } |
1034 |
1169 |
1035 _EMPTY_MODULES = [ |
|
1036 'imp', |
|
1037 'ftplib', |
|
1038 'select', |
|
1039 'socket', |
|
1040 'tempfile', |
|
1041 ] |
|
1042 |
|
1043 _MODULE_OVERRIDES = { |
1170 _MODULE_OVERRIDES = { |
1044 'locale': { |
1171 'locale': { |
1045 'setlocale': FakeSetLocale, |
1172 'setlocale': FakeSetLocale, |
1046 }, |
1173 }, |
1047 |
1174 |
1048 'os': { |
1175 'os': { |
1176 'access': FakeAccess, |
|
1049 'listdir': RestrictedPathFunction(os.listdir), |
1177 'listdir': RestrictedPathFunction(os.listdir), |
1050 |
1178 |
1051 'lstat': RestrictedPathFunction(os.stat), |
1179 'lstat': RestrictedPathFunction(os.stat), |
1180 'readlink': FakeReadlink, |
|
1181 'remove': FakeUnlink, |
|
1052 'stat': RestrictedPathFunction(os.stat), |
1182 'stat': RestrictedPathFunction(os.stat), |
1053 'uname': FakeUname, |
1183 'uname': FakeUname, |
1184 'unlink': FakeUnlink, |
|
1054 'urandom': FakeURandom, |
1185 'urandom': FakeURandom, |
1055 }, |
|
1056 |
|
1057 'socket': { |
|
1058 'AF_INET': None, |
|
1059 'SOCK_STREAM': None, |
|
1060 'SOCK_DGRAM': None, |
|
1061 '_GLOBAL_DEFAULT_TIMEOUT': getattr(socket, '_GLOBAL_DEFAULT_TIMEOUT', |
|
1062 None), |
|
1063 }, |
|
1064 |
|
1065 'tempfile': { |
|
1066 'TemporaryFile': FakeTemporaryFile, |
|
1067 'gettempdir': NotImplementedFake, |
|
1068 'gettempprefix': NotImplementedFake, |
|
1069 'mkdtemp': NotImplementedFake, |
|
1070 'mkstemp': NotImplementedFake, |
|
1071 'mktemp': NotImplementedFake, |
|
1072 'NamedTemporaryFile': NotImplementedFake, |
|
1073 'tempdir': NotImplementedFake, |
|
1074 }, |
1186 }, |
1075 } |
1187 } |
1076 |
1188 |
1077 _ENABLED_FILE_TYPES = ( |
1189 _ENABLED_FILE_TYPES = ( |
1078 imp.PKG_DIRECTORY, |
1190 imp.PKG_DIRECTORY, |
1105 self._indent_level = 0 |
1217 self._indent_level = 0 |
1106 |
1218 |
1107 @Trace |
1219 @Trace |
1108 def find_module(self, fullname, path=None): |
1220 def find_module(self, fullname, path=None): |
1109 """See PEP 302.""" |
1221 """See PEP 302.""" |
1110 if (fullname in ('cPickle', 'thread') or |
1222 if fullname in ('cPickle', 'thread'): |
1111 fullname in HardenedModulesHook._EMPTY_MODULES): |
|
1112 return self |
1223 return self |
1113 |
1224 |
1114 search_path = path |
1225 search_path = path |
1115 all_modules = fullname.split('.') |
1226 all_modules = fullname.split('.') |
1116 try: |
1227 try: |
1117 for index, current_module in enumerate(all_modules): |
1228 for index, current_module in enumerate(all_modules): |
1118 current_module_fullname = '.'.join(all_modules[:index + 1]) |
1229 current_module_fullname = '.'.join(all_modules[:index + 1]) |
1119 if current_module_fullname == fullname: |
1230 if (current_module_fullname == fullname and not |
1231 self.StubModuleExists(fullname)): |
|
1120 self.FindModuleRestricted(current_module, |
1232 self.FindModuleRestricted(current_module, |
1121 current_module_fullname, |
1233 current_module_fullname, |
1122 search_path) |
1234 search_path) |
1123 else: |
1235 else: |
1124 if current_module_fullname in self._module_dict: |
1236 if current_module_fullname in self._module_dict: |
1132 search_path = module.__path__ |
1244 search_path = module.__path__ |
1133 except CouldNotFindModuleError: |
1245 except CouldNotFindModuleError: |
1134 return None |
1246 return None |
1135 |
1247 |
1136 return self |
1248 return self |
1249 |
|
1250 def StubModuleExists(self, name): |
|
1251 """Check if the named module has a stub replacement.""" |
|
1252 if name in sys.builtin_module_names: |
|
1253 name = 'py_%s' % name |
|
1254 if name in dist.__all__: |
|
1255 return True |
|
1256 return False |
|
1257 |
|
1258 def ImportStubModule(self, name): |
|
1259 """Import the stub module replacement for the specified module.""" |
|
1260 if name in sys.builtin_module_names: |
|
1261 name = 'py_%s' % name |
|
1262 module = __import__(dist.__name__, {}, {}, [name]) |
|
1263 return getattr(module, name) |
|
1137 |
1264 |
1138 @Trace |
1265 @Trace |
1139 def FixModule(self, module): |
1266 def FixModule(self, module): |
1140 """Prunes and overrides restricted module attributes. |
1267 """Prunes and overrides restricted module attributes. |
1141 |
1268 |
1332 ImportError exception if the module could not be loaded for whatever |
1459 ImportError exception if the module could not be loaded for whatever |
1333 reason (e.g., missing, not allowed). |
1460 reason (e.g., missing, not allowed). |
1334 """ |
1461 """ |
1335 module = self._imp.new_module(submodule_fullname) |
1462 module = self._imp.new_module(submodule_fullname) |
1336 |
1463 |
1337 if submodule_fullname in self._EMPTY_MODULES: |
1464 if submodule_fullname == 'thread': |
1338 module.__file__ = self.EMPTY_MODULE_FILE |
|
1339 elif submodule_fullname == 'thread': |
|
1340 module.__dict__.update(self._dummy_thread.__dict__) |
1465 module.__dict__.update(self._dummy_thread.__dict__) |
1341 module.__name__ = 'thread' |
1466 module.__name__ = 'thread' |
1342 elif submodule_fullname == 'cPickle': |
1467 elif submodule_fullname == 'cPickle': |
1343 module.__dict__.update(self._pickle.__dict__) |
1468 module.__dict__.update(self._pickle.__dict__) |
1344 module.__name__ = 'cPickle' |
1469 module.__name__ = 'cPickle' |
1345 elif submodule_fullname == 'os': |
1470 elif submodule_fullname == 'os': |
1346 module.__dict__.update(self._os.__dict__) |
1471 module.__dict__.update(self._os.__dict__) |
1347 self._module_dict['os.path'] = module.path |
1472 self._module_dict['os.path'] = module.path |
1473 elif self.StubModuleExists(submodule_fullname): |
|
1474 module = self.ImportStubModule(submodule_fullname) |
|
1348 else: |
1475 else: |
1349 source_file, pathname, description = self.FindModuleRestricted(submodule, submodule_fullname, search_path) |
1476 source_file, pathname, description = self.FindModuleRestricted(submodule, submodule_fullname, search_path) |
1350 module = self.LoadModuleRestricted(submodule_fullname, |
1477 module = self.LoadModuleRestricted(submodule_fullname, |
1351 source_file, |
1478 source_file, |
1352 pathname, |
1479 pathname, |
2002 regex = entry.upload + '$' |
2129 regex = entry.upload + '$' |
2003 else: |
2130 else: |
2004 path = entry.static_dir |
2131 path = entry.static_dir |
2005 if path[-1] == '/': |
2132 if path[-1] == '/': |
2006 path = path[:-1] |
2133 path = path[:-1] |
2007 regex = re.escape(path) + r'/(.*)' |
2134 regex = re.escape(path + os.path.sep) + r'(.*)' |
2008 |
2135 |
2009 try: |
2136 try: |
2010 path_re = re.compile(regex) |
2137 path_re = re.compile(regex) |
2011 except re.error, e: |
2138 except re.error, e: |
2012 raise InvalidAppConfigError('regex %s does not compile: %s' % |
2139 raise InvalidAppConfigError('regex %s does not compile: %s' % |
2018 expiration = self._default_expiration |
2145 expiration = self._default_expiration |
2019 else: |
2146 else: |
2020 expiration = appinfo.ParseExpiration(entry.expiration) |
2147 expiration = appinfo.ParseExpiration(entry.expiration) |
2021 |
2148 |
2022 self._patterns.append((path_re, entry.mime_type, expiration)) |
2149 self._patterns.append((path_re, entry.mime_type, expiration)) |
2150 |
|
2151 def IsStaticFile(self, path): |
|
2152 """Tests if the given path points to a "static" file. |
|
2153 |
|
2154 Args: |
|
2155 path: String containing the file's path relative to the app. |
|
2156 |
|
2157 Returns: |
|
2158 Boolean, True if the file was configured to be static. |
|
2159 """ |
|
2160 for (path_re, _, _) in self._patterns: |
|
2161 if path_re.match(path): |
|
2162 return True |
|
2163 return False |
|
2023 |
2164 |
2024 def GetMimeType(self, path): |
2165 def GetMimeType(self, path): |
2025 """Returns the mime type that we should use when serving the specified file. |
2166 """Returns the mime type that we should use when serving the specified file. |
2026 |
2167 |
2027 Args: |
2168 Args: |
2141 'content-encoding', 'accept-encoding', 'transfer-encoding', |
2282 'content-encoding', 'accept-encoding', 'transfer-encoding', |
2142 'server', 'date', |
2283 'server', 'date', |
2143 ]) |
2284 ]) |
2144 |
2285 |
2145 |
2286 |
2146 def RewriteResponse(response_file): |
2287 def IgnoreHeadersRewriter(status_code, status_message, headers, body): |
2147 """Interprets server-side headers and adjusts the HTTP response accordingly. |
2288 """Ignore specific response headers. |
2289 |
|
2290 Certain response headers cannot be modified by an Application. For a |
|
2291 complete list of these headers please see: |
|
2292 |
|
2293 http://code.google.com/appengine/docs/webapp/responseclass.html#Disallowed_HTTP_Response_Headers |
|
2294 |
|
2295 This rewriter simply removes those headers. |
|
2296 """ |
|
2297 for h in _IGNORE_RESPONSE_HEADERS: |
|
2298 if h in headers: |
|
2299 del headers[h] |
|
2300 |
|
2301 return status_code, status_message, headers, body |
|
2302 |
|
2303 |
|
2304 def ParseStatusRewriter(status_code, status_message, headers, body): |
|
2305 """Parse status header, if it exists. |
|
2148 |
2306 |
2149 Handles the server-side 'status' header, which instructs the server to change |
2307 Handles the server-side 'status' header, which instructs the server to change |
2150 the HTTP response code accordingly. Handles the 'location' header, which |
2308 the HTTP response code accordingly. Handles the 'location' header, which |
2151 issues an HTTP 302 redirect to the client. Also corrects the 'content-length' |
2309 issues an HTTP 302 redirect to the client. Also corrects the 'content-length' |
2152 header to reflect actual content length in case extra information has been |
2310 header to reflect actual content length in case extra information has been |
2153 appended to the response body. |
2311 appended to the response body. |
2154 |
2312 |
2155 If the 'status' header supplied by the client is invalid, this method will |
2313 If the 'status' header supplied by the client is invalid, this method will |
2156 set the response to a 500 with an error message as content. |
2314 set the response to a 500 with an error message as content. |
2315 """ |
|
2316 location_value = headers.getheader('location') |
|
2317 status_value = headers.getheader('status') |
|
2318 if status_value: |
|
2319 response_status = status_value |
|
2320 del headers['status'] |
|
2321 elif location_value: |
|
2322 response_status = '%d Redirecting' % httplib.FOUND |
|
2323 else: |
|
2324 return status_code, status_message, headers, body |
|
2325 |
|
2326 status_parts = response_status.split(' ', 1) |
|
2327 status_code, status_message = (status_parts + [''])[:2] |
|
2328 try: |
|
2329 status_code = int(status_code) |
|
2330 except ValueError: |
|
2331 status_code = 500 |
|
2332 body = cStringIO.StringIO('Error: Invalid "status" header value returned.') |
|
2333 |
|
2334 return status_code, status_message, headers, body |
|
2335 |
|
2336 |
|
2337 def CacheRewriter(status_code, status_message, headers, body): |
|
2338 """Update the cache header.""" |
|
2339 if not 'Cache-Control' in headers: |
|
2340 headers['Cache-Control'] = 'no-cache' |
|
2341 return status_code, status_message, headers, body |
|
2342 |
|
2343 |
|
2344 def ContentLengthRewriter(status_code, status_message, headers, body): |
|
2345 """Rewrite the Content-Length header. |
|
2346 |
|
2347 Even though Content-Length is not a user modifiable header, App Engine |
|
2348 sends a correct Content-Length to the user based on the actual response. |
|
2349 """ |
|
2350 current_position = body.tell() |
|
2351 body.seek(0, 2) |
|
2352 |
|
2353 headers['Content-Length'] = str(body.tell() - current_position) |
|
2354 body.seek(current_position) |
|
2355 return status_code, status_message, headers, body |
|
2356 |
|
2357 |
|
2358 def CreateResponseRewritersChain(): |
|
2359 """Create the default response rewriter chain. |
|
2360 |
|
2361 A response rewriter is the a function that gets a final chance to change part |
|
2362 of the dev_appservers response. A rewriter is not like a dispatcher in that |
|
2363 it is called after every request has been handled by the dispatchers |
|
2364 regardless of which dispatcher was used. |
|
2365 |
|
2366 The order in which rewriters are registered will be the order in which they |
|
2367 are used to rewrite the response. Modifications from earlier rewriters |
|
2368 are used as input to later rewriters. |
|
2369 |
|
2370 A response rewriter is a function that can rewrite the request in any way. |
|
2371 Thefunction can returned modified values or the original values it was |
|
2372 passed. |
|
2373 |
|
2374 A rewriter function has the following parameters and return values: |
|
2375 |
|
2376 Args: |
|
2377 status_code: Status code of response from dev_appserver or previous |
|
2378 rewriter. |
|
2379 status_message: Text corresponding to status code. |
|
2380 headers: mimetools.Message instance with parsed headers. NOTE: These |
|
2381 headers can contain its own 'status' field, but the default |
|
2382 dev_appserver implementation will remove this. Future rewriters |
|
2383 should avoid re-introducing the status field and return new codes |
|
2384 instead. |
|
2385 body: File object containing the body of the response. This position of |
|
2386 this file may not be at the start of the file. Any content before the |
|
2387 files position is considered not to be part of the final body. |
|
2388 |
|
2389 Returns: |
|
2390 status_code: Rewritten status code or original. |
|
2391 status_message: Rewritter message or original. |
|
2392 headers: Rewritten/modified headers or original. |
|
2393 body: Rewritten/modified body or original. |
|
2394 |
|
2395 Returns: |
|
2396 List of response rewriters. |
|
2397 """ |
|
2398 return [IgnoreHeadersRewriter, |
|
2399 ParseStatusRewriter, |
|
2400 CacheRewriter, |
|
2401 ContentLengthRewriter, |
|
2402 ] |
|
2403 |
|
2404 |
|
2405 def RewriteResponse(response_file, response_rewriters=None): |
|
2406 """Allows final rewrite of dev_appserver response. |
|
2407 |
|
2408 This function receives the unparsed HTTP response from the application |
|
2409 or internal handler, parses out the basic structure and feeds that structure |
|
2410 in to a chain of response rewriters. |
|
2411 |
|
2412 It also makes sure the final HTTP headers are properly terminated. |
|
2413 |
|
2414 For more about response rewriters, please see documentation for |
|
2415 CreateResponeRewritersChain. |
|
2157 |
2416 |
2158 Args: |
2417 Args: |
2159 response_file: File-like object containing the full HTTP response including |
2418 response_file: File-like object containing the full HTTP response including |
2160 the response code, all headers, and the request body. |
2419 the response code, all headers, and the request body. |
2161 gmtime: Function which returns current time in a format matching standard |
2420 response_rewriters: A list of response rewriters. If none is provided it |
2162 time.gmtime(). |
2421 will create a new chain using CreateResponseRewritersChain. |
2163 |
2422 |
2164 Returns: |
2423 Returns: |
2165 Tuple (status_code, status_message, header, body) where: |
2424 Tuple (status_code, status_message, header, body) where: |
2166 status_code: Integer HTTP response status (e.g., 200, 302, 404, 500) |
2425 status_code: Integer HTTP response status (e.g., 200, 302, 404, 500) |
2167 status_message: String containing an informational message about the |
2426 status_message: String containing an informational message about the |
2168 response code, possibly derived from the 'status' header, if supplied. |
2427 response code, possibly derived from the 'status' header, if supplied. |
2169 header: String containing the HTTP headers of the response, without |
2428 header: String containing the HTTP headers of the response, without |
2170 a trailing new-line (CRLF). |
2429 a trailing new-line (CRLF). |
2171 body: String containing the body of the response. |
2430 body: String containing the body of the response. |
2172 """ |
2431 """ |
2432 if response_rewriters is None: |
|
2433 response_rewriters = CreateResponseRewritersChain() |
|
2434 |
|
2435 status_code = 200 |
|
2436 status_message = 'Good to go' |
|
2173 headers = mimetools.Message(response_file) |
2437 headers = mimetools.Message(response_file) |
2174 |
2438 |
2175 for h in _IGNORE_RESPONSE_HEADERS: |
2439 for response_rewriter in response_rewriters: |
2176 if h in headers: |
2440 status_code, status_message, headers, response_file = response_rewriter( |
2177 del headers[h] |
2441 status_code, |
2178 |
2442 status_message, |
2179 response_status = '%d Good to go' % httplib.OK |
2443 headers, |
2180 |
2444 response_file) |
2181 location_value = headers.getheader('location') |
|
2182 status_value = headers.getheader('status') |
|
2183 if status_value: |
|
2184 response_status = status_value |
|
2185 del headers['status'] |
|
2186 elif location_value: |
|
2187 response_status = '%d Redirecting' % httplib.FOUND |
|
2188 |
|
2189 if not 'Cache-Control' in headers: |
|
2190 headers['Cache-Control'] = 'no-cache' |
|
2191 |
|
2192 status_parts = response_status.split(' ', 1) |
|
2193 status_code, status_message = (status_parts + [''])[:2] |
|
2194 try: |
|
2195 status_code = int(status_code) |
|
2196 except ValueError: |
|
2197 status_code = 500 |
|
2198 body = 'Error: Invalid "status" header value returned.' |
|
2199 else: |
|
2200 body = response_file.read() |
|
2201 |
|
2202 headers['Content-Length'] = str(len(body)) |
|
2203 |
2445 |
2204 header_list = [] |
2446 header_list = [] |
2205 for header in headers.headers: |
2447 for header in headers.headers: |
2206 header = header.rstrip('\n') |
2448 header = header.rstrip('\n') |
2207 header = header.rstrip('\r') |
2449 header = header.rstrip('\r') |
2208 header_list.append(header) |
2450 header_list.append(header) |
2209 |
2451 |
2210 header_data = '\r\n'.join(header_list) + '\r\n' |
2452 header_data = '\r\n'.join(header_list) + '\r\n' |
2211 return status_code, status_message, header_data, body |
2453 return status_code, status_message, header_data, response_file.read() |
2212 |
2454 |
2213 |
2455 |
2214 class ModuleManager(object): |
2456 class ModuleManager(object): |
2215 """Manages loaded modules in the runtime. |
2457 """Manages loaded modules in the runtime. |
2216 |
2458 |
2243 Path of the module's corresponding Python source file if it exists, or |
2485 Path of the module's corresponding Python source file if it exists, or |
2244 just the module's compiled Python file. If the module has an invalid |
2486 just the module's compiled Python file. If the module has an invalid |
2245 __file__ attribute, None will be returned. |
2487 __file__ attribute, None will be returned. |
2246 """ |
2488 """ |
2247 module_file = getattr(module, '__file__', None) |
2489 module_file = getattr(module, '__file__', None) |
2248 if not module_file or module_file == HardenedModulesHook.EMPTY_MODULE_FILE: |
2490 if module_file is None: |
2249 return None |
2491 return None |
2250 |
2492 |
2251 source_file = module_file[:module_file.rfind('py') + 2] |
2493 source_file = module_file[:module_file.rfind('py') + 2] |
2252 |
2494 |
2253 if is_file(source_file): |
2495 if is_file(source_file): |
2307 template_module = module_dict.get('google.appengine.ext.webapp.template') |
2549 template_module = module_dict.get('google.appengine.ext.webapp.template') |
2308 if template_module is not None: |
2550 if template_module is not None: |
2309 template_module.template_cache.clear() |
2551 template_module.template_cache.clear() |
2310 |
2552 |
2311 |
2553 |
2312 def CreateRequestHandler(root_path, login_url, require_indexes=False, |
2554 def CreateRequestHandler(root_path, |
2555 login_url, |
|
2556 require_indexes=False, |
|
2313 static_caching=True): |
2557 static_caching=True): |
2314 """Creates a new BaseHTTPRequestHandler sub-class for use with the Python |
2558 """Creates a new BaseHTTPRequestHandler sub-class for use with the Python |
2315 BaseHTTPServer module's HTTP server. |
2559 BaseHTTPServer module's HTTP server. |
2316 |
2560 |
2317 Python's built-in HTTP server does not support passing context information |
2561 Python's built-in HTTP server does not support passing context information |
2356 |
2600 |
2357 module_dict = application_module_dict |
2601 module_dict = application_module_dict |
2358 module_manager = ModuleManager(application_module_dict) |
2602 module_manager = ModuleManager(application_module_dict) |
2359 |
2603 |
2360 config_cache = application_config_cache |
2604 config_cache = application_config_cache |
2605 |
|
2606 rewriter_chain = CreateResponseRewritersChain() |
|
2361 |
2607 |
2362 def __init__(self, *args, **kwargs): |
2608 def __init__(self, *args, **kwargs): |
2363 """Initializer. |
2609 """Initializer. |
2364 |
2610 |
2365 Args: |
2611 Args: |
2430 root_path, |
2676 root_path, |
2431 login_url) |
2677 login_url) |
2432 config, explicit_matcher = LoadAppConfig(root_path, self.module_dict, |
2678 config, explicit_matcher = LoadAppConfig(root_path, self.module_dict, |
2433 cache=self.config_cache, |
2679 cache=self.config_cache, |
2434 static_caching=static_caching) |
2680 static_caching=static_caching) |
2681 if config.api_version != API_VERSION: |
|
2682 logging.error("API versions cannot be switched dynamically: %r != %r" |
|
2683 % (config.api_version, API_VERSION)) |
|
2684 sys.exit(1) |
|
2435 env_dict['CURRENT_VERSION_ID'] = config.version + ".1" |
2685 env_dict['CURRENT_VERSION_ID'] = config.version + ".1" |
2436 env_dict['APPLICATION_ID'] = config.application |
2686 env_dict['APPLICATION_ID'] = config.application |
2437 dispatcher = MatcherDispatcher(login_url, |
2687 dispatcher = MatcherDispatcher(login_url, |
2438 [implicit_matcher, explicit_matcher]) |
2688 [implicit_matcher, explicit_matcher]) |
2439 |
2689 |
2463 self.module_manager.UpdateModuleFileModificationTimes() |
2713 self.module_manager.UpdateModuleFileModificationTimes() |
2464 |
2714 |
2465 outfile.flush() |
2715 outfile.flush() |
2466 outfile.seek(0) |
2716 outfile.seek(0) |
2467 |
2717 |
2468 status_code, status_message, header_data, body = RewriteResponse(outfile) |
2718 status_code, status_message, header_data, body = RewriteResponse(outfile, self.rewriter_chain) |
2469 |
2719 |
2470 runtime_response_size = len(outfile.getvalue()) |
2720 runtime_response_size = len(outfile.getvalue()) |
2471 if runtime_response_size > MAX_RUNTIME_RESPONSE_SIZE: |
2721 if runtime_response_size > MAX_RUNTIME_RESPONSE_SIZE: |
2472 status_code = 403 |
2722 status_code = 403 |
2473 status_message = 'Forbidden' |
2723 status_message = 'Forbidden' |
2580 Instance of URLMatcher with the supplied URLMap objects properly loaded. |
2830 Instance of URLMatcher with the supplied URLMap objects properly loaded. |
2581 """ |
2831 """ |
2582 url_matcher = create_url_matcher() |
2832 url_matcher = create_url_matcher() |
2583 path_adjuster = create_path_adjuster(root_path) |
2833 path_adjuster = create_path_adjuster(root_path) |
2584 cgi_dispatcher = create_cgi_dispatcher(module_dict, root_path, path_adjuster) |
2834 cgi_dispatcher = create_cgi_dispatcher(module_dict, root_path, path_adjuster) |
2835 static_file_config_matcher = StaticFileConfigMatcher(url_map_list, |
|
2836 path_adjuster, |
|
2837 default_expiration) |
|
2585 file_dispatcher = create_file_dispatcher(path_adjuster, |
2838 file_dispatcher = create_file_dispatcher(path_adjuster, |
2586 StaticFileConfigMatcher(url_map_list, path_adjuster, default_expiration)) |
2839 static_file_config_matcher) |
2840 |
|
2841 FakeFile.SetStaticFileConfigMatcher(static_file_config_matcher) |
|
2587 |
2842 |
2588 for url_map in url_map_list: |
2843 for url_map in url_map_list: |
2589 admin_only = url_map.login == appinfo.LOGIN_ADMIN |
2844 admin_only = url_map.login == appinfo.LOGIN_ADMIN |
2590 requires_login = url_map.login == appinfo.LOGIN_REQUIRED or admin_only |
2845 requires_login = url_map.login == appinfo.LOGIN_REQUIRED or admin_only |
2591 |
2846 |
2685 matcher = create_matcher(root_path, |
2940 matcher = create_matcher(root_path, |
2686 config.handlers, |
2941 config.handlers, |
2687 module_dict, |
2942 module_dict, |
2688 default_expiration) |
2943 default_expiration) |
2689 |
2944 |
2945 FakeFile.SetSkippedFiles(config.skip_files) |
|
2946 |
|
2690 if cache is not None: |
2947 if cache is not None: |
2691 cache.path = appinfo_path |
2948 cache.path = appinfo_path |
2692 cache.config = config |
2949 cache.config = config |
2693 cache.matcher = matcher |
2950 cache.matcher = matcher |
2694 |
2951 |
2866 port, |
3123 port, |
2867 template_dir, |
3124 template_dir, |
2868 serve_address='', |
3125 serve_address='', |
2869 require_indexes=False, |
3126 require_indexes=False, |
2870 static_caching=True, |
3127 static_caching=True, |
2871 python_path_list=sys.path): |
3128 python_path_list=sys.path, |
3129 sdk_dir=os.path.dirname(os.path.dirname(google.__file__))): |
|
2872 """Creates an new HTTPServer for an application. |
3130 """Creates an new HTTPServer for an application. |
3131 |
|
3132 The sdk_dir argument must be specified for the directory storing all code for |
|
3133 the SDK so as to allow for the sandboxing of module access to work for any |
|
3134 and all SDK code. While typically this is where the 'google' package lives, |
|
3135 it can be in another location because of API version support. |
|
2873 |
3136 |
2874 Args: |
3137 Args: |
2875 root_path: String containing the path to the root directory of the |
3138 root_path: String containing the path to the root directory of the |
2876 application where the app.yaml file is. |
3139 application where the app.yaml file is. |
2877 login_url: Relative URL which should be used for handling user login/logout. |
3140 login_url: Relative URL which should be used for handling user login/logout. |
2880 are stored. |
3143 are stored. |
2881 serve_address: Address on which the server should serve. |
3144 serve_address: Address on which the server should serve. |
2882 require_indexes: True if index.yaml is read-only gospel; default False. |
3145 require_indexes: True if index.yaml is read-only gospel; default False. |
2883 static_caching: True if browser caching of static files should be allowed. |
3146 static_caching: True if browser caching of static files should be allowed. |
2884 python_path_list: Used for dependency injection. |
3147 python_path_list: Used for dependency injection. |
3148 sdk_dir: Directory where the SDK is stored. |
|
2885 |
3149 |
2886 Returns: |
3150 Returns: |
2887 Instance of BaseHTTPServer.HTTPServer that's ready to start accepting. |
3151 Instance of BaseHTTPServer.HTTPServer that's ready to start accepting. |
2888 """ |
3152 """ |
2889 absolute_root_path = os.path.realpath(root_path) |
3153 absolute_root_path = os.path.realpath(root_path) |
2890 |
3154 |
2891 SetupTemplates(template_dir) |
3155 SetupTemplates(template_dir) |
2892 FakeFile.SetAllowedPaths([absolute_root_path, |
3156 FakeFile.SetAllowedPaths(absolute_root_path, |
2893 os.path.dirname(os.path.dirname(google.__file__)), |
3157 [sdk_dir, |
2894 template_dir]) |
3158 template_dir]) |
2895 |
3159 |
2896 handler_class = CreateRequestHandler(absolute_root_path, login_url, |
3160 handler_class = CreateRequestHandler(absolute_root_path, |
2897 require_indexes, static_caching) |
3161 login_url, |
3162 require_indexes, |
|
3163 static_caching) |
|
2898 |
3164 |
2899 if absolute_root_path not in python_path_list: |
3165 if absolute_root_path not in python_path_list: |
2900 python_path_list.insert(0, absolute_root_path) |
3166 python_path_list.insert(0, absolute_root_path) |
2901 |
3167 |
2902 return BaseHTTPServer.HTTPServer((serve_address, port), handler_class) |
3168 return BaseHTTPServer.HTTPServer((serve_address, port), handler_class) |