1 # (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org) 2 # Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php 3 4 """ 5 Cascades through several applications, so long as applications 6 return ``404 Not Found``. 7 """ 8 from paste import httpexceptions 9 from paste.util import converters 10 import tempfile 11 from cStringIO import StringIO 12 13 __all__ = ['Cascade'] 14 15 def make_cascade(loader, global_conf, catch='404', **local_conf): 16 """ 17 Entry point for Paste Deploy configuration 18 19 Expects configuration like:: 20 21 [composit:cascade] 22 use = egg:Paste#cascade 23 # all start with 'app' and are sorted alphabetically 24 app1 = foo 25 app2 = bar 26 ... 27 catch = 404 500 ... 28 """ 29 catch = map(int, converters.aslist(catch)) 30 apps = [] 31 for name, value in local_conf.items(): 32 if not name.startswith('app'): 33 raise ValueError( 34 "Bad configuration key %r (=%r); all configuration keys " 35 "must start with 'app'" 36 % (name, value)) 37 app = loader.get_app(value, global_conf=global_conf) 38 apps.append((name, app)) 39 apps.sort() 40 apps = [app for name, app in apps] 41 return Cascade(apps, catch=catch) 42 43 class Cascade(object): 44 45 """ 46 Passed a list of applications, ``Cascade`` will try each of them 47 in turn. If one returns a status code listed in ``catch`` (by 48 default just ``404 Not Found``) then the next application is 49 tried. 50 51 If all applications fail, then the last application's failure 52 response is used. 53 54 Instances of this class are WSGI applications. 55 """ 56 57 def __init__(self, applications, catch=(404,)): 58 self.apps = applications 59 self.catch_codes = {} 60 self.catch_exceptions = [] 61 for error in catch: 62 if isinstance(error, str): 63 error = int(error.split(None, 1)[0]) 64 if isinstance(error, httpexceptions.HTTPException): 65 exc = error 66 code = error.code 67 else: 68 exc = httpexceptions.get_exception(error) 69 code = error 70 self.catch_codes[code] = exc 71 self.catch_exceptions.append(exc) 72 self.catch_exceptions = tuple(self.catch_exceptions) 73 74 def __call__(self, environ, start_response): 75 """ 76 WSGI application interface 77 """ 78 failed = [] 79 def repl_start_response(status, headers, exc_info=None): 80 code = int(status.split(None, 1)[0]) 81 if code in self.catch_codes: 82 failed.append(None) 83 return _consuming_writer 84 return start_response(status, headers, exc_info) 85 86 try: 87 length = int(environ.get('CONTENT_LENGTH', 0) or 0) 88 except ValueError: 89 length = 0 90 if length > 0: 91 # We have to copy wsgi.input 92 copy_wsgi_input = True 93 if length > 4096 or length < 0: 94 f = tempfile.TemporaryFile() 95 if length < 0: 96 f.write(environ['wsgi.input'].read()) 97 else: 98 copy_len = length 99 while copy_len > 0: 100 chunk = environ['wsgi.input'].read(min(copy_len, 4096)) 101 if not chunk: 102 raise IOError("Request body truncated") 103 f.write(chunk) 104 copy_len -= len(chunk) 105 f.seek(0) 106 else: 107 f = StringIO(environ['wsgi.input'].read(length)) 108 environ['wsgi.input'] = f 109 else: 110 copy_wsgi_input = False 111 for app in self.apps[:-1]: 112 environ_copy = environ.copy() 113 if copy_wsgi_input: 114 environ_copy['wsgi.input'].seek(0) 115 failed = [] 116 try: 117 v = app(environ_copy, repl_start_response) 118 if not failed: 119 return v 120 else: 121 if hasattr(v, 'close'): 122 # Exhaust the iterator first: 123 list(v) 124 # then close: 125 v.close() 126 except self.catch_exceptions: 127 pass 128 if copy_wsgi_input: 129 environ['wsgi.input'].seek(0) 130 return self.apps[-1](environ, start_response) 131 132 def _consuming_writer(s): 133 pass 134