|
1 #!/usr/bin/env python |
|
2 # |
|
3 # Copyright 2007 Google Inc. |
|
4 # |
|
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 |
|
8 # |
|
9 # http://www.apache.org/licenses/LICENSE-2.0 |
|
10 # |
|
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. |
|
16 # |
|
17 """Pure-Python application server for testing applications locally. |
|
18 |
|
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. |
|
23 |
|
24 Example: |
|
25 root_path = '/path/to/application/directory' |
|
26 login_url = '/login' |
|
27 port = 8080 |
|
28 template_dir = '/path/to/appserver/templates' |
|
29 server = dev_appserver.CreateServer(root_path, login_url, port, template_dir) |
|
30 server.serve_forever() |
|
31 """ |
|
32 |
|
33 |
|
34 import os |
|
35 os.environ['TZ'] = 'UTC' |
|
36 import time |
|
37 if hasattr(time, 'tzset'): |
|
38 time.tzset() |
|
39 |
|
40 import __builtin__ |
|
41 import BaseHTTPServer |
|
42 import Cookie |
|
43 import cStringIO |
|
44 import cgi |
|
45 import cgitb |
|
46 import dummy_thread |
|
47 import errno |
|
48 import httplib |
|
49 import imp |
|
50 import inspect |
|
51 import itertools |
|
52 import logging |
|
53 import mimetools |
|
54 import mimetypes |
|
55 import pickle |
|
56 import pprint |
|
57 import random |
|
58 |
|
59 import re |
|
60 import sre_compile |
|
61 import sre_constants |
|
62 import sre_parse |
|
63 |
|
64 import mimetypes |
|
65 import socket |
|
66 import sys |
|
67 import urlparse |
|
68 import urllib |
|
69 import traceback |
|
70 import types |
|
71 |
|
72 import google |
|
73 from google.pyglib import gexcept |
|
74 |
|
75 from google.appengine.api import apiproxy_stub_map |
|
76 from google.appengine.api import appinfo |
|
77 from google.appengine.api import datastore_admin |
|
78 from google.appengine.api import datastore_file_stub |
|
79 from google.appengine.api import urlfetch_stub |
|
80 from google.appengine.api import mail_stub |
|
81 from google.appengine.api import user_service_stub |
|
82 from google.appengine.api import yaml_errors |
|
83 from google.appengine.api.memcache import memcache_stub |
|
84 |
|
85 from google.appengine.tools import dev_appserver_index |
|
86 from google.appengine.tools import dev_appserver_login |
|
87 |
|
88 |
|
89 PYTHON_LIB_VAR = '$PYTHON_LIB' |
|
90 DEVEL_CONSOLE_PATH = PYTHON_LIB_VAR + '/google/appengine/ext/admin' |
|
91 |
|
92 FILE_MISSING_EXCEPTIONS = frozenset([errno.ENOENT, errno.ENOTDIR]) |
|
93 |
|
94 MAX_URL_LENGTH = 2047 |
|
95 |
|
96 HEADER_TEMPLATE = 'logging_console_header.html' |
|
97 SCRIPT_TEMPLATE = 'logging_console.js' |
|
98 MIDDLE_TEMPLATE = 'logging_console_middle.html' |
|
99 FOOTER_TEMPLATE = 'logging_console_footer.html' |
|
100 |
|
101 DEFAULT_ENV = { |
|
102 'GATEWAY_INTERFACE': 'CGI/1.1', |
|
103 'AUTH_DOMAIN': 'gmail.com', |
|
104 'TZ': 'UTC', |
|
105 } |
|
106 |
|
107 for ext, mime_type in (('.asc', 'text/plain'), |
|
108 ('.diff', 'text/plain'), |
|
109 ('.csv', 'text/comma-separated-values'), |
|
110 ('.rss', 'application/rss+xml'), |
|
111 ('.text', 'text/plain'), |
|
112 ('.wbmp', 'image/vnd.wap.wbmp')): |
|
113 mimetypes.add_type(mime_type, ext) |
|
114 |
|
115 |
|
116 class Error(Exception): |
|
117 """Base-class for exceptions in this module.""" |
|
118 |
|
119 class InvalidAppConfigError(Error): |
|
120 """The supplied application configuration file is invalid.""" |
|
121 |
|
122 class AppConfigNotFoundError(Error): |
|
123 """Application configuration file not found.""" |
|
124 |
|
125 class TemplatesNotLoadedError(Error): |
|
126 """Templates for the debugging console were not loaded.""" |
|
127 |
|
128 |
|
129 def SplitURL(relative_url): |
|
130 """Splits a relative URL into its path and query-string components. |
|
131 |
|
132 Args: |
|
133 relative_url: String containing the relative URL (often starting with '/') |
|
134 to split. Should be properly escaped as www-form-urlencoded data. |
|
135 |
|
136 Returns: |
|
137 Tuple (script_name, query_string) where: |
|
138 script_name: Relative URL of the script that was accessed. |
|
139 query_string: String containing everything after the '?' character. |
|
140 """ |
|
141 scheme, netloc, path, query, fragment = urlparse.urlsplit(relative_url) |
|
142 return path, query |
|
143 |
|
144 |
|
145 def GetFullURL(server_name, server_port, relative_url): |
|
146 """Returns the full, original URL used to access the relative URL. |
|
147 |
|
148 Args: |
|
149 server_name: Name of the local host, or the value of the 'host' header |
|
150 from the request. |
|
151 server_port: Port on which the request was served (string or int). |
|
152 relative_url: Relative URL that was accessed, including query string. |
|
153 |
|
154 Returns: |
|
155 String containing the original URL. |
|
156 """ |
|
157 if str(server_port) != '80': |
|
158 netloc = '%s:%s' % (server_name, server_port) |
|
159 else: |
|
160 netloc = server_name |
|
161 return 'http://%s%s' % (netloc, relative_url) |
|
162 |
|
163 |
|
164 class URLDispatcher(object): |
|
165 """Base-class for handling HTTP requests.""" |
|
166 |
|
167 def Dispatch(self, |
|
168 relative_url, |
|
169 path, |
|
170 headers, |
|
171 infile, |
|
172 outfile, |
|
173 base_env_dict=None): |
|
174 """Dispatch and handle an HTTP request. |
|
175 |
|
176 base_env_dict should contain at least these CGI variables: |
|
177 REQUEST_METHOD, REMOTE_ADDR, SERVER_SOFTWARE, SERVER_NAME, |
|
178 SERVER_PROTOCOL, SERVER_PORT |
|
179 |
|
180 Args: |
|
181 relative_url: String containing the URL accessed. |
|
182 path: Local path of the resource that was matched; back-references will be |
|
183 replaced by values matched in the relative_url. Path may be relative |
|
184 or absolute, depending on the resource being served (e.g., static files |
|
185 will have an absolute path; scripts will be relative). |
|
186 headers: Instance of mimetools.Message with headers from the request. |
|
187 infile: File-like object with input data from the request. |
|
188 outfile: File-like object where output data should be written. |
|
189 base_env_dict: Dictionary of CGI environment parameters if available. |
|
190 Defaults to None. |
|
191 """ |
|
192 raise NotImplementedError |
|
193 |
|
194 |
|
195 class URLMatcher(object): |
|
196 """Matches an arbitrary URL using a list of URL patterns from an application. |
|
197 |
|
198 Each URL pattern has an associated URLDispatcher instance and path to the |
|
199 resource's location on disk. See AddURL for more details. The first pattern |
|
200 that matches an inputted URL will have its associated values returned by |
|
201 Match(). |
|
202 """ |
|
203 |
|
204 def __init__(self): |
|
205 """Initializer.""" |
|
206 self._url_patterns = [] |
|
207 |
|
208 def AddURL(self, regex, dispatcher, path, requires_login, admin_only): |
|
209 """Adds a URL pattern to the list of patterns. |
|
210 |
|
211 If the supplied regex starts with a '^' or ends with a '$' an |
|
212 InvalidAppConfigError exception will be raised. Start and end symbols |
|
213 and implicitly added to all regexes, meaning we assume that all regexes |
|
214 consume all input from a URL. |
|
215 |
|
216 Args: |
|
217 regex: String containing the regular expression pattern. |
|
218 dispatcher: Instance of URLDispatcher that should handle requests that |
|
219 match this regex. |
|
220 path: Path on disk for the resource. May contain back-references like |
|
221 r'\1', r'\2', etc, which will be replaced by the corresponding groups |
|
222 matched by the regex if present. |
|
223 requires_login: True if the user must be logged-in before accessing this |
|
224 URL; False if anyone can access this URL. |
|
225 admin_only: True if the user must be a logged-in administrator to |
|
226 access the URL; False if anyone can access the URL. |
|
227 """ |
|
228 if not isinstance(dispatcher, URLDispatcher): |
|
229 raise TypeError, 'dispatcher must be a URLDispatcher sub-class' |
|
230 |
|
231 if regex.startswith('^') or regex.endswith('$'): |
|
232 raise InvalidAppConfigError, 'regex starts with "^" or ends with "$"' |
|
233 |
|
234 adjusted_regex = '^%s$' % regex |
|
235 |
|
236 try: |
|
237 url_re = re.compile(adjusted_regex) |
|
238 except re.error, e: |
|
239 raise InvalidAppConfigError, 'regex invalid: %s' % e |
|
240 |
|
241 match_tuple = (url_re, dispatcher, path, requires_login, admin_only) |
|
242 self._url_patterns.append(match_tuple) |
|
243 |
|
244 def Match(self, |
|
245 relative_url, |
|
246 split_url=SplitURL): |
|
247 """Matches a URL from a request against the list of URL patterns. |
|
248 |
|
249 The supplied relative_url may include the query string (i.e., the '?' |
|
250 character and everything following). |
|
251 |
|
252 Args: |
|
253 relative_url: Relative URL being accessed in a request. |
|
254 |
|
255 Returns: |
|
256 Tuple (dispatcher, matched_path, requires_login, admin_only), which are |
|
257 the corresponding values passed to AddURL when the matching URL pattern |
|
258 was added to this matcher. The matched_path will have back-references |
|
259 replaced using values matched by the URL pattern. If no match was found, |
|
260 dispatcher will be None. |
|
261 """ |
|
262 adjusted_url, query_string = split_url(relative_url) |
|
263 |
|
264 for url_tuple in self._url_patterns: |
|
265 url_re, dispatcher, path, requires_login, admin_only = url_tuple |
|
266 the_match = url_re.match(adjusted_url) |
|
267 |
|
268 if the_match: |
|
269 adjusted_path = the_match.expand(path) |
|
270 return dispatcher, adjusted_path, requires_login, admin_only |
|
271 |
|
272 return None, None, None, None |
|
273 |
|
274 def GetDispatchers(self): |
|
275 """Retrieves the URLDispatcher objects that could be matched. |
|
276 |
|
277 Should only be used in tests. |
|
278 |
|
279 Returns: |
|
280 A set of URLDispatcher objects. |
|
281 """ |
|
282 return set([url_tuple[1] for url_tuple in self._url_patterns]) |
|
283 |
|
284 |
|
285 class MatcherDispatcher(URLDispatcher): |
|
286 """Dispatcher across multiple URLMatcher instances.""" |
|
287 |
|
288 def __init__(self, |
|
289 login_url, |
|
290 url_matchers, |
|
291 get_user_info=dev_appserver_login.GetUserInfo, |
|
292 login_redirect=dev_appserver_login.LoginRedirect): |
|
293 """Initializer. |
|
294 |
|
295 Args: |
|
296 login_url: Relative URL which should be used for handling user logins. |
|
297 url_matchers: Sequence of URLMatcher objects. |
|
298 get_user_info, login_redirect: Used for dependency injection. |
|
299 """ |
|
300 self._login_url = login_url |
|
301 self._url_matchers = tuple(url_matchers) |
|
302 self._get_user_info = get_user_info |
|
303 self._login_redirect = login_redirect |
|
304 |
|
305 def Dispatch(self, |
|
306 relative_url, |
|
307 path, |
|
308 headers, |
|
309 infile, |
|
310 outfile, |
|
311 base_env_dict=None): |
|
312 """Dispatches a request to the first matching dispatcher. |
|
313 |
|
314 Matchers are checked in the order they were supplied to the constructor. |
|
315 If no matcher matches, a 404 error will be written to the outfile. The |
|
316 path variable supplied to this method is ignored. |
|
317 """ |
|
318 cookies = ', '.join(headers.getheaders('cookie')) |
|
319 email, admin = self._get_user_info(cookies) |
|
320 |
|
321 for matcher in self._url_matchers: |
|
322 dispatcher, matched_path, requires_login, admin_only = matcher.Match(relative_url) |
|
323 if dispatcher is None: |
|
324 continue |
|
325 |
|
326 logging.debug('Matched "%s" to %s with path %s', |
|
327 relative_url, dispatcher, matched_path) |
|
328 |
|
329 if (requires_login or admin_only) and not email: |
|
330 logging.debug('Login required, redirecting user') |
|
331 self._login_redirect( |
|
332 self._login_url, |
|
333 base_env_dict['SERVER_NAME'], |
|
334 base_env_dict['SERVER_PORT'], |
|
335 relative_url, |
|
336 outfile) |
|
337 elif admin_only and not admin: |
|
338 outfile.write('Status: %d Not authorized\r\n' |
|
339 '\r\n' |
|
340 'Current logged in user %s is not ' |
|
341 'authorized to view this page.' |
|
342 % (httplib.FORBIDDEN, email)) |
|
343 else: |
|
344 dispatcher.Dispatch(relative_url, |
|
345 matched_path, |
|
346 headers, |
|
347 infile, |
|
348 outfile, |
|
349 base_env_dict=base_env_dict) |
|
350 |
|
351 return |
|
352 |
|
353 outfile.write('Status: %d URL did not match\r\n' |
|
354 '\r\n' |
|
355 'Not found error: %s did not match any patterns ' |
|
356 'in application configuration.' |
|
357 % (httplib.NOT_FOUND, relative_url)) |
|
358 |
|
359 |
|
360 class ApplicationLoggingHandler(logging.Handler): |
|
361 """Python Logging handler that displays the debugging console to users.""" |
|
362 |
|
363 _COOKIE_NAME = '_ah_severity' |
|
364 |
|
365 _TEMPLATES_INITIALIZED = False |
|
366 _HEADER = None |
|
367 _SCRIPT = None |
|
368 _MIDDLE = None |
|
369 _FOOTER = None |
|
370 |
|
371 @staticmethod |
|
372 def InitializeTemplates(header, script, middle, footer): |
|
373 """Initializes the templates used to render the debugging console. |
|
374 |
|
375 This method must be called before any ApplicationLoggingHandler instances |
|
376 are created. |
|
377 |
|
378 Args: |
|
379 header: The header template that is printed first. |
|
380 script: The script template that is printed after the logging messages. |
|
381 middle: The middle element that's printed before the footer. |
|
382 footer; The last element that's printed at the end of the document. |
|
383 """ |
|
384 ApplicationLoggingHandler._HEADER = header |
|
385 ApplicationLoggingHandler._SCRIPT = script |
|
386 ApplicationLoggingHandler._MIDDLE = middle |
|
387 ApplicationLoggingHandler._FOOTER = footer |
|
388 ApplicationLoggingHandler._TEMPLATES_INITIALIZED = True |
|
389 |
|
390 @staticmethod |
|
391 def AreTemplatesInitialized(): |
|
392 """Returns True if InitializeTemplates has been called, False otherwise.""" |
|
393 return ApplicationLoggingHandler._TEMPLATES_INITIALIZED |
|
394 |
|
395 def __init__(self, *args, **kwargs): |
|
396 """Initializer. |
|
397 |
|
398 Args: |
|
399 args, kwargs: See logging.Handler. |
|
400 |
|
401 Raises: |
|
402 TemplatesNotLoadedError exception if the InitializeTemplates method was |
|
403 not called before creating this instance. |
|
404 """ |
|
405 if not self._TEMPLATES_INITIALIZED: |
|
406 raise TemplatesNotLoadedError |
|
407 |
|
408 logging.Handler.__init__(self, *args, **kwargs) |
|
409 self._record_list = [] |
|
410 self._start_time = time.time() |
|
411 |
|
412 def emit(self, record): |
|
413 """Called by the logging module each time the application logs a message. |
|
414 |
|
415 Args: |
|
416 record: logging.LogRecord instance corresponding to the newly logged |
|
417 message. |
|
418 """ |
|
419 self._record_list.append(record) |
|
420 |
|
421 def AddDebuggingConsole(self, relative_url, env, outfile): |
|
422 """Prints an HTML debugging console to an output stream, if requested. |
|
423 |
|
424 Args: |
|
425 relative_url: Relative URL that was accessed, including the query string. |
|
426 Used to determine if the parameter 'debug' was supplied, in which case |
|
427 the console will be shown. |
|
428 env: Dictionary containing CGI environment variables. Checks for the |
|
429 HTTP_COOKIE entry to see if the accessing user has any logging-related |
|
430 cookies set. |
|
431 outfile: Output stream to which the console should be written if either |
|
432 a debug parameter was supplied or a logging cookie is present. |
|
433 """ |
|
434 script_name, query_string = SplitURL(relative_url) |
|
435 param_dict = cgi.parse_qs(query_string, True) |
|
436 cookie_dict = Cookie.SimpleCookie(env.get('HTTP_COOKIE', '')) |
|
437 if 'debug' not in param_dict and self._COOKIE_NAME not in cookie_dict: |
|
438 return |
|
439 |
|
440 outfile.write(self._HEADER) |
|
441 for record in self._record_list: |
|
442 self._PrintRecord(record, outfile) |
|
443 |
|
444 outfile.write(self._MIDDLE) |
|
445 outfile.write(self._SCRIPT) |
|
446 outfile.write(self._FOOTER) |
|
447 |
|
448 def _PrintRecord(self, record, outfile): |
|
449 """Prints a single logging record to an output stream. |
|
450 |
|
451 Args: |
|
452 record: logging.LogRecord instance to print. |
|
453 outfile: Output stream to which the LogRecord should be printed. |
|
454 """ |
|
455 message = cgi.escape(record.getMessage()) |
|
456 level_name = logging.getLevelName(record.levelno).lower() |
|
457 level_letter = level_name[:1].upper() |
|
458 time_diff = record.created - self._start_time |
|
459 outfile.write('<span class="_ah_logline_%s">\n' % level_name) |
|
460 outfile.write('<span class="_ah_logline_%s_prefix">%2.5f %s ></span>\n' |
|
461 % (level_name, time_diff, level_letter)) |
|
462 outfile.write('%s\n' % message) |
|
463 outfile.write('</span>\n') |
|
464 |
|
465 |
|
466 _IGNORE_HEADERS = frozenset(['content-type', 'content-length']) |
|
467 |
|
468 def SetupEnvironment(cgi_path, |
|
469 relative_url, |
|
470 headers, |
|
471 split_url=SplitURL, |
|
472 get_user_info=dev_appserver_login.GetUserInfo): |
|
473 """Sets up environment variables for a CGI. |
|
474 |
|
475 Args: |
|
476 cgi_path: Full file-system path to the CGI being executed. |
|
477 relative_url: Relative URL used to access the CGI. |
|
478 headers: Instance of mimetools.Message containing request headers. |
|
479 split_url, get_user_info: Used for dependency injection. |
|
480 |
|
481 Returns: |
|
482 Dictionary containing CGI environment variables. |
|
483 """ |
|
484 env = DEFAULT_ENV.copy() |
|
485 |
|
486 script_name, query_string = split_url(relative_url) |
|
487 |
|
488 env['SCRIPT_NAME'] = '' |
|
489 env['QUERY_STRING'] = query_string |
|
490 env['PATH_INFO'] = urllib.unquote(script_name) |
|
491 env['PATH_TRANSLATED'] = cgi_path |
|
492 env['CONTENT_TYPE'] = headers.getheader('content-type', |
|
493 'application/x-www-form-urlencoded') |
|
494 env['CONTENT_LENGTH'] = headers.getheader('content-length', '') |
|
495 |
|
496 cookies = ', '.join(headers.getheaders('cookie')) |
|
497 email, admin = get_user_info(cookies) |
|
498 env['USER_EMAIL'] = email |
|
499 if admin: |
|
500 env['USER_IS_ADMIN'] = '1' |
|
501 |
|
502 for key in headers: |
|
503 if key in _IGNORE_HEADERS: |
|
504 continue |
|
505 adjusted_name = key.replace('-', '_').upper() |
|
506 env['HTTP_' + adjusted_name] = ', '.join(headers.getheaders(key)) |
|
507 |
|
508 return env |
|
509 |
|
510 |
|
511 def FakeTemporaryFile(*args, **kwargs): |
|
512 """Fake for tempfile.TemporaryFile that just uses StringIO.""" |
|
513 return cStringIO.StringIO() |
|
514 |
|
515 |
|
516 def NotImplementedFake(*args, **kwargs): |
|
517 """Fake for methods/classes that are not implemented in the production |
|
518 environment. |
|
519 """ |
|
520 raise NotImplementedError("This class/method is not available.") |
|
521 |
|
522 |
|
523 def IsEncodingsModule(module_name): |
|
524 """Determines if the supplied module is related to encodings in any way. |
|
525 |
|
526 Encodings-related modules cannot be reloaded, so they need to be treated |
|
527 specially when sys.modules is modified in any way. |
|
528 |
|
529 Args: |
|
530 module_name: Absolute name of the module regardless of how it is imported |
|
531 into the local namespace (e.g., foo.bar.baz). |
|
532 |
|
533 Returns: |
|
534 True if it's an encodings-related module; False otherwise. |
|
535 """ |
|
536 if (module_name in ('codecs', 'encodings') or |
|
537 module_name.startswith('encodings.')): |
|
538 return True |
|
539 return False |
|
540 |
|
541 |
|
542 def ClearAllButEncodingsModules(module_dict): |
|
543 """Clear all modules in a module dictionary except for those modules that |
|
544 are in any way related to encodings. |
|
545 |
|
546 Args: |
|
547 module_dict: Dictionary in the form used by sys.modules. |
|
548 """ |
|
549 for module_name in module_dict.keys(): |
|
550 if not IsEncodingsModule(module_name): |
|
551 del module_dict[module_name] |
|
552 |
|
553 |
|
554 def FakeURandom(n): |
|
555 """Fake version of os.urandom.""" |
|
556 bytes = '' |
|
557 for i in xrange(n): |
|
558 bytes += chr(random.randint(0, 255)) |
|
559 return bytes |
|
560 |
|
561 |
|
562 def FakeUname(): |
|
563 """Fake version of os.uname.""" |
|
564 return ('Linux', '', '', '', '') |
|
565 |
|
566 |
|
567 def IsPathInSubdirectories(filename, |
|
568 subdirectories, |
|
569 normcase=os.path.normcase): |
|
570 """Determines if a filename is contained within one of a set of directories. |
|
571 |
|
572 Args: |
|
573 filename: Path of the file (relative or absolute). |
|
574 subdirectories: Iterable collection of paths to subdirectories which the |
|
575 given filename may be under. |
|
576 normcase: Used for dependency injection. |
|
577 |
|
578 Returns: |
|
579 True if the supplied filename is in one of the given sub-directories or |
|
580 its hierarchy of children. False otherwise. |
|
581 """ |
|
582 file_dir = normcase(os.path.dirname(os.path.abspath(filename))) |
|
583 for parent in subdirectories: |
|
584 fixed_parent = normcase(os.path.abspath(parent)) |
|
585 if os.path.commonprefix([file_dir, fixed_parent]) == fixed_parent: |
|
586 return True |
|
587 return False |
|
588 |
|
589 SHARED_MODULE_PREFIXES = set([ |
|
590 'google', |
|
591 'logging', |
|
592 'sys', |
|
593 'warnings', |
|
594 |
|
595 |
|
596 |
|
597 |
|
598 're', |
|
599 'sre_compile', |
|
600 'sre_constants', |
|
601 'sre_parse', |
|
602 |
|
603 |
|
604 |
|
605 |
|
606 'wsgiref', |
|
607 ]) |
|
608 |
|
609 NOT_SHARED_MODULE_PREFIXES = set([ |
|
610 'google.appengine.ext', |
|
611 ]) |
|
612 |
|
613 |
|
614 def ModuleNameHasPrefix(module_name, prefix_set): |
|
615 """Determines if a module's name belongs to a set of prefix strings. |
|
616 |
|
617 Args: |
|
618 module_name: String containing the fully qualified module name. |
|
619 prefix_set: Iterable set of module name prefixes to check against. |
|
620 |
|
621 Returns: |
|
622 True if the module_name belongs to the prefix set or is a submodule of |
|
623 any of the modules specified in the prefix_set. Otherwise False. |
|
624 """ |
|
625 for prefix in prefix_set: |
|
626 if prefix == module_name: |
|
627 return True |
|
628 |
|
629 if module_name.startswith(prefix + '.'): |
|
630 return True |
|
631 |
|
632 return False |
|
633 |
|
634 |
|
635 def SetupSharedModules(module_dict): |
|
636 """Creates a module dictionary for the hardened part of the process. |
|
637 |
|
638 Module dictionary will contain modules that should be shared between the |
|
639 hardened and unhardened parts of the process. |
|
640 |
|
641 Args: |
|
642 module_dict: Module dictionary from which existing modules should be |
|
643 pulled (usually sys.modules). |
|
644 |
|
645 Returns: |
|
646 A new module dictionary. |
|
647 """ |
|
648 output_dict = {} |
|
649 for module_name, module in module_dict.iteritems(): |
|
650 if module is None: |
|
651 continue |
|
652 |
|
653 if IsEncodingsModule(module_name): |
|
654 output_dict[module_name] = module |
|
655 continue |
|
656 |
|
657 shared_prefix = ModuleNameHasPrefix(module_name, SHARED_MODULE_PREFIXES) |
|
658 banned_prefix = ModuleNameHasPrefix(module_name, NOT_SHARED_MODULE_PREFIXES) |
|
659 |
|
660 if shared_prefix and not banned_prefix: |
|
661 output_dict[module_name] = module |
|
662 |
|
663 return output_dict |
|
664 |
|
665 |
|
666 class FakeFile(file): |
|
667 """File sub-class that enforces the security restrictions of the production |
|
668 environment. |
|
669 """ |
|
670 |
|
671 ALLOWED_MODES = frozenset(['r', 'rb', 'U', 'rU']) |
|
672 |
|
673 ALLOWED_FILES = set(os.path.normcase(filename) |
|
674 for filename in mimetypes.knownfiles |
|
675 if os.path.isfile(filename)) |
|
676 |
|
677 ALLOWED_DIRS = set([ |
|
678 os.path.normcase(os.path.abspath(os.path.dirname(os.__file__))) |
|
679 ]) |
|
680 |
|
681 NOT_ALLOWED_DIRS = set([ |
|
682 |
|
683 |
|
684 |
|
685 |
|
686 os.path.normcase(os.path.join(os.path.dirname(os.__file__), |
|
687 'site-packages')) |
|
688 ]) |
|
689 |
|
690 ALLOWED_SITE_PACKAGE_DIRS = set( |
|
691 os.path.normcase(os.path.abspath(os.path.join( |
|
692 os.path.dirname(os.__file__), 'site-packages', path))) |
|
693 for path in [ |
|
694 |
|
695 ]) |
|
696 |
|
697 _application_paths = None |
|
698 _original_file = file |
|
699 |
|
700 @staticmethod |
|
701 def SetAllowedPaths(application_paths): |
|
702 """Sets the root path of the application that is currently running. |
|
703 |
|
704 Must be called at least once before any file objects are created in the |
|
705 hardened environment. |
|
706 |
|
707 Args: |
|
708 root_path: Path to the root of the application. |
|
709 """ |
|
710 FakeFile._application_paths = set(os.path.abspath(path) |
|
711 for path in application_paths) |
|
712 |
|
713 @staticmethod |
|
714 def IsFileAccessible(filename, normcase=os.path.normcase): |
|
715 """Determines if a file's path is accessible. |
|
716 |
|
717 SetAllowedPaths() must be called before this method or else all file |
|
718 accesses will raise an error. |
|
719 |
|
720 Args: |
|
721 filename: Path of the file to check (relative or absolute). May be a |
|
722 directory, in which case access for files inside that directory will |
|
723 be checked. |
|
724 normcase: Used for dependency injection. |
|
725 |
|
726 Returns: |
|
727 True if the file is accessible, False otherwise. |
|
728 """ |
|
729 logical_filename = normcase(os.path.abspath(filename)) |
|
730 |
|
731 if os.path.isdir(logical_filename): |
|
732 logical_filename = os.path.join(logical_filename, 'foo') |
|
733 |
|
734 if logical_filename in FakeFile.ALLOWED_FILES: |
|
735 return True |
|
736 |
|
737 if IsPathInSubdirectories(logical_filename, |
|
738 FakeFile.ALLOWED_SITE_PACKAGE_DIRS, |
|
739 normcase=normcase): |
|
740 return True |
|
741 |
|
742 allowed_dirs = FakeFile._application_paths | FakeFile.ALLOWED_DIRS |
|
743 if (IsPathInSubdirectories(logical_filename, |
|
744 allowed_dirs, |
|
745 normcase=normcase) and |
|
746 not IsPathInSubdirectories(logical_filename, |
|
747 FakeFile.NOT_ALLOWED_DIRS, |
|
748 normcase=normcase)): |
|
749 return True |
|
750 |
|
751 return False |
|
752 |
|
753 def __init__(self, filename, mode='r', **kwargs): |
|
754 """Initializer. See file built-in documentation.""" |
|
755 if mode not in FakeFile.ALLOWED_MODES: |
|
756 raise IOError('invalid mode: %s' % mode) |
|
757 |
|
758 if not FakeFile.IsFileAccessible(filename): |
|
759 raise IOError(errno.EACCES, 'file not accessible') |
|
760 |
|
761 super(FakeFile, self).__init__(filename, mode, **kwargs) |
|
762 |
|
763 |
|
764 class RestrictedPathFunction(object): |
|
765 """Enforces access restrictions for functions that have a file or |
|
766 directory path as their first argument.""" |
|
767 |
|
768 _original_os = os |
|
769 |
|
770 def __init__(self, original_func): |
|
771 """Initializer. |
|
772 |
|
773 Args: |
|
774 original_func: Callable that takes as its first argument the path to a |
|
775 file or directory on disk; all subsequent arguments may be variable. |
|
776 """ |
|
777 self._original_func = original_func |
|
778 |
|
779 def __call__(self, path, *args, **kwargs): |
|
780 """Enforces access permissions for the function passed to the constructor. |
|
781 """ |
|
782 if not FakeFile.IsFileAccessible(path): |
|
783 raise OSError(errno.EACCES, 'path not accessible') |
|
784 |
|
785 return self._original_func(path, *args, **kwargs) |
|
786 |
|
787 |
|
788 def GetSubmoduleName(fullname): |
|
789 """Determines the leaf submodule name of a full module name. |
|
790 |
|
791 Args: |
|
792 fullname: Fully qualified module name, e.g. 'foo.bar.baz' |
|
793 |
|
794 Returns: |
|
795 Submodule name, e.g. 'baz'. If the supplied module has no submodule (e.g., |
|
796 'stuff'), the returned value will just be that module name ('stuff'). |
|
797 """ |
|
798 return fullname.rsplit('.', 1)[-1] |
|
799 |
|
800 |
|
801 class CouldNotFindModuleError(ImportError): |
|
802 """Raised when a module could not be found. |
|
803 |
|
804 In contrast to when a module has been found, but cannot be loaded because of |
|
805 hardening restrictions. |
|
806 """ |
|
807 |
|
808 |
|
809 def Trace(func): |
|
810 """Decorator that logs the call stack of the HardenedModulesHook class as |
|
811 it executes, indenting logging messages based on the current stack depth. |
|
812 """ |
|
813 def decorate(self, *args, **kwargs): |
|
814 args_to_show = [] |
|
815 if args is not None: |
|
816 args_to_show.extend(str(argument) for argument in args) |
|
817 if kwargs is not None: |
|
818 args_to_show.extend('%s=%s' % (key, value) |
|
819 for key, value in kwargs.iteritems()) |
|
820 |
|
821 args_string = ', '.join(args_to_show) |
|
822 |
|
823 self.log('Entering %s(%s)', func.func_name, args_string) |
|
824 self._indent_level += 1 |
|
825 try: |
|
826 return func(self, *args, **kwargs) |
|
827 finally: |
|
828 self._indent_level -= 1 |
|
829 self.log('Exiting %s(%s)', func.func_name, args_string) |
|
830 |
|
831 return decorate |
|
832 |
|
833 |
|
834 class HardenedModulesHook(object): |
|
835 """Meta import hook that restricts the modules used by applications to match |
|
836 the production environment. |
|
837 |
|
838 Module controls supported: |
|
839 - Disallow native/extension modules from being loaded |
|
840 - Disallow built-in and/or Python-distributed modules from being loaded |
|
841 - Replace modules with completely empty modules |
|
842 - Override specific module attributes |
|
843 - Replace one module with another |
|
844 |
|
845 After creation, this object should be added to the front of the sys.meta_path |
|
846 list (which may need to be created). The sys.path_importer_cache dictionary |
|
847 should also be cleared, to prevent loading any non-restricted modules. |
|
848 |
|
849 See PEP302 for more info on how this works: |
|
850 http://www.python.org/dev/peps/pep-0302/ |
|
851 """ |
|
852 |
|
853 ENABLE_LOGGING = False |
|
854 |
|
855 def log(self, message, *args): |
|
856 """Logs an import-related message to stderr, with indentation based on |
|
857 current call-stack depth. |
|
858 |
|
859 Args: |
|
860 message: Logging format string. |
|
861 args: Positional format parameters for the logging message. |
|
862 """ |
|
863 if HardenedModulesHook.ENABLE_LOGGING: |
|
864 indent = self._indent_level * ' ' |
|
865 print >>sys.stderr, indent + (message % args) |
|
866 |
|
867 EMPTY_MODULE_FILE = '<empty module>' |
|
868 |
|
869 _WHITE_LIST_C_MODULES = [ |
|
870 'array', |
|
871 'binascii', |
|
872 'bz2', |
|
873 'cmath', |
|
874 'collections', |
|
875 'crypt', |
|
876 'cStringIO', |
|
877 'datetime', |
|
878 'errno', |
|
879 'exceptions', |
|
880 'gc', |
|
881 'itertools', |
|
882 'math', |
|
883 'md5', |
|
884 'operator', |
|
885 'posix', |
|
886 'posixpath', |
|
887 'pyexpat', |
|
888 'sha', |
|
889 'struct', |
|
890 'sys', |
|
891 'time', |
|
892 'timing', |
|
893 'unicodedata', |
|
894 'zlib', |
|
895 '_bisect', |
|
896 '_codecs', |
|
897 '_codecs_cn', |
|
898 '_codecs_hk', |
|
899 '_codecs_iso2022', |
|
900 '_codecs_jp', |
|
901 '_codecs_kr', |
|
902 '_codecs_tw', |
|
903 '_csv', |
|
904 '_elementtree', |
|
905 '_functools', |
|
906 '_hashlib', |
|
907 '_heapq', |
|
908 '_locale', |
|
909 '_lsprof', |
|
910 '_md5', |
|
911 '_multibytecodec', |
|
912 '_random', |
|
913 '_sha', |
|
914 '_sha256', |
|
915 '_sha512', |
|
916 '_sre', |
|
917 '_struct', |
|
918 '_types', |
|
919 '_weakref', |
|
920 '__main__', |
|
921 ] |
|
922 |
|
923 _WHITE_LIST_PARTIAL_MODULES = { |
|
924 'gc': [ |
|
925 'enable', |
|
926 'disable', |
|
927 'isenabled', |
|
928 'collect', |
|
929 'get_debug', |
|
930 'set_threshold', |
|
931 'get_threshold', |
|
932 'get_count' |
|
933 ], |
|
934 |
|
935 |
|
936 |
|
937 'os': [ |
|
938 'altsep', |
|
939 'curdir', |
|
940 'defpath', |
|
941 'devnull', |
|
942 'environ', |
|
943 'error', |
|
944 'extsep', |
|
945 'EX_NOHOST', |
|
946 'EX_NOINPUT', |
|
947 'EX_NOPERM', |
|
948 'EX_NOUSER', |
|
949 'EX_OK', |
|
950 'EX_OSERR', |
|
951 'EX_OSFILE', |
|
952 'EX_PROTOCOL', |
|
953 'EX_SOFTWARE', |
|
954 'EX_TEMPFAIL', |
|
955 'EX_UNAVAILABLE', |
|
956 'EX_USAGE', |
|
957 'F_OK', |
|
958 'getcwd', |
|
959 'getcwdu', |
|
960 'getenv', |
|
961 'listdir', |
|
962 'lstat', |
|
963 'name', |
|
964 'NGROUPS_MAX', |
|
965 'O_APPEND', |
|
966 'O_CREAT', |
|
967 'O_DIRECT', |
|
968 'O_DIRECTORY', |
|
969 'O_DSYNC', |
|
970 'O_EXCL', |
|
971 'O_LARGEFILE', |
|
972 'O_NDELAY', |
|
973 'O_NOCTTY', |
|
974 'O_NOFOLLOW', |
|
975 'O_NONBLOCK', |
|
976 'O_RDONLY', |
|
977 'O_RDWR', |
|
978 'O_RSYNC', |
|
979 'O_SYNC', |
|
980 'O_TRUNC', |
|
981 'O_WRONLY', |
|
982 'pardir', |
|
983 'path', |
|
984 'pathsep', |
|
985 'R_OK', |
|
986 'SEEK_CUR', |
|
987 'SEEK_END', |
|
988 'SEEK_SET', |
|
989 'sep', |
|
990 'stat', |
|
991 'stat_float_times', |
|
992 'stat_result', |
|
993 'strerror', |
|
994 'TMP_MAX', |
|
995 'urandom', |
|
996 'walk', |
|
997 'WCOREDUMP', |
|
998 'WEXITSTATUS', |
|
999 'WIFEXITED', |
|
1000 'WIFSIGNALED', |
|
1001 'WIFSTOPPED', |
|
1002 'WNOHANG', |
|
1003 'WSTOPSIG', |
|
1004 'WTERMSIG', |
|
1005 'WUNTRACED', |
|
1006 'W_OK', |
|
1007 'X_OK', |
|
1008 ], |
|
1009 } |
|
1010 |
|
1011 _EMPTY_MODULES = [ |
|
1012 'imp', |
|
1013 'ftplib', |
|
1014 'select', |
|
1015 'socket', |
|
1016 'tempfile', |
|
1017 ] |
|
1018 |
|
1019 _MODULE_OVERRIDES = { |
|
1020 'os': { |
|
1021 'listdir': RestrictedPathFunction(os.listdir), |
|
1022 'lstat': RestrictedPathFunction(os.lstat), |
|
1023 'stat': RestrictedPathFunction(os.stat), |
|
1024 'uname': FakeUname, |
|
1025 'urandom': FakeURandom, |
|
1026 }, |
|
1027 |
|
1028 'socket': { |
|
1029 'AF_INET': None, |
|
1030 'SOCK_STREAM': None, |
|
1031 'SOCK_DGRAM': None, |
|
1032 }, |
|
1033 |
|
1034 'tempfile': { |
|
1035 'TemporaryFile': FakeTemporaryFile, |
|
1036 'gettempdir': NotImplementedFake, |
|
1037 'gettempprefix': NotImplementedFake, |
|
1038 'mkdtemp': NotImplementedFake, |
|
1039 'mkstemp': NotImplementedFake, |
|
1040 'mktemp': NotImplementedFake, |
|
1041 'NamedTemporaryFile': NotImplementedFake, |
|
1042 'tempdir': NotImplementedFake, |
|
1043 }, |
|
1044 } |
|
1045 |
|
1046 _ENABLED_FILE_TYPES = ( |
|
1047 imp.PKG_DIRECTORY, |
|
1048 imp.PY_SOURCE, |
|
1049 imp.PY_COMPILED, |
|
1050 imp.C_BUILTIN, |
|
1051 ) |
|
1052 |
|
1053 def __init__(self, |
|
1054 module_dict, |
|
1055 imp_module=imp, |
|
1056 os_module=os, |
|
1057 dummy_thread_module=dummy_thread, |
|
1058 pickle_module=pickle): |
|
1059 """Initializer. |
|
1060 |
|
1061 Args: |
|
1062 module_dict: Module dictionary to use for managing system modules. |
|
1063 Should be sys.modules. |
|
1064 imp_module, os_module, dummy_thread_module, pickle_module: References to |
|
1065 modules that exist in the dev_appserver that must be used by this class |
|
1066 in order to function, even if these modules have been unloaded from |
|
1067 sys.modules. |
|
1068 """ |
|
1069 self._module_dict = module_dict |
|
1070 self._imp = imp_module |
|
1071 self._os = os_module |
|
1072 self._dummy_thread = dummy_thread_module |
|
1073 self._pickle = pickle |
|
1074 self._indent_level = 0 |
|
1075 |
|
1076 @Trace |
|
1077 def find_module(self, fullname, path=None): |
|
1078 """See PEP 302.""" |
|
1079 if (fullname in ('cPickle', 'thread') or |
|
1080 fullname in HardenedModulesHook._EMPTY_MODULES): |
|
1081 return self |
|
1082 |
|
1083 search_path = path |
|
1084 all_modules = fullname.split('.') |
|
1085 try: |
|
1086 for index, current_module in enumerate(all_modules): |
|
1087 current_module_fullname = '.'.join(all_modules[:index + 1]) |
|
1088 if current_module_fullname == fullname: |
|
1089 self.FindModuleRestricted(current_module, |
|
1090 current_module_fullname, |
|
1091 search_path) |
|
1092 else: |
|
1093 if current_module_fullname in self._module_dict: |
|
1094 module = self._module_dict[current_module_fullname] |
|
1095 else: |
|
1096 module = self.FindAndLoadModule(current_module, |
|
1097 current_module_fullname, |
|
1098 search_path) |
|
1099 |
|
1100 if hasattr(module, '__path__'): |
|
1101 search_path = module.__path__ |
|
1102 except CouldNotFindModuleError: |
|
1103 return None |
|
1104 |
|
1105 return self |
|
1106 |
|
1107 @Trace |
|
1108 def FixModule(self, module): |
|
1109 """Prunes and overrides restricted module attributes. |
|
1110 |
|
1111 Args: |
|
1112 module: The module to prune. This should be a new module whose attributes |
|
1113 reference back to the real module's __dict__ members. |
|
1114 """ |
|
1115 if module.__name__ in self._WHITE_LIST_PARTIAL_MODULES: |
|
1116 allowed_symbols = self._WHITE_LIST_PARTIAL_MODULES[module.__name__] |
|
1117 for symbol in set(module.__dict__) - set(allowed_symbols): |
|
1118 if not (symbol.startswith('__') and symbol.endswith('__')): |
|
1119 del module.__dict__[symbol] |
|
1120 |
|
1121 if module.__name__ in self._MODULE_OVERRIDES: |
|
1122 module.__dict__.update(self._MODULE_OVERRIDES[module.__name__]) |
|
1123 |
|
1124 @Trace |
|
1125 def FindModuleRestricted(self, |
|
1126 submodule, |
|
1127 submodule_fullname, |
|
1128 search_path): |
|
1129 """Locates a module while enforcing module import restrictions. |
|
1130 |
|
1131 Args: |
|
1132 submodule: The short name of the submodule (i.e., the last section of |
|
1133 the fullname; for 'foo.bar' this would be 'bar'). |
|
1134 submodule_fullname: The fully qualified name of the module to find (e.g., |
|
1135 'foo.bar'). |
|
1136 search_path: List of paths to search for to find this module. Should be |
|
1137 None if the current sys.path should be used. |
|
1138 |
|
1139 Returns: |
|
1140 Tuple (source_file, pathname, description) where: |
|
1141 source_file: File-like object that contains the module; in the case |
|
1142 of packages, this will be None, which implies to look at __init__.py. |
|
1143 pathname: String containing the full path of the module on disk. |
|
1144 description: Tuple returned by imp.find_module(). |
|
1145 |
|
1146 Raises: |
|
1147 ImportError exception if the requested module was found, but importing |
|
1148 it is disallowed. |
|
1149 |
|
1150 CouldNotFindModuleError exception if the request module could not even |
|
1151 be found for import. |
|
1152 """ |
|
1153 try: |
|
1154 source_file, pathname, description = self._imp.find_module(submodule, search_path) |
|
1155 except ImportError: |
|
1156 self.log('Could not find module "%s"', submodule_fullname) |
|
1157 raise CouldNotFindModuleError() |
|
1158 |
|
1159 suffix, mode, file_type = description |
|
1160 |
|
1161 if (file_type not in (self._imp.C_BUILTIN, self._imp.C_EXTENSION) and |
|
1162 not FakeFile.IsFileAccessible(pathname)): |
|
1163 error_message = 'Access to module file denied: %s' % pathname |
|
1164 logging.debug(error_message) |
|
1165 raise ImportError(error_message) |
|
1166 |
|
1167 if (file_type not in self._ENABLED_FILE_TYPES and |
|
1168 submodule not in self._WHITE_LIST_C_MODULES): |
|
1169 error_message = ('Could not import "%s": Disallowed C-extension ' |
|
1170 'or built-in module' % submodule_fullname) |
|
1171 logging.debug(error_message) |
|
1172 raise ImportError(error_message) |
|
1173 |
|
1174 return source_file, pathname, description |
|
1175 |
|
1176 @Trace |
|
1177 def LoadModuleRestricted(self, |
|
1178 submodule_fullname, |
|
1179 source_file, |
|
1180 pathname, |
|
1181 description): |
|
1182 """Loads a module while enforcing module import restrictions. |
|
1183 |
|
1184 As a byproduct, the new module will be added to the module dictionary. |
|
1185 |
|
1186 Args: |
|
1187 submodule_fullname: The fully qualified name of the module to find (e.g., |
|
1188 'foo.bar'). |
|
1189 source_file: File-like object that contains the module's source code. |
|
1190 pathname: String containing the full path of the module on disk. |
|
1191 description: Tuple returned by imp.find_module(). |
|
1192 |
|
1193 Returns: |
|
1194 The new module. |
|
1195 |
|
1196 Raises: |
|
1197 ImportError exception of the specified module could not be loaded for |
|
1198 whatever reason. |
|
1199 """ |
|
1200 try: |
|
1201 try: |
|
1202 return self._imp.load_module(submodule_fullname, |
|
1203 source_file, |
|
1204 pathname, |
|
1205 description) |
|
1206 except: |
|
1207 if submodule_fullname in self._module_dict: |
|
1208 del self._module_dict[submodule_fullname] |
|
1209 raise |
|
1210 |
|
1211 finally: |
|
1212 if source_file is not None: |
|
1213 source_file.close() |
|
1214 |
|
1215 @Trace |
|
1216 def FindAndLoadModule(self, |
|
1217 submodule, |
|
1218 submodule_fullname, |
|
1219 search_path): |
|
1220 """Finds and loads a module, loads it, and adds it to the module dictionary. |
|
1221 |
|
1222 Args: |
|
1223 submodule: Name of the module to import (e.g., baz). |
|
1224 submodule_fullname: Full name of the module to import (e.g., foo.bar.baz). |
|
1225 search_path: Path to use for searching for this submodule. For top-level |
|
1226 modules this should be None; otherwise it should be the __path__ |
|
1227 attribute from the parent package. |
|
1228 |
|
1229 Returns: |
|
1230 A new module instance that has been inserted into the module dictionary |
|
1231 supplied to __init__. |
|
1232 |
|
1233 Raises: |
|
1234 ImportError exception if the module could not be loaded for whatever |
|
1235 reason (e.g., missing, not allowed). |
|
1236 """ |
|
1237 module = self._imp.new_module(submodule_fullname) |
|
1238 |
|
1239 if submodule_fullname in self._EMPTY_MODULES: |
|
1240 module.__file__ = self.EMPTY_MODULE_FILE |
|
1241 elif submodule_fullname == 'thread': |
|
1242 module.__dict__.update(self._dummy_thread.__dict__) |
|
1243 module.__name__ = 'thread' |
|
1244 elif submodule_fullname == 'cPickle': |
|
1245 module.__dict__.update(self._pickle.__dict__) |
|
1246 module.__name__ = 'cPickle' |
|
1247 elif submodule_fullname == 'os': |
|
1248 module.__dict__.update(self._os.__dict__) |
|
1249 self._module_dict['os.path'] = module.path |
|
1250 else: |
|
1251 source_file, pathname, description = self.FindModuleRestricted(submodule, submodule_fullname, search_path) |
|
1252 module = self.LoadModuleRestricted(submodule_fullname, |
|
1253 source_file, |
|
1254 pathname, |
|
1255 description) |
|
1256 |
|
1257 module.__loader__ = self |
|
1258 self.FixModule(module) |
|
1259 if submodule_fullname not in self._module_dict: |
|
1260 self._module_dict[submodule_fullname] = module |
|
1261 |
|
1262 return module |
|
1263 |
|
1264 @Trace |
|
1265 def GetParentPackage(self, fullname): |
|
1266 """Retrieves the parent package of a fully qualified module name. |
|
1267 |
|
1268 Args: |
|
1269 fullname: Full name of the module whose parent should be retrieved (e.g., |
|
1270 foo.bar). |
|
1271 |
|
1272 Returns: |
|
1273 Module instance for the parent or None if there is no parent module. |
|
1274 |
|
1275 Raise: |
|
1276 ImportError exception if the module's parent could not be found. |
|
1277 """ |
|
1278 all_modules = fullname.split('.') |
|
1279 parent_module_fullname = '.'.join(all_modules[:-1]) |
|
1280 if parent_module_fullname: |
|
1281 if self.find_module(fullname) is None: |
|
1282 raise ImportError('Could not find module %s' % fullname) |
|
1283 |
|
1284 return self._module_dict[parent_module_fullname] |
|
1285 return None |
|
1286 |
|
1287 @Trace |
|
1288 def GetParentSearchPath(self, fullname): |
|
1289 """Determines the search path of a module's parent package. |
|
1290 |
|
1291 Args: |
|
1292 fullname: Full name of the module to look up (e.g., foo.bar). |
|
1293 |
|
1294 Returns: |
|
1295 Tuple (submodule, search_path) where: |
|
1296 submodule: The last portion of the module name from fullname (e.g., |
|
1297 if fullname is foo.bar, then this is bar). |
|
1298 search_path: List of paths that belong to the parent package's search |
|
1299 path or None if there is no parent package. |
|
1300 |
|
1301 Raises: |
|
1302 ImportError exception if the module or its parent could not be found. |
|
1303 """ |
|
1304 submodule = GetSubmoduleName(fullname) |
|
1305 parent_package = self.GetParentPackage(fullname) |
|
1306 search_path = None |
|
1307 if parent_package is not None and hasattr(parent_package, '__path__'): |
|
1308 search_path = parent_package.__path__ |
|
1309 return submodule, search_path |
|
1310 |
|
1311 @Trace |
|
1312 def GetModuleInfo(self, fullname): |
|
1313 """Determines the path on disk and the search path of a module or package. |
|
1314 |
|
1315 Args: |
|
1316 fullname: Full name of the module to look up (e.g., foo.bar). |
|
1317 |
|
1318 Returns: |
|
1319 Tuple (pathname, search_path, submodule) where: |
|
1320 pathname: String containing the full path of the module on disk. |
|
1321 search_path: List of paths that belong to the found package's search |
|
1322 path or None if found module is not a package. |
|
1323 submodule: The relative name of the submodule that's being imported. |
|
1324 """ |
|
1325 submodule, search_path = self.GetParentSearchPath(fullname) |
|
1326 source_file, pathname, description = self.FindModuleRestricted(submodule, fullname, search_path) |
|
1327 suffix, mode, file_type = description |
|
1328 module_search_path = None |
|
1329 if file_type == self._imp.PKG_DIRECTORY: |
|
1330 module_search_path = [pathname] |
|
1331 pathname = os.path.join(pathname, '__init__%spy' % os.extsep) |
|
1332 return pathname, module_search_path, submodule |
|
1333 |
|
1334 @Trace |
|
1335 def load_module(self, fullname): |
|
1336 """See PEP 302.""" |
|
1337 all_modules = fullname.split('.') |
|
1338 submodule = all_modules[-1] |
|
1339 parent_module_fullname = '.'.join(all_modules[:-1]) |
|
1340 search_path = None |
|
1341 if parent_module_fullname and parent_module_fullname in self._module_dict: |
|
1342 parent_module = self._module_dict[parent_module_fullname] |
|
1343 if hasattr(parent_module, '__path__'): |
|
1344 search_path = parent_module.__path__ |
|
1345 |
|
1346 return self.FindAndLoadModule(submodule, fullname, search_path) |
|
1347 |
|
1348 @Trace |
|
1349 def is_package(self, fullname): |
|
1350 """See PEP 302 extensions.""" |
|
1351 submodule, search_path = self.GetParentSearchPath(fullname) |
|
1352 source_file, pathname, description = self.FindModuleRestricted(submodule, fullname, search_path) |
|
1353 suffix, mode, file_type = description |
|
1354 if file_type == self._imp.PKG_DIRECTORY: |
|
1355 return True |
|
1356 return False |
|
1357 |
|
1358 @Trace |
|
1359 def get_source(self, fullname): |
|
1360 """See PEP 302 extensions.""" |
|
1361 full_path, search_path, submodule = self.GetModuleInfo(fullname) |
|
1362 source_file = open(full_path) |
|
1363 try: |
|
1364 return source_file.read() |
|
1365 finally: |
|
1366 source_file.close() |
|
1367 |
|
1368 @Trace |
|
1369 def get_code(self, fullname): |
|
1370 """See PEP 302 extensions.""" |
|
1371 full_path, search_path, submodule = self.GetModuleInfo(fullname) |
|
1372 source_file = open(full_path) |
|
1373 try: |
|
1374 source_code = source_file.read() |
|
1375 finally: |
|
1376 source_file.close() |
|
1377 |
|
1378 source_code = source_code.replace('\r\n', '\n') |
|
1379 if not source_code.endswith('\n'): |
|
1380 source_code += '\n' |
|
1381 |
|
1382 return compile(source_code, full_path, 'exec') |
|
1383 |
|
1384 |
|
1385 def ModuleHasValidMainFunction(module): |
|
1386 """Determines if a module has a main function that takes no arguments. |
|
1387 |
|
1388 This includes functions that have arguments with defaults that are all |
|
1389 assigned, thus requiring no additional arguments in order to be called. |
|
1390 |
|
1391 Args: |
|
1392 module: A types.ModuleType instance. |
|
1393 |
|
1394 Returns: |
|
1395 True if the module has a valid, reusable main function; False otherwise. |
|
1396 """ |
|
1397 if hasattr(module, 'main') and type(module.main) is types.FunctionType: |
|
1398 arg_names, var_args, var_kwargs, default_values = inspect.getargspec(module.main) |
|
1399 if len(arg_names) == 0: |
|
1400 return True |
|
1401 if default_values is not None and len(arg_names) == len(default_values): |
|
1402 return True |
|
1403 return False |
|
1404 |
|
1405 |
|
1406 def GetScriptModuleName(handler_path): |
|
1407 """Determines the fully-qualified Python module name of a script on disk. |
|
1408 |
|
1409 Args: |
|
1410 handler_path: CGI path stored in the application configuration (as a path |
|
1411 like 'foo/bar/baz.py'). May contain $PYTHON_LIB references. |
|
1412 |
|
1413 Returns: |
|
1414 String containing the corresponding module name (e.g., 'foo.bar.baz'). |
|
1415 """ |
|
1416 if handler_path.startswith(PYTHON_LIB_VAR + '/'): |
|
1417 handler_path = handler_path[len(PYTHON_LIB_VAR):] |
|
1418 handler_path = os.path.normpath(handler_path) |
|
1419 |
|
1420 extension_index = handler_path.rfind('.py') |
|
1421 if extension_index != -1: |
|
1422 handler_path = handler_path[:extension_index] |
|
1423 module_fullname = handler_path.replace(os.sep, '.') |
|
1424 module_fullname = module_fullname.strip('.') |
|
1425 module_fullname = re.sub('\.+', '.', module_fullname) |
|
1426 |
|
1427 if module_fullname.endswith('.__init__'): |
|
1428 module_fullname = module_fullname[:-len('.__init__')] |
|
1429 |
|
1430 return module_fullname |
|
1431 |
|
1432 |
|
1433 def FindMissingInitFiles(cgi_path, module_fullname, isfile=os.path.isfile): |
|
1434 """Determines which __init__.py files are missing from a module's parent |
|
1435 packages. |
|
1436 |
|
1437 Args: |
|
1438 cgi_path: Absolute path of the CGI module file on disk. |
|
1439 module_fullname: Fully qualified Python module name used to import the |
|
1440 cgi_path module. |
|
1441 |
|
1442 Returns: |
|
1443 List containing the paths to the missing __init__.py files. |
|
1444 """ |
|
1445 missing_init_files = [] |
|
1446 |
|
1447 if cgi_path.endswith('.py'): |
|
1448 module_base = os.path.dirname(cgi_path) |
|
1449 else: |
|
1450 module_base = cgi_path |
|
1451 |
|
1452 depth_count = module_fullname.count('.') |
|
1453 if cgi_path.endswith('__init__.py') or not cgi_path.endswith('.py'): |
|
1454 depth_count += 1 |
|
1455 |
|
1456 for index in xrange(depth_count): |
|
1457 current_init_file = os.path.join(module_base, '__init__.py') |
|
1458 |
|
1459 if not isfile(current_init_file): |
|
1460 missing_init_files.append(current_init_file) |
|
1461 |
|
1462 module_base = os.path.abspath(os.path.join(module_base, os.pardir)) |
|
1463 |
|
1464 return missing_init_files |
|
1465 |
|
1466 |
|
1467 def LoadTargetModule(handler_path, |
|
1468 cgi_path, |
|
1469 import_hook, |
|
1470 module_dict=sys.modules): |
|
1471 """Loads a target CGI script by importing it as a Python module. |
|
1472 |
|
1473 If the module for the target CGI script has already been loaded before, |
|
1474 the new module will be loaded in its place using the same module object, |
|
1475 possibly overwriting existing module attributes. |
|
1476 |
|
1477 Args: |
|
1478 handler_path: CGI path stored in the application configuration (as a path |
|
1479 like 'foo/bar/baz.py'). Should not have $PYTHON_LIB references. |
|
1480 cgi_path: Absolute path to the CGI script file on disk. |
|
1481 import_hook: Instance of HardenedModulesHook to use for module loading. |
|
1482 module_dict: Used for dependency injection. |
|
1483 |
|
1484 Returns: |
|
1485 Tuple (module_fullname, script_module, module_code) where: |
|
1486 module_fullname: Fully qualified module name used to import the script. |
|
1487 script_module: The ModuleType object corresponding to the module_fullname. |
|
1488 If the module has not already been loaded, this will be an empty |
|
1489 shell of a module. |
|
1490 module_code: Code object (returned by compile built-in) corresponding |
|
1491 to the cgi_path to run. If the script_module was previously loaded |
|
1492 and has a main() function that can be reused, this will be None. |
|
1493 """ |
|
1494 module_fullname = GetScriptModuleName(handler_path) |
|
1495 script_module = module_dict.get(module_fullname) |
|
1496 module_code = None |
|
1497 if script_module != None and ModuleHasValidMainFunction(script_module): |
|
1498 logging.debug('Reusing main() function of module "%s"', module_fullname) |
|
1499 else: |
|
1500 if script_module is None: |
|
1501 script_module = imp.new_module(module_fullname) |
|
1502 script_module.__loader__ = import_hook |
|
1503 |
|
1504 try: |
|
1505 module_code = import_hook.get_code(module_fullname) |
|
1506 full_path, search_path, submodule = import_hook.GetModuleInfo(module_fullname) |
|
1507 script_module.__file__ = full_path |
|
1508 if search_path is not None: |
|
1509 script_module.__path__ = search_path |
|
1510 except: |
|
1511 exc_type, exc_value, exc_tb = sys.exc_info() |
|
1512 import_error_message = str(exc_type) |
|
1513 if exc_value: |
|
1514 import_error_message += ': ' + str(exc_value) |
|
1515 |
|
1516 logging.error('Encountered error loading module "%s": %s', |
|
1517 module_fullname, import_error_message) |
|
1518 missing_inits = FindMissingInitFiles(cgi_path, module_fullname) |
|
1519 if missing_inits: |
|
1520 logging.warning('Missing package initialization files: %s', |
|
1521 ', '.join(missing_inits)) |
|
1522 else: |
|
1523 logging.error('Parent package initialization files are present, ' |
|
1524 'but must be broken') |
|
1525 |
|
1526 independent_load_successful = True |
|
1527 |
|
1528 if not os.path.isfile(cgi_path): |
|
1529 independent_load_successful = False |
|
1530 else: |
|
1531 try: |
|
1532 source_file = open(cgi_path) |
|
1533 try: |
|
1534 module_code = compile(source_file.read(), cgi_path, 'exec') |
|
1535 script_module.__file__ = cgi_path |
|
1536 finally: |
|
1537 source_file.close() |
|
1538 |
|
1539 except OSError: |
|
1540 independent_load_successful = False |
|
1541 |
|
1542 if not independent_load_successful: |
|
1543 raise exc_type, exc_value, exc_tb |
|
1544 |
|
1545 module_dict[module_fullname] = script_module |
|
1546 |
|
1547 return module_fullname, script_module, module_code |
|
1548 |
|
1549 |
|
1550 def ExecuteOrImportScript(handler_path, cgi_path, import_hook): |
|
1551 """Executes a CGI script by importing it as a new module; possibly reuses |
|
1552 the module's main() function if it is defined and takes no arguments. |
|
1553 |
|
1554 Basic technique lifted from PEP 338 and Python2.5's runpy module. See: |
|
1555 http://www.python.org/dev/peps/pep-0338/ |
|
1556 |
|
1557 See the section entitled "Import Statements and the Main Module" to understand |
|
1558 why a module named '__main__' cannot do relative imports. To get around this, |
|
1559 the requested module's path could be added to sys.path on each request. |
|
1560 |
|
1561 Args: |
|
1562 handler_path: CGI path stored in the application configuration (as a path |
|
1563 like 'foo/bar/baz.py'). Should not have $PYTHON_LIB references. |
|
1564 cgi_path: Absolute path to the CGI script file on disk. |
|
1565 import_hook: Instance of HardenedModulesHook to use for module loading. |
|
1566 |
|
1567 Returns: |
|
1568 True if the response code had an error status (e.g., 404), or False if it |
|
1569 did not. |
|
1570 |
|
1571 Raises: |
|
1572 Any kind of exception that could have been raised when loading the target |
|
1573 module, running a target script, or executing the application code itself. |
|
1574 """ |
|
1575 module_fullname, script_module, module_code = LoadTargetModule( |
|
1576 handler_path, cgi_path, import_hook) |
|
1577 script_module.__name__ = '__main__' |
|
1578 sys.modules['__main__'] = script_module |
|
1579 try: |
|
1580 if module_code: |
|
1581 exec module_code in script_module.__dict__ |
|
1582 else: |
|
1583 script_module.main() |
|
1584 |
|
1585 sys.stdout.flush() |
|
1586 sys.stdout.seek(0) |
|
1587 try: |
|
1588 headers = mimetools.Message(sys.stdout) |
|
1589 finally: |
|
1590 sys.stdout.seek(0, 2) |
|
1591 status_header = headers.get('status') |
|
1592 error_response = False |
|
1593 if status_header: |
|
1594 try: |
|
1595 status_code = int(status_header.split(' ', 1)[0]) |
|
1596 error_response = status_code >= 400 |
|
1597 except ValueError: |
|
1598 error_response = True |
|
1599 |
|
1600 if not error_response: |
|
1601 try: |
|
1602 parent_package = import_hook.GetParentPackage(module_fullname) |
|
1603 except Exception: |
|
1604 parent_package = None |
|
1605 |
|
1606 if parent_package is not None: |
|
1607 submodule = GetSubmoduleName(module_fullname) |
|
1608 setattr(parent_package, submodule, script_module) |
|
1609 |
|
1610 return error_response |
|
1611 finally: |
|
1612 script_module.__name__ = module_fullname |
|
1613 |
|
1614 |
|
1615 def ExecuteCGI(root_path, |
|
1616 handler_path, |
|
1617 cgi_path, |
|
1618 env, |
|
1619 infile, |
|
1620 outfile, |
|
1621 module_dict, |
|
1622 exec_script=ExecuteOrImportScript): |
|
1623 """Executes Python file in this process as if it were a CGI. |
|
1624 |
|
1625 Does not return an HTTP response line. CGIs should output headers followed by |
|
1626 the body content. |
|
1627 |
|
1628 The modules in sys.modules should be the same before and after the CGI is |
|
1629 executed, with the specific exception of encodings-related modules, which |
|
1630 cannot be reloaded and thus must always stay in sys.modules. |
|
1631 |
|
1632 Args: |
|
1633 root_path: Path to the root of the application. |
|
1634 handler_path: CGI path stored in the application configuration (as a path |
|
1635 like 'foo/bar/baz.py'). May contain $PYTHON_LIB references. |
|
1636 cgi_path: Absolute path to the CGI script file on disk. |
|
1637 env: Dictionary of environment variables to use for the execution. |
|
1638 infile: File-like object to read HTTP request input data from. |
|
1639 outfile: FIle-like object to write HTTP response data to. |
|
1640 module_dict: Dictionary in which application-loaded modules should be |
|
1641 preserved between requests. This removes the need to reload modules that |
|
1642 are reused between requests, significantly increasing load performance. |
|
1643 This dictionary must be separate from the sys.modules dictionary. |
|
1644 exec_script: Used for dependency injection. |
|
1645 """ |
|
1646 old_module_dict = sys.modules.copy() |
|
1647 old_builtin = __builtin__.__dict__.copy() |
|
1648 old_argv = sys.argv |
|
1649 old_stdin = sys.stdin |
|
1650 old_stdout = sys.stdout |
|
1651 old_env = os.environ.copy() |
|
1652 old_cwd = os.getcwd() |
|
1653 old_file_type = types.FileType |
|
1654 reset_modules = False |
|
1655 |
|
1656 try: |
|
1657 ClearAllButEncodingsModules(sys.modules) |
|
1658 sys.modules.update(module_dict) |
|
1659 sys.argv = [cgi_path] |
|
1660 sys.stdin = infile |
|
1661 sys.stdout = outfile |
|
1662 os.environ.clear() |
|
1663 os.environ.update(env) |
|
1664 before_path = sys.path[:] |
|
1665 os.chdir(os.path.dirname(cgi_path)) |
|
1666 |
|
1667 hook = HardenedModulesHook(sys.modules) |
|
1668 sys.meta_path = [hook] |
|
1669 if hasattr(sys, 'path_importer_cache'): |
|
1670 sys.path_importer_cache.clear() |
|
1671 |
|
1672 __builtin__.file = FakeFile |
|
1673 __builtin__.open = FakeFile |
|
1674 types.FileType = FakeFile |
|
1675 |
|
1676 __builtin__.buffer = NotImplementedFake |
|
1677 |
|
1678 logging.debug('Executing CGI with env:\n%s', pprint.pformat(env)) |
|
1679 try: |
|
1680 reset_modules = exec_script(handler_path, cgi_path, hook) |
|
1681 except SystemExit, e: |
|
1682 logging.debug('CGI exited with status: %s', e) |
|
1683 except: |
|
1684 reset_modules = True |
|
1685 raise |
|
1686 |
|
1687 finally: |
|
1688 sys.meta_path = [] |
|
1689 sys.path_importer_cache.clear() |
|
1690 |
|
1691 _ClearTemplateCache(sys.modules) |
|
1692 |
|
1693 module_dict.update(sys.modules) |
|
1694 ClearAllButEncodingsModules(sys.modules) |
|
1695 sys.modules.update(old_module_dict) |
|
1696 |
|
1697 __builtin__.__dict__.update(old_builtin) |
|
1698 sys.argv = old_argv |
|
1699 sys.stdin = old_stdin |
|
1700 sys.stdout = old_stdout |
|
1701 |
|
1702 sys.path[:] = before_path |
|
1703 |
|
1704 os.environ.clear() |
|
1705 os.environ.update(old_env) |
|
1706 os.chdir(old_cwd) |
|
1707 |
|
1708 types.FileType = old_file_type |
|
1709 |
|
1710 |
|
1711 class CGIDispatcher(URLDispatcher): |
|
1712 """Dispatcher that executes Python CGI scripts.""" |
|
1713 |
|
1714 def __init__(self, |
|
1715 module_dict, |
|
1716 root_path, |
|
1717 path_adjuster, |
|
1718 setup_env=SetupEnvironment, |
|
1719 exec_cgi=ExecuteCGI, |
|
1720 create_logging_handler=ApplicationLoggingHandler): |
|
1721 """Initializer. |
|
1722 |
|
1723 Args: |
|
1724 module_dict: Dictionary in which application-loaded modules should be |
|
1725 preserved between requests. This dictionary must be separate from the |
|
1726 sys.modules dictionary. |
|
1727 path_adjuster: Instance of PathAdjuster to use for finding absolute |
|
1728 paths of CGI files on disk. |
|
1729 setup_env, exec_cgi, create_logging_handler: Used for dependency |
|
1730 injection. |
|
1731 """ |
|
1732 self._module_dict = module_dict |
|
1733 self._root_path = root_path |
|
1734 self._path_adjuster = path_adjuster |
|
1735 self._setup_env = setup_env |
|
1736 self._exec_cgi = exec_cgi |
|
1737 self._create_logging_handler = create_logging_handler |
|
1738 |
|
1739 def Dispatch(self, |
|
1740 relative_url, |
|
1741 path, |
|
1742 headers, |
|
1743 infile, |
|
1744 outfile, |
|
1745 base_env_dict=None): |
|
1746 """Dispatches the Python CGI.""" |
|
1747 handler = self._create_logging_handler() |
|
1748 logging.getLogger().addHandler(handler) |
|
1749 before_level = logging.root.level |
|
1750 try: |
|
1751 env = {} |
|
1752 if base_env_dict: |
|
1753 env.update(base_env_dict) |
|
1754 cgi_path = self._path_adjuster.AdjustPath(path) |
|
1755 env.update(self._setup_env(cgi_path, relative_url, headers)) |
|
1756 self._exec_cgi(self._root_path, |
|
1757 path, |
|
1758 cgi_path, |
|
1759 env, |
|
1760 infile, |
|
1761 outfile, |
|
1762 self._module_dict) |
|
1763 handler.AddDebuggingConsole(relative_url, env, outfile) |
|
1764 finally: |
|
1765 logging.root.level = before_level |
|
1766 logging.getLogger().removeHandler(handler) |
|
1767 |
|
1768 def __str__(self): |
|
1769 """Returns a string representation of this dispatcher.""" |
|
1770 return 'CGI dispatcher' |
|
1771 |
|
1772 |
|
1773 class LocalCGIDispatcher(CGIDispatcher): |
|
1774 """Dispatcher that executes local functions like they're CGIs. |
|
1775 |
|
1776 The contents of sys.modules will be preserved for local CGIs running this |
|
1777 dispatcher, but module hardening will still occur for any new imports. Thus, |
|
1778 be sure that any local CGIs have loaded all of their dependent modules |
|
1779 _before_ they are executed. |
|
1780 """ |
|
1781 |
|
1782 def __init__(self, module_dict, path_adjuster, cgi_func): |
|
1783 """Initializer. |
|
1784 |
|
1785 Args: |
|
1786 module_dict: Passed to CGIDispatcher. |
|
1787 path_adjuster: Passed to CGIDispatcher. |
|
1788 cgi_func: Callable function taking no parameters that should be |
|
1789 executed in a CGI environment in the current process. |
|
1790 """ |
|
1791 self._cgi_func = cgi_func |
|
1792 |
|
1793 def curried_exec_script(*args, **kwargs): |
|
1794 cgi_func() |
|
1795 return False |
|
1796 |
|
1797 def curried_exec_cgi(*args, **kwargs): |
|
1798 kwargs['exec_script'] = curried_exec_script |
|
1799 return ExecuteCGI(*args, **kwargs) |
|
1800 |
|
1801 CGIDispatcher.__init__(self, |
|
1802 module_dict, |
|
1803 '', |
|
1804 path_adjuster, |
|
1805 exec_cgi=curried_exec_cgi) |
|
1806 |
|
1807 def Dispatch(self, *args, **kwargs): |
|
1808 """Preserves sys.modules for CGIDispatcher.Dispatch.""" |
|
1809 self._module_dict.update(sys.modules) |
|
1810 CGIDispatcher.Dispatch(self, *args, **kwargs) |
|
1811 |
|
1812 def __str__(self): |
|
1813 """Returns a string representation of this dispatcher.""" |
|
1814 return 'Local CGI dispatcher for %s' % self._cgi_func |
|
1815 |
|
1816 |
|
1817 class PathAdjuster(object): |
|
1818 """Adjusts application file paths to paths relative to the application or |
|
1819 external library directories.""" |
|
1820 |
|
1821 def __init__(self, root_path): |
|
1822 """Initializer. |
|
1823 |
|
1824 Args: |
|
1825 root_path: Path to the root of the application running on the server. |
|
1826 """ |
|
1827 self._root_path = os.path.abspath(root_path) |
|
1828 |
|
1829 def AdjustPath(self, path): |
|
1830 """Adjusts application file path to paths relative to the application or |
|
1831 external library directories. |
|
1832 |
|
1833 Handler paths that start with $PYTHON_LIB will be converted to paths |
|
1834 relative to the google directory. |
|
1835 |
|
1836 Args: |
|
1837 path: File path that should be adjusted. |
|
1838 |
|
1839 Returns: |
|
1840 The adjusted path. |
|
1841 """ |
|
1842 if path.startswith(PYTHON_LIB_VAR): |
|
1843 path = os.path.join(os.path.dirname(os.path.dirname(google.__file__)), |
|
1844 path[len(PYTHON_LIB_VAR) + 1:]) |
|
1845 else: |
|
1846 path = os.path.join(self._root_path, path) |
|
1847 |
|
1848 return path |
|
1849 |
|
1850 |
|
1851 class StaticFileMimeTypeMatcher(object): |
|
1852 """Computes mime type based on URLMap and file extension. |
|
1853 |
|
1854 To determine the mime type, we first see if there is any mime-type property |
|
1855 on each URLMap entry. If non is specified, we use the mimetypes module to |
|
1856 guess the mime type from the file path extension, and use |
|
1857 application/octet-stream if we can't find the mimetype. |
|
1858 """ |
|
1859 |
|
1860 def __init__(self, |
|
1861 url_map_list, |
|
1862 path_adjuster): |
|
1863 """Initializer. |
|
1864 |
|
1865 Args: |
|
1866 url_map_list: List of appinfo.URLMap objects. |
|
1867 If empty or None, then we always use the mime type chosen by the |
|
1868 mimetypes module. |
|
1869 path_adjuster: PathAdjuster object used to adjust application file paths. |
|
1870 """ |
|
1871 self._patterns = [] |
|
1872 |
|
1873 if url_map_list: |
|
1874 for entry in url_map_list: |
|
1875 if entry.mime_type is None: |
|
1876 continue |
|
1877 handler_type = entry.GetHandlerType() |
|
1878 if handler_type not in (appinfo.STATIC_FILES, appinfo.STATIC_DIR): |
|
1879 continue |
|
1880 |
|
1881 if handler_type == appinfo.STATIC_FILES: |
|
1882 regex = entry.upload |
|
1883 else: |
|
1884 static_dir = entry.static_dir |
|
1885 if static_dir[-1] == '/': |
|
1886 static_dir = static_dir[:-1] |
|
1887 regex = '/'.join((entry.static_dir, r'(.*)')) |
|
1888 |
|
1889 adjusted_regex = r'^%s$' % path_adjuster.AdjustPath(regex) |
|
1890 try: |
|
1891 path_re = re.compile(adjusted_regex) |
|
1892 except re.error, e: |
|
1893 raise InvalidAppConfigError('regex does not compile: %s' % e) |
|
1894 |
|
1895 self._patterns.append((path_re, entry.mime_type)) |
|
1896 |
|
1897 def GetMimeType(self, path): |
|
1898 """Returns the mime type that we should use when serving the specified file. |
|
1899 |
|
1900 Args: |
|
1901 path: String containing the file's path on disk. |
|
1902 |
|
1903 Returns: |
|
1904 String containing the mime type to use. Will be 'application/octet-stream' |
|
1905 if we have no idea what it should be. |
|
1906 """ |
|
1907 for (path_re, mime_type) in self._patterns: |
|
1908 the_match = path_re.match(path) |
|
1909 if the_match: |
|
1910 return mime_type |
|
1911 |
|
1912 filename, extension = os.path.splitext(path) |
|
1913 return mimetypes.types_map.get(extension, 'application/octet-stream') |
|
1914 |
|
1915 |
|
1916 def ReadDataFile(data_path, openfile=file): |
|
1917 """Reads a file on disk, returning a corresponding HTTP status and data. |
|
1918 |
|
1919 Args: |
|
1920 data_path: Path to the file on disk to read. |
|
1921 openfile: Used for dependency injection. |
|
1922 |
|
1923 Returns: |
|
1924 Tuple (status, data) where status is an HTTP response code, and data is |
|
1925 the data read; will be an empty string if an error occurred or the |
|
1926 file was empty. |
|
1927 """ |
|
1928 status = httplib.INTERNAL_SERVER_ERROR |
|
1929 data = "" |
|
1930 |
|
1931 try: |
|
1932 data_file = openfile(data_path, 'rb') |
|
1933 try: |
|
1934 data = data_file.read() |
|
1935 finally: |
|
1936 data_file.close() |
|
1937 status = httplib.OK |
|
1938 except (OSError, IOError), e: |
|
1939 logging.error('Error encountered reading file "%s":\n%s', data_path, e) |
|
1940 if e.errno in FILE_MISSING_EXCEPTIONS: |
|
1941 status = httplib.NOT_FOUND |
|
1942 else: |
|
1943 status = httplib.FORBIDDEN |
|
1944 |
|
1945 return status, data |
|
1946 |
|
1947 |
|
1948 class FileDispatcher(URLDispatcher): |
|
1949 """Dispatcher that reads data files from disk.""" |
|
1950 |
|
1951 def __init__(self, |
|
1952 path_adjuster, |
|
1953 static_file_mime_type_matcher, |
|
1954 read_data_file=ReadDataFile): |
|
1955 """Initializer. |
|
1956 |
|
1957 Args: |
|
1958 path_adjuster: Instance of PathAdjuster to use for finding absolute |
|
1959 paths of data files on disk. |
|
1960 static_file_mime_type_matcher: StaticFileMimeTypeMatcher object. |
|
1961 read_data_file: Used for dependency injection. |
|
1962 """ |
|
1963 self._path_adjuster = path_adjuster |
|
1964 self._static_file_mime_type_matcher = static_file_mime_type_matcher |
|
1965 self._read_data_file = read_data_file |
|
1966 |
|
1967 def Dispatch(self, |
|
1968 relative_url, |
|
1969 path, |
|
1970 headers, |
|
1971 infile, |
|
1972 outfile, |
|
1973 base_env_dict=None): |
|
1974 """Reads the file and returns the response status and data.""" |
|
1975 full_path = self._path_adjuster.AdjustPath(path) |
|
1976 status, data = self._read_data_file(full_path) |
|
1977 content_type = self._static_file_mime_type_matcher.GetMimeType(full_path) |
|
1978 |
|
1979 outfile.write('Status: %d\r\n' % status) |
|
1980 outfile.write('Content-type: %s\r\n' % content_type) |
|
1981 outfile.write('\r\n') |
|
1982 outfile.write(data) |
|
1983 |
|
1984 def __str__(self): |
|
1985 """Returns a string representation of this dispatcher.""" |
|
1986 return 'File dispatcher' |
|
1987 |
|
1988 |
|
1989 def RewriteResponse(response_file): |
|
1990 """Interprets server-side headers and adjusts the HTTP response accordingly. |
|
1991 |
|
1992 Handles the server-side 'status' header, which instructs the server to change |
|
1993 the HTTP response code accordingly. Handles the 'location' header, which |
|
1994 issues an HTTP 302 redirect to the client. Also corrects the 'content-length' |
|
1995 header to reflect actual content length in case extra information has been |
|
1996 appended to the response body. |
|
1997 |
|
1998 If the 'status' header supplied by the client is invalid, this method will |
|
1999 set the response to a 500 with an error message as content. |
|
2000 |
|
2001 Args: |
|
2002 response_file: File-like object containing the full HTTP response including |
|
2003 the response code, all headers, and the request body. |
|
2004 |
|
2005 Returns: |
|
2006 Tuple (status_code, status_message, header, body) where: |
|
2007 status_code: Integer HTTP response status (e.g., 200, 302, 404, 500) |
|
2008 status_message: String containing an informational message about the |
|
2009 response code, possibly derived from the 'status' header, if supplied. |
|
2010 header: String containing the HTTP headers of the response, without |
|
2011 a trailing new-line (CRLF). |
|
2012 body: String containing the body of the response. |
|
2013 """ |
|
2014 headers = mimetools.Message(response_file) |
|
2015 |
|
2016 response_status = '%d Good to go' % httplib.OK |
|
2017 |
|
2018 location_value = headers.getheader('location') |
|
2019 status_value = headers.getheader('status') |
|
2020 if status_value: |
|
2021 response_status = status_value |
|
2022 del headers['status'] |
|
2023 elif location_value: |
|
2024 response_status = '%d Redirecting' % httplib.FOUND |
|
2025 |
|
2026 if not 'Cache-Control' in headers: |
|
2027 headers['Cache-Control'] = 'no-cache' |
|
2028 |
|
2029 status_parts = response_status.split(' ', 1) |
|
2030 status_code, status_message = (status_parts + [''])[:2] |
|
2031 try: |
|
2032 status_code = int(status_code) |
|
2033 except ValueError: |
|
2034 status_code = 500 |
|
2035 body = 'Error: Invalid "status" header value returned.' |
|
2036 else: |
|
2037 body = response_file.read() |
|
2038 |
|
2039 headers['content-length'] = str(len(body)) |
|
2040 |
|
2041 header_list = [] |
|
2042 for header in headers.headers: |
|
2043 header = header.rstrip('\n') |
|
2044 header = header.rstrip('\r') |
|
2045 header_list.append(header) |
|
2046 |
|
2047 header_data = '\r\n'.join(header_list) + '\r\n' |
|
2048 return status_code, status_message, header_data, body |
|
2049 |
|
2050 |
|
2051 class ModuleManager(object): |
|
2052 """Manages loaded modules in the runtime. |
|
2053 |
|
2054 Responsible for monitoring and reporting about file modification times. |
|
2055 Modules can be loaded from source or precompiled byte-code files. When a |
|
2056 file has source code, the ModuleManager monitors the modification time of |
|
2057 the source file even if the module itself is loaded from byte-code. |
|
2058 """ |
|
2059 |
|
2060 def __init__(self, modules): |
|
2061 """Initializer. |
|
2062 |
|
2063 Args: |
|
2064 modules: Dictionary containing monitored modules. |
|
2065 """ |
|
2066 self._modules = modules |
|
2067 self._default_modules = self._modules.copy() |
|
2068 |
|
2069 self._modification_times = {} |
|
2070 |
|
2071 @staticmethod |
|
2072 def GetModuleFile(module, is_file=os.path.isfile): |
|
2073 """Helper method to try to determine modules source file. |
|
2074 |
|
2075 Args: |
|
2076 module: Module object to get file for. |
|
2077 is_file: Function used to determine if a given path is a file. |
|
2078 |
|
2079 Returns: |
|
2080 Path of the module's corresponding Python source file if it exists, or |
|
2081 just the module's compiled Python file. If the module has an invalid |
|
2082 __file__ attribute, None will be returned. |
|
2083 """ |
|
2084 module_file = getattr(module, '__file__', None) |
|
2085 if not module_file or module_file == HardenedModulesHook.EMPTY_MODULE_FILE: |
|
2086 return None |
|
2087 |
|
2088 source_file = module_file[:module_file.rfind('py') + 2] |
|
2089 |
|
2090 if is_file(source_file): |
|
2091 return source_file |
|
2092 return module.__file__ |
|
2093 |
|
2094 def AreModuleFilesModified(self): |
|
2095 """Determines if any monitored files have been modified. |
|
2096 |
|
2097 Returns: |
|
2098 True if one or more files have been modified, False otherwise. |
|
2099 """ |
|
2100 for name, (mtime, fname) in self._modification_times.iteritems(): |
|
2101 if name not in self._modules: |
|
2102 continue |
|
2103 |
|
2104 module = self._modules[name] |
|
2105 |
|
2106 if not os.path.isfile(fname): |
|
2107 return True |
|
2108 |
|
2109 if mtime != os.path.getmtime(fname): |
|
2110 return True |
|
2111 |
|
2112 return False |
|
2113 |
|
2114 def UpdateModuleFileModificationTimes(self): |
|
2115 """Records the current modification times of all monitored modules. |
|
2116 """ |
|
2117 self._modification_times.clear() |
|
2118 for name, module in self._modules.items(): |
|
2119 if not isinstance(module, types.ModuleType): |
|
2120 continue |
|
2121 module_file = self.GetModuleFile(module) |
|
2122 if not module_file: |
|
2123 continue |
|
2124 try: |
|
2125 self._modification_times[name] = (os.path.getmtime(module_file), |
|
2126 module_file) |
|
2127 except OSError, e: |
|
2128 if e.errno not in FILE_MISSING_EXCEPTIONS: |
|
2129 raise e |
|
2130 |
|
2131 def ResetModules(self): |
|
2132 """Clear modules so that when request is run they are reloaded.""" |
|
2133 self._modules.clear() |
|
2134 self._modules.update(self._default_modules) |
|
2135 |
|
2136 |
|
2137 def _ClearTemplateCache(module_dict=sys.modules): |
|
2138 """Clear template cache in webapp.template module. |
|
2139 |
|
2140 Attempts to load template module. Ignores failure. If module loads, the |
|
2141 template cache is cleared. |
|
2142 """ |
|
2143 template_module = module_dict.get('google.appengine.ext.webapp.template') |
|
2144 if template_module is not None: |
|
2145 template_module.template_cache.clear() |
|
2146 |
|
2147 |
|
2148 def CreateRequestHandler(root_path, login_url, require_indexes=False): |
|
2149 """Creates a new BaseHTTPRequestHandler sub-class for use with the Python |
|
2150 BaseHTTPServer module's HTTP server. |
|
2151 |
|
2152 Python's built-in HTTP server does not support passing context information |
|
2153 along to instances of its request handlers. This function gets around that |
|
2154 by creating a sub-class of the handler in a closure that has access to |
|
2155 this context information. |
|
2156 |
|
2157 Args: |
|
2158 root_path: Path to the root of the application running on the server. |
|
2159 login_url: Relative URL which should be used for handling user logins. |
|
2160 require_indexes: True if index.yaml is read-only gospel; default False. |
|
2161 |
|
2162 Returns: |
|
2163 Sub-class of BaseHTTPRequestHandler. |
|
2164 """ |
|
2165 application_module_dict = SetupSharedModules(sys.modules) |
|
2166 |
|
2167 if require_indexes: |
|
2168 index_yaml_updater = None |
|
2169 else: |
|
2170 index_yaml_updater = dev_appserver_index.IndexYamlUpdater(root_path) |
|
2171 |
|
2172 class DevAppServerRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler): |
|
2173 """Dispatches URLs using patterns from a URLMatcher, which is created by |
|
2174 loading an application's configuration file. Executes CGI scripts in the |
|
2175 local process so the scripts can use mock versions of APIs. |
|
2176 |
|
2177 HTTP requests that correctly specify a user info cookie |
|
2178 (dev_appserver_login.COOKIE_NAME) will have the 'USER_EMAIL' environment |
|
2179 variable set accordingly. If the user is also an admin, the |
|
2180 'USER_IS_ADMIN' variable will exist and be set to '1'. If the user is not |
|
2181 logged in, 'USER_EMAIL' will be set to the empty string. |
|
2182 |
|
2183 On each request, raises an InvalidAppConfigError exception if the |
|
2184 application configuration file in the directory specified by the root_path |
|
2185 argument is invalid. |
|
2186 """ |
|
2187 server_version = 'Development/1.0' |
|
2188 |
|
2189 module_dict = application_module_dict |
|
2190 module_manager = ModuleManager(application_module_dict) |
|
2191 |
|
2192 def __init__(self, *args, **kwargs): |
|
2193 """Initializer. |
|
2194 |
|
2195 Args: |
|
2196 args, kwargs: Positional and keyword arguments passed to the constructor |
|
2197 of the super class. |
|
2198 """ |
|
2199 BaseHTTPServer.BaseHTTPRequestHandler.__init__(self, *args, **kwargs) |
|
2200 |
|
2201 def do_GET(self): |
|
2202 """Handle GET requests.""" |
|
2203 self._HandleRequest() |
|
2204 |
|
2205 def do_POST(self): |
|
2206 """Handles POST requests.""" |
|
2207 self._HandleRequest() |
|
2208 |
|
2209 def do_PUT(self): |
|
2210 """Handle PUT requests.""" |
|
2211 self._HandleRequest() |
|
2212 |
|
2213 def do_HEAD(self): |
|
2214 """Handle HEAD requests.""" |
|
2215 self._HandleRequest() |
|
2216 |
|
2217 def do_OPTIONS(self): |
|
2218 """Handles OPTIONS requests.""" |
|
2219 self._HandleRequest() |
|
2220 |
|
2221 def do_DELETE(self): |
|
2222 """Handle DELETE requests.""" |
|
2223 self._HandleRequest() |
|
2224 |
|
2225 def do_TRACE(self): |
|
2226 """Handles TRACE requests.""" |
|
2227 self._HandleRequest() |
|
2228 |
|
2229 def _HandleRequest(self): |
|
2230 """Handles any type of request and prints exceptions if they occur.""" |
|
2231 server_name = self.headers.get('host') or self.server.server_name |
|
2232 server_name = server_name.split(':', 1)[0] |
|
2233 |
|
2234 env_dict = { |
|
2235 'REQUEST_METHOD': self.command, |
|
2236 'REMOTE_ADDR': self.client_address[0], |
|
2237 'SERVER_SOFTWARE': self.server_version, |
|
2238 'SERVER_NAME': server_name, |
|
2239 'SERVER_PROTOCOL': self.protocol_version, |
|
2240 'SERVER_PORT': str(self.server.server_port), |
|
2241 } |
|
2242 |
|
2243 full_url = GetFullURL(server_name, self.server.server_port, self.path) |
|
2244 if len(full_url) > MAX_URL_LENGTH: |
|
2245 msg = 'Requested URI too long: %s' % full_url |
|
2246 logging.error(msg) |
|
2247 self.send_response(httplib.REQUEST_URI_TOO_LONG, msg) |
|
2248 return |
|
2249 |
|
2250 tbhandler = cgitb.Hook(file=self.wfile).handle |
|
2251 try: |
|
2252 if self.module_manager.AreModuleFilesModified(): |
|
2253 self.module_manager.ResetModules() |
|
2254 |
|
2255 implicit_matcher = CreateImplicitMatcher(self.module_dict, |
|
2256 root_path, |
|
2257 login_url) |
|
2258 config, explicit_matcher = LoadAppConfig(root_path, self.module_dict) |
|
2259 env_dict['CURRENT_VERSION_ID'] = config.version + ".1" |
|
2260 env_dict['APPLICATION_ID'] = config.application |
|
2261 dispatcher = MatcherDispatcher(login_url, |
|
2262 [implicit_matcher, explicit_matcher]) |
|
2263 |
|
2264 if require_indexes: |
|
2265 dev_appserver_index.SetupIndexes(config.application, root_path) |
|
2266 |
|
2267 infile = cStringIO.StringIO(self.rfile.read( |
|
2268 int(self.headers.get('content-length', 0)))) |
|
2269 outfile = cStringIO.StringIO() |
|
2270 try: |
|
2271 dispatcher.Dispatch(self.path, |
|
2272 None, |
|
2273 self.headers, |
|
2274 infile, |
|
2275 outfile, |
|
2276 base_env_dict=env_dict) |
|
2277 finally: |
|
2278 self.module_manager.UpdateModuleFileModificationTimes() |
|
2279 |
|
2280 outfile.flush() |
|
2281 outfile.seek(0) |
|
2282 status_code, status_message, header_data, body = RewriteResponse(outfile) |
|
2283 |
|
2284 except yaml_errors.EventListenerError, e: |
|
2285 title = 'Fatal error when loading application configuration' |
|
2286 msg = '%s:\n%s' % (title, str(e)) |
|
2287 logging.error(msg) |
|
2288 self.send_response(httplib.INTERNAL_SERVER_ERROR, title) |
|
2289 self.wfile.write('Content-Type: text/html\n\n') |
|
2290 self.wfile.write('<pre>%s</pre>' % cgi.escape(msg)) |
|
2291 except: |
|
2292 msg = 'Exception encountered handling request' |
|
2293 logging.exception(msg) |
|
2294 self.send_response(httplib.INTERNAL_SERVER_ERROR, msg) |
|
2295 tbhandler() |
|
2296 else: |
|
2297 try: |
|
2298 self.send_response(status_code, status_message) |
|
2299 self.wfile.write(header_data) |
|
2300 self.wfile.write('\r\n') |
|
2301 if self.command != 'HEAD': |
|
2302 self.wfile.write(body) |
|
2303 elif body: |
|
2304 logging.warning('Dropping unexpected body in response ' |
|
2305 'to HEAD request') |
|
2306 except (IOError, OSError), e: |
|
2307 if e.errno != errno.EPIPE: |
|
2308 raise e |
|
2309 except socket.error, e: |
|
2310 if len(e.args) >= 1 and e.args[0] != errno.EPIPE: |
|
2311 raise e |
|
2312 else: |
|
2313 if index_yaml_updater is not None: |
|
2314 index_yaml_updater.UpdateIndexYaml() |
|
2315 |
|
2316 def log_error(self, format, *args): |
|
2317 """Redirect error messages through the logging module.""" |
|
2318 logging.error(format, *args) |
|
2319 |
|
2320 def log_message(self, format, *args): |
|
2321 """Redirect log messages through the logging module.""" |
|
2322 logging.info(format, *args) |
|
2323 |
|
2324 return DevAppServerRequestHandler |
|
2325 |
|
2326 |
|
2327 def ReadAppConfig(appinfo_path, parse_app_config=appinfo.LoadSingleAppInfo): |
|
2328 """Reads app.yaml file and returns its app id and list of URLMap instances. |
|
2329 |
|
2330 Args: |
|
2331 appinfo_path: String containing the path to the app.yaml file. |
|
2332 parse_app_config: Used for dependency injection. |
|
2333 |
|
2334 Returns: |
|
2335 AppInfoExternal instance. |
|
2336 |
|
2337 Raises: |
|
2338 If the config file could not be read or the config does not contain any |
|
2339 URLMap instances, this function will raise an InvalidAppConfigError |
|
2340 exception. |
|
2341 """ |
|
2342 try: |
|
2343 appinfo_file = file(appinfo_path, 'r') |
|
2344 try: |
|
2345 return parse_app_config(appinfo_file) |
|
2346 finally: |
|
2347 appinfo_file.close() |
|
2348 except IOError, e: |
|
2349 raise InvalidAppConfigError( |
|
2350 'Application configuration could not be read from "%s"' % appinfo_path) |
|
2351 |
|
2352 |
|
2353 def CreateURLMatcherFromMaps(root_path, |
|
2354 url_map_list, |
|
2355 module_dict, |
|
2356 create_url_matcher=URLMatcher, |
|
2357 create_cgi_dispatcher=CGIDispatcher, |
|
2358 create_file_dispatcher=FileDispatcher, |
|
2359 create_path_adjuster=PathAdjuster): |
|
2360 """Creates a URLMatcher instance from URLMap. |
|
2361 |
|
2362 Creates all of the correct URLDispatcher instances to handle the various |
|
2363 content types in the application configuration. |
|
2364 |
|
2365 Args: |
|
2366 root_path: Path to the root of the application running on the server. |
|
2367 url_map_list: List of appinfo.URLMap objects to initialize this |
|
2368 matcher with. Can be an empty list if you would like to add patterns |
|
2369 manually. |
|
2370 module_dict: Dictionary in which application-loaded modules should be |
|
2371 preserved between requests. This dictionary must be separate from the |
|
2372 sys.modules dictionary. |
|
2373 create_url_matcher, create_cgi_dispatcher, create_file_dispatcher, |
|
2374 create_path_adjuster: Used for dependency injection. |
|
2375 |
|
2376 Returns: |
|
2377 Instance of URLMatcher with the supplied URLMap objects properly loaded. |
|
2378 """ |
|
2379 url_matcher = create_url_matcher() |
|
2380 path_adjuster = create_path_adjuster(root_path) |
|
2381 cgi_dispatcher = create_cgi_dispatcher(module_dict, root_path, path_adjuster) |
|
2382 file_dispatcher = create_file_dispatcher(path_adjuster, |
|
2383 StaticFileMimeTypeMatcher(url_map_list, path_adjuster)) |
|
2384 |
|
2385 for url_map in url_map_list: |
|
2386 admin_only = url_map.login == appinfo.LOGIN_ADMIN |
|
2387 requires_login = url_map.login == appinfo.LOGIN_REQUIRED or admin_only |
|
2388 |
|
2389 handler_type = url_map.GetHandlerType() |
|
2390 if handler_type == appinfo.HANDLER_SCRIPT: |
|
2391 dispatcher = cgi_dispatcher |
|
2392 elif handler_type in (appinfo.STATIC_FILES, appinfo.STATIC_DIR): |
|
2393 dispatcher = file_dispatcher |
|
2394 else: |
|
2395 raise InvalidAppConfigError('Unknown handler type "%s"' % handler_type) |
|
2396 |
|
2397 regex = url_map.url |
|
2398 path = url_map.GetHandler() |
|
2399 if handler_type == appinfo.STATIC_DIR: |
|
2400 if regex[-1] == r'/': |
|
2401 regex = regex[:-1] |
|
2402 if path[-1] == os.path.sep: |
|
2403 path = path[:-1] |
|
2404 regex = '/'.join((re.escape(regex), '(.*)')) |
|
2405 if os.path.sep == '\\': |
|
2406 backref = r'\\1' |
|
2407 else: |
|
2408 backref = r'\1' |
|
2409 path = os.path.normpath(path) + os.path.sep + backref |
|
2410 |
|
2411 url_matcher.AddURL(regex, |
|
2412 dispatcher, |
|
2413 path, |
|
2414 requires_login, admin_only) |
|
2415 |
|
2416 return url_matcher |
|
2417 |
|
2418 |
|
2419 def LoadAppConfig(root_path, |
|
2420 module_dict, |
|
2421 read_app_config=ReadAppConfig, |
|
2422 create_matcher=CreateURLMatcherFromMaps): |
|
2423 """Creates a Matcher instance for an application configuration file. |
|
2424 |
|
2425 Raises an InvalidAppConfigError exception if there is anything wrong with |
|
2426 the application configuration file. |
|
2427 |
|
2428 Args: |
|
2429 root_path: Path to the root of the application to load. |
|
2430 module_dict: Dictionary in which application-loaded modules should be |
|
2431 preserved between requests. This dictionary must be separate from the |
|
2432 sys.modules dictionary. |
|
2433 read_url_map, create_matcher: Used for dependency injection. |
|
2434 |
|
2435 Returns: |
|
2436 tuple: (AppInfoExternal, URLMatcher) |
|
2437 """ |
|
2438 |
|
2439 for appinfo_path in [os.path.join(root_path, 'app.yaml'), |
|
2440 os.path.join(root_path, 'app.yml')]: |
|
2441 |
|
2442 if os.path.isfile(appinfo_path): |
|
2443 try: |
|
2444 config = read_app_config(appinfo_path, appinfo.LoadSingleAppInfo) |
|
2445 |
|
2446 matcher = create_matcher(root_path, |
|
2447 config.handlers, |
|
2448 module_dict) |
|
2449 |
|
2450 return (config, matcher) |
|
2451 except gexcept.AbstractMethod: |
|
2452 pass |
|
2453 |
|
2454 raise AppConfigNotFoundError |
|
2455 |
|
2456 |
|
2457 def SetupStubs(app_id, **config): |
|
2458 """Sets up testing stubs of APIs. |
|
2459 |
|
2460 Args: |
|
2461 app_id: Application ID being served. |
|
2462 |
|
2463 Keywords: |
|
2464 login_url: Relative URL which should be used for handling user login/logout. |
|
2465 datastore_path: Path to the file to store Datastore file stub data in. |
|
2466 history_path: Path to the file to store Datastore history in. |
|
2467 clear_datastore: If the datastore and history should be cleared on startup. |
|
2468 smtp_host: SMTP host used for sending test mail. |
|
2469 smtp_port: SMTP port. |
|
2470 smtp_user: SMTP user. |
|
2471 smtp_password: SMTP password. |
|
2472 enable_sendmail: Whether to use sendmail as an alternative to SMTP. |
|
2473 show_mail_body: Whether to log the body of emails. |
|
2474 remove: Used for dependency injection. |
|
2475 """ |
|
2476 login_url = config['login_url'] |
|
2477 datastore_path = config['datastore_path'] |
|
2478 history_path = config['history_path'] |
|
2479 clear_datastore = config['clear_datastore'] |
|
2480 require_indexes = config.get('require_indexes', False) |
|
2481 smtp_host = config.get('smtp_host', None) |
|
2482 smtp_port = config.get('smtp_port', 25) |
|
2483 smtp_user = config.get('smtp_user', '') |
|
2484 smtp_password = config.get('smtp_password', '') |
|
2485 enable_sendmail = config.get('enable_sendmail', False) |
|
2486 show_mail_body = config.get('show_mail_body', False) |
|
2487 remove = config.get('remove', os.remove) |
|
2488 |
|
2489 if clear_datastore: |
|
2490 for path in (datastore_path, history_path): |
|
2491 if os.path.lexists(path): |
|
2492 logging.info('Attempting to remove file at %s', path) |
|
2493 try: |
|
2494 remove(path) |
|
2495 except OSError, e: |
|
2496 logging.warning('Removing file failed: %s', e) |
|
2497 |
|
2498 apiproxy_stub_map.apiproxy = apiproxy_stub_map.APIProxyStubMap() |
|
2499 |
|
2500 datastore = datastore_file_stub.DatastoreFileStub( |
|
2501 app_id, datastore_path, history_path, require_indexes=require_indexes) |
|
2502 apiproxy_stub_map.apiproxy.RegisterStub('datastore_v3', datastore) |
|
2503 |
|
2504 fixed_login_url = '%s?%s=%%s' % (login_url, |
|
2505 dev_appserver_login.CONTINUE_PARAM) |
|
2506 fixed_logout_url = '%s&%s' % (fixed_login_url, |
|
2507 dev_appserver_login.LOGOUT_PARAM) |
|
2508 |
|
2509 apiproxy_stub_map.apiproxy.RegisterStub( |
|
2510 'user', |
|
2511 user_service_stub.UserServiceStub(login_url=fixed_login_url, |
|
2512 logout_url=fixed_logout_url)) |
|
2513 |
|
2514 apiproxy_stub_map.apiproxy.RegisterStub( |
|
2515 'urlfetch', |
|
2516 urlfetch_stub.URLFetchServiceStub()) |
|
2517 |
|
2518 apiproxy_stub_map.apiproxy.RegisterStub( |
|
2519 'mail', |
|
2520 mail_stub.MailServiceStub(smtp_host, |
|
2521 smtp_port, |
|
2522 smtp_user, |
|
2523 smtp_password, |
|
2524 enable_sendmail=enable_sendmail, |
|
2525 show_mail_body=show_mail_body)) |
|
2526 |
|
2527 apiproxy_stub_map.apiproxy.RegisterStub( |
|
2528 'memcache', |
|
2529 memcache_stub.MemcacheServiceStub()) |
|
2530 |
|
2531 try: |
|
2532 from google.appengine.api.images import images_stub |
|
2533 apiproxy_stub_map.apiproxy.RegisterStub( |
|
2534 'images', |
|
2535 images_stub.ImagesServiceStub()) |
|
2536 except ImportError, e: |
|
2537 logging.warning('Could not initialize images API; you are likely missing ' |
|
2538 'the Python "PIL" module. ImportError: %s', e) |
|
2539 from google.appengine.api.images import images_not_implemented_stub |
|
2540 apiproxy_stub_map.apiproxy.RegisterStub('images', |
|
2541 images_not_implemented_stub.ImagesNotImplementedServiceStub()) |
|
2542 |
|
2543 |
|
2544 def CreateImplicitMatcher(module_dict, |
|
2545 root_path, |
|
2546 login_url, |
|
2547 create_path_adjuster=PathAdjuster, |
|
2548 create_local_dispatcher=LocalCGIDispatcher, |
|
2549 create_cgi_dispatcher=CGIDispatcher): |
|
2550 """Creates a URLMatcher instance that handles internal URLs. |
|
2551 |
|
2552 Used to facilitate handling user login/logout, debugging, info about the |
|
2553 currently running app, etc. |
|
2554 |
|
2555 Args: |
|
2556 module_dict: Dictionary in the form used by sys.modules. |
|
2557 root_path: Path to the root of the application. |
|
2558 login_url: Relative URL which should be used for handling user login/logout. |
|
2559 create_local_dispatcher: Used for dependency injection. |
|
2560 |
|
2561 Returns: |
|
2562 Instance of URLMatcher with appropriate dispatchers. |
|
2563 """ |
|
2564 url_matcher = URLMatcher() |
|
2565 path_adjuster = create_path_adjuster(root_path) |
|
2566 |
|
2567 login_dispatcher = create_local_dispatcher(sys.modules, path_adjuster, |
|
2568 dev_appserver_login.main) |
|
2569 url_matcher.AddURL(login_url, |
|
2570 login_dispatcher, |
|
2571 '', |
|
2572 False, |
|
2573 False) |
|
2574 |
|
2575 |
|
2576 admin_dispatcher = create_cgi_dispatcher(module_dict, root_path, |
|
2577 path_adjuster) |
|
2578 url_matcher.AddURL('/_ah/admin(?:/.*)?', |
|
2579 admin_dispatcher, |
|
2580 DEVEL_CONSOLE_PATH, |
|
2581 False, |
|
2582 False) |
|
2583 |
|
2584 return url_matcher |
|
2585 |
|
2586 |
|
2587 def SetupTemplates(template_dir): |
|
2588 """Reads debugging console template files and initializes the console. |
|
2589 |
|
2590 Does nothing if templates have already been initialized. |
|
2591 |
|
2592 Args: |
|
2593 template_dir: Path to the directory containing the templates files. |
|
2594 |
|
2595 Raises: |
|
2596 OSError or IOError if any of the template files could not be read. |
|
2597 """ |
|
2598 if ApplicationLoggingHandler.AreTemplatesInitialized(): |
|
2599 return |
|
2600 |
|
2601 try: |
|
2602 header = open(os.path.join(template_dir, HEADER_TEMPLATE)).read() |
|
2603 script = open(os.path.join(template_dir, SCRIPT_TEMPLATE)).read() |
|
2604 middle = open(os.path.join(template_dir, MIDDLE_TEMPLATE)).read() |
|
2605 footer = open(os.path.join(template_dir, FOOTER_TEMPLATE)).read() |
|
2606 except (OSError, IOError): |
|
2607 logging.error('Could not read template files from %s', template_dir) |
|
2608 raise |
|
2609 |
|
2610 ApplicationLoggingHandler.InitializeTemplates(header, script, middle, footer) |
|
2611 |
|
2612 |
|
2613 def CreateServer(root_path, |
|
2614 login_url, |
|
2615 port, |
|
2616 template_dir, |
|
2617 serve_address='', |
|
2618 require_indexes=False, |
|
2619 python_path_list=sys.path): |
|
2620 """Creates an new HTTPServer for an application. |
|
2621 |
|
2622 Args: |
|
2623 root_path: String containing the path to the root directory of the |
|
2624 application where the app.yaml file is. |
|
2625 login_url: Relative URL which should be used for handling user login/logout. |
|
2626 port: Port to start the application server on. |
|
2627 template_dir: Path to the directory in which the debug console templates |
|
2628 are stored. |
|
2629 serve_address: Address on which the server should serve. |
|
2630 require_indexes: True if index.yaml is read-only gospel; default False. |
|
2631 python_path_list: Used for dependency injection. |
|
2632 |
|
2633 Returns: |
|
2634 Instance of BaseHTTPServer.HTTPServer that's ready to start accepting. |
|
2635 """ |
|
2636 absolute_root_path = os.path.abspath(root_path) |
|
2637 |
|
2638 SetupTemplates(template_dir) |
|
2639 FakeFile.SetAllowedPaths([absolute_root_path, |
|
2640 os.path.dirname(os.path.dirname(google.__file__)), |
|
2641 template_dir]) |
|
2642 |
|
2643 handler_class = CreateRequestHandler(absolute_root_path, login_url, |
|
2644 require_indexes) |
|
2645 |
|
2646 if absolute_root_path not in python_path_list: |
|
2647 python_path_list.insert(0, absolute_root_path) |
|
2648 |
|
2649 return BaseHTTPServer.HTTPServer((serve_address, port), handler_class) |