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