1# (c) 2005-2006 James Gardner <james@pythonweb.org> 2# This module is part of the Python Paste Project and is released under 3# the MIT License: http://www.opensource.org/licenses/mit-license.php 4""" 5Middleware to display error documents for certain status codes 6 7The middleware in this module can be used to intercept responses with 8specified status codes and internally forward the request to an appropriate 9URL where the content can be displayed to the user as an error document. 10""" 11 12import warnings 13import sys 14from six.moves.urllib import parse as urlparse 15from paste.recursive import ForwardRequestException, RecursiveMiddleware, RecursionLoop 16from paste.util import converters 17from paste.response import replace_header 18import six 19 20def forward(app, codes): 21 """ 22 Intercepts a response with a particular status code and returns the 23 content from a specified URL instead. 24 25 The arguments are: 26 27 ``app`` 28 The WSGI application or middleware chain. 29 30 ``codes`` 31 A dictionary of integer status codes and the URL to be displayed 32 if the response uses that code. 33 34 For example, you might want to create a static file to display a 35 "File Not Found" message at the URL ``/error404.html`` and then use 36 ``forward`` middleware to catch all 404 status codes and display the page 37 you created. In this example ``app`` is your exisiting WSGI 38 applicaiton:: 39 40 from paste.errordocument import forward 41 app = forward(app, codes={404:'/error404.html'}) 42 43 """ 44 for code in codes: 45 if not isinstance(code, int): 46 raise TypeError('All status codes should be type int. ' 47 '%s is not valid'%repr(code)) 48 49 def error_codes_mapper(code, message, environ, global_conf, codes): 50 if code in codes: 51 return codes[code] 52 else: 53 return None 54 55 #return _StatusBasedRedirect(app, error_codes_mapper, codes=codes) 56 return RecursiveMiddleware( 57 StatusBasedForward( 58 app, 59 error_codes_mapper, 60 codes=codes, 61 ) 62 ) 63 64class StatusKeeper(object): 65 def __init__(self, app, status, url, headers): 66 self.app = app 67 self.status = status 68 self.url = url 69 self.headers = headers 70 71 def __call__(self, environ, start_response): 72 def keep_status_start_response(status, headers, exc_info=None): 73 for header, value in headers: 74 if header.lower() == 'set-cookie': 75 self.headers.append((header, value)) 76 else: 77 replace_header(self.headers, header, value) 78 return start_response(self.status, self.headers, exc_info) 79 parts = self.url.split('?') 80 environ['PATH_INFO'] = parts[0] 81 if len(parts) > 1: 82 environ['QUERY_STRING'] = parts[1] 83 else: 84 environ['QUERY_STRING'] = '' 85 #raise Exception(self.url, self.status) 86 try: 87 return self.app(environ, keep_status_start_response) 88 except RecursionLoop as e: 89 line = 'Recursion error getting error page: %s\n' % e 90 if six.PY3: 91 line = line.encode('utf8') 92 environ['wsgi.errors'].write(line) 93 keep_status_start_response('500 Server Error', [('Content-type', 'text/plain')], sys.exc_info()) 94 body = ('Error: %s. (Error page could not be fetched)' 95 % self.status) 96 if six.PY3: 97 body = body.encode('utf8') 98 return [body] 99 100 101class StatusBasedForward(object): 102 """ 103 Middleware that lets you test a response against a custom mapper object to 104 programatically determine whether to internally forward to another URL and 105 if so, which URL to forward to. 106 107 If you don't need the full power of this middleware you might choose to use 108 the simpler ``forward`` middleware instead. 109 110 The arguments are: 111 112 ``app`` 113 The WSGI application or middleware chain. 114 115 ``mapper`` 116 A callable that takes a status code as the 117 first parameter, a message as the second, and accepts optional environ, 118 global_conf and named argments afterwards. It should return a 119 URL to forward to or ``None`` if the code is not to be intercepted. 120 121 ``global_conf`` 122 Optional default configuration from your config file. If ``debug`` is 123 set to ``true`` a message will be written to ``wsgi.errors`` on each 124 internal forward stating the URL forwarded to. 125 126 ``**params`` 127 Optional, any other configuration and extra arguments you wish to 128 pass which will in turn be passed back to the custom mapper object. 129 130 Here is an example where a ``404 File Not Found`` status response would be 131 redirected to the URL ``/error?code=404&message=File%20Not%20Found``. This 132 could be useful for passing the status code and message into another 133 application to display an error document: 134 135 .. code-block:: python 136 137 from paste.errordocument import StatusBasedForward 138 from paste.recursive import RecursiveMiddleware 139 from urllib import urlencode 140 141 def error_mapper(code, message, environ, global_conf, kw) 142 if code in [404, 500]: 143 params = urlencode({'message':message, 'code':code}) 144 url = '/error?'%(params) 145 return url 146 else: 147 return None 148 149 app = RecursiveMiddleware( 150 StatusBasedForward(app, mapper=error_mapper), 151 ) 152 153 """ 154 155 def __init__(self, app, mapper, global_conf=None, **params): 156 if global_conf is None: 157 global_conf = {} 158 # @@: global_conf shouldn't really come in here, only in a 159 # separate make_status_based_forward function 160 if global_conf: 161 self.debug = converters.asbool(global_conf.get('debug', False)) 162 else: 163 self.debug = False 164 self.application = app 165 self.mapper = mapper 166 self.global_conf = global_conf 167 self.params = params 168 169 def __call__(self, environ, start_response): 170 url = [] 171 172 def change_response(status, headers, exc_info=None): 173 status_code = status.split(' ') 174 try: 175 code = int(status_code[0]) 176 except (ValueError, TypeError): 177 raise Exception( 178 'StatusBasedForward middleware ' 179 'received an invalid status code %s'%repr(status_code[0]) 180 ) 181 message = ' '.join(status_code[1:]) 182 new_url = self.mapper( 183 code, 184 message, 185 environ, 186 self.global_conf, 187 **self.params 188 ) 189 if not (new_url == None or isinstance(new_url, str)): 190 raise TypeError( 191 'Expected the url to internally ' 192 'redirect to in the StatusBasedForward mapper' 193 'to be a string or None, not %r' % new_url) 194 if new_url: 195 url.append([new_url, status, headers]) 196 # We have to allow the app to write stuff, even though 197 # we'll ignore it: 198 return [].append 199 else: 200 return start_response(status, headers, exc_info) 201 202 app_iter = self.application(environ, change_response) 203 if url: 204 if hasattr(app_iter, 'close'): 205 app_iter.close() 206 207 def factory(app): 208 return StatusKeeper(app, status=url[0][1], url=url[0][0], 209 headers=url[0][2]) 210 raise ForwardRequestException(factory=factory) 211 else: 212 return app_iter 213 214def make_errordocument(app, global_conf, **kw): 215 """ 216 Paste Deploy entry point to create a error document wrapper. 217 218 Use like:: 219 220 [filter-app:main] 221 use = egg:Paste#errordocument 222 next = real-app 223 500 = /lib/msg/500.html 224 404 = /lib/msg/404.html 225 """ 226 map = {} 227 for status, redir_loc in kw.items(): 228 try: 229 status = int(status) 230 except ValueError: 231 raise ValueError('Bad status code: %r' % status) 232 map[status] = redir_loc 233 forwarder = forward(app, map) 234 return forwarder 235 236__pudge_all__ = [ 237 'forward', 238 'make_errordocument', 239 'empty_error', 240 'make_empty_error', 241 'StatusBasedForward', 242] 243 244 245############################################################################### 246## Deprecated 247############################################################################### 248 249def custom_forward(app, mapper, global_conf=None, **kw): 250 """ 251 Deprectated; use StatusBasedForward instead. 252 """ 253 warnings.warn( 254 "errordocuments.custom_forward has been deprecated; please " 255 "use errordocuments.StatusBasedForward", 256 DeprecationWarning, 2) 257 if global_conf is None: 258 global_conf = {} 259 return _StatusBasedRedirect(app, mapper, global_conf, **kw) 260 261class _StatusBasedRedirect(object): 262 """ 263 Deprectated; use StatusBasedForward instead. 264 """ 265 def __init__(self, app, mapper, global_conf=None, **kw): 266 267 warnings.warn( 268 "errordocuments._StatusBasedRedirect has been deprecated; please " 269 "use errordocuments.StatusBasedForward", 270 DeprecationWarning, 2) 271 272 if global_conf is None: 273 global_conf = {} 274 self.application = app 275 self.mapper = mapper 276 self.global_conf = global_conf 277 self.kw = kw 278 self.fallback_template = """ 279 <html> 280 <head> 281 <title>Error %(code)s</title> 282 </html> 283 <body> 284 <h1>Error %(code)s</h1> 285 <p>%(message)s</p> 286 <hr> 287 <p> 288 Additionally an error occurred trying to produce an 289 error document. A description of the error was logged 290 to <tt>wsgi.errors</tt>. 291 </p> 292 </body> 293 </html> 294 """ 295 296 def __call__(self, environ, start_response): 297 url = [] 298 code_message = [] 299 try: 300 def change_response(status, headers, exc_info=None): 301 new_url = None 302 parts = status.split(' ') 303 try: 304 code = int(parts[0]) 305 except (ValueError, TypeError): 306 raise Exception( 307 '_StatusBasedRedirect middleware ' 308 'received an invalid status code %s'%repr(parts[0]) 309 ) 310 message = ' '.join(parts[1:]) 311 new_url = self.mapper( 312 code, 313 message, 314 environ, 315 self.global_conf, 316 self.kw 317 ) 318 if not (new_url == None or isinstance(new_url, str)): 319 raise TypeError( 320 'Expected the url to internally ' 321 'redirect to in the _StatusBasedRedirect error_mapper' 322 'to be a string or None, not %s'%repr(new_url) 323 ) 324 if new_url: 325 url.append(new_url) 326 code_message.append([code, message]) 327 return start_response(status, headers, exc_info) 328 app_iter = self.application(environ, change_response) 329 except: 330 try: 331 import sys 332 error = str(sys.exc_info()[1]) 333 except: 334 error = '' 335 try: 336 code, message = code_message[0] 337 except: 338 code, message = ['', ''] 339 environ['wsgi.errors'].write( 340 'Error occurred in _StatusBasedRedirect ' 341 'intercepting the response: '+str(error) 342 ) 343 return [self.fallback_template 344 % {'message': message, 'code': code}] 345 else: 346 if url: 347 url_ = url[0] 348 new_environ = {} 349 for k, v in environ.items(): 350 if k != 'QUERY_STRING': 351 new_environ['QUERY_STRING'] = urlparse.urlparse(url_)[4] 352 else: 353 new_environ[k] = v 354 class InvalidForward(Exception): 355 pass 356 def eat_start_response(status, headers, exc_info=None): 357 """ 358 We don't want start_response to do anything since it 359 has already been called 360 """ 361 if status[:3] != '200': 362 raise InvalidForward( 363 "The URL %s to internally forward " 364 "to in order to create an error document did not " 365 "return a '200' status code." % url_ 366 ) 367 forward = environ['paste.recursive.forward'] 368 old_start_response = forward.start_response 369 forward.start_response = eat_start_response 370 try: 371 app_iter = forward(url_, new_environ) 372 except InvalidForward: 373 code, message = code_message[0] 374 environ['wsgi.errors'].write( 375 'Error occurred in ' 376 '_StatusBasedRedirect redirecting ' 377 'to new URL: '+str(url[0]) 378 ) 379 return [ 380 self.fallback_template%{ 381 'message':message, 382 'code':code, 383 } 384 ] 385 else: 386 forward.start_response = old_start_response 387 return app_iter 388 else: 389 return app_iter 390