0001# (c) 2005-2006 James Gardner <james@pythonweb.org>
0002# This module is part of the Python Paste Project and is released under
0003# the MIT License: http://www.opensource.org/licenses/mit-license.php
0004"""
0005Middleware to display error documents for certain status codes
0006
0007The middleware in this module can be used to intercept responses with
0008specified status codes and internally forward the request to an appropriate
0009URL where the content can be displayed to the user as an error document.
0010"""
0011
0012import warnings
0013from urlparse import urlparse
0014from paste.recursive import ForwardRequestException, RecursiveMiddleware
0015from paste.util import converters
0016from paste.response import replace_header
0017
0018def forward(app, codes):
0019 """
0020 Intercepts a response with a particular status code and returns the
0021 content from a specified URL instead.
0022
0023 The arguments are:
0024
0025 ``app``
0026 The WSGI application or middleware chain.
0027
0028 ``codes``
0029 A dictionary of integer status codes and the URL to be displayed
0030 if the response uses that code.
0031
0032 For example, you might want to create a static file to display a
0033 "File Not Found" message at the URL ``/error404.html`` and then use
0034 ``forward`` middleware to catch all 404 status codes and display the page
0035 you created. In this example ``app`` is your exisiting WSGI
0036 applicaiton::
0037
0038 from paste.errordocument import forward
0039 app = forward(app, codes={404:'/error404.html'})
0040
0041 """
0042 for code in codes:
0043 if not isinstance(code, int):
0044 raise TypeError('All status codes should be type int. '
0045 '%s is not valid'%repr(code))
0046
0047 def error_codes_mapper(code, message, environ, global_conf, codes):
0048 if codes.has_key(code):
0049 return codes[code]
0050 else:
0051 return None
0052
0053 #return _StatusBasedRedirect(app, error_codes_mapper, codes=codes)
0054 return RecursiveMiddleware(
0055 StatusBasedForward(
0056 app,
0057 error_codes_mapper,
0058 codes=codes,
0059 )
0060 )
0061
0062class StatusKeeper(object):
0063 def __init__(self, app, status, url, headers):
0064 self.app = app
0065 self.status = status
0066 self.url = url
0067 self.headers = headers
0068
0069 def __call__(self, environ, start_response):
0070 def keep_status_start_response(status, headers, exc_info=None):
0071 for header, value in headers:
0072 if header.lower() == 'set-cookie':
0073 self.headers.append((header, value))
0074 else:
0075 replace_header(self.headers, header, value)
0076 return start_response(self.status, self.headers, exc_info)
0077 parts = self.url.split('?')
0078 environ['PATH_INFO'] = parts[0]
0079 if len(parts) > 1:
0080 environ['QUERY_STRING'] = parts[1]
0081 else:
0082 environ['QUERY_STRING'] = ''
0083 #raise Exception(self.url, self.status)
0084 return self.app(environ, keep_status_start_response)
0085
0086class StatusBasedForward(object):
0087 """
0088 Middleware that lets you test a response against a custom mapper object to
0089 programatically determine whether to internally forward to another URL and
0090 if so, which URL to forward to.
0091
0092 If you don't need the full power of this middleware you might choose to use
0093 the simpler ``forward`` middleware instead.
0094
0095 The arguments are:
0096
0097 ``app``
0098 The WSGI application or middleware chain.
0099
0100 ``mapper``
0101 A callable that takes a status code as the
0102 first parameter, a message as the second, and accepts optional environ,
0103 global_conf and named argments afterwards. It should return a
0104 URL to forward to or ``None`` if the code is not to be intercepted.
0105
0106 ``global_conf``
0107 Optional default configuration from your config file. If ``debug`` is
0108 set to ``true`` a message will be written to ``wsgi.errors`` on each
0109 internal forward stating the URL forwarded to.
0110
0111 ``**params``
0112 Optional, any other configuration and extra arguments you wish to
0113 pass which will in turn be passed back to the custom mapper object.
0114
0115 Here is an example where a ``404 File Not Found`` status response would be
0116 redirected to the URL ``/error?code=404&message=File%20Not%20Found``. This
0117 could be useful for passing the status code and message into another
0118 application to display an error document:
0119
0120 .. code-block:: Python
0121
0122 from paste.errordocument import StatusBasedForward
0123 from paste.recursive import RecursiveMiddleware
0124 from urllib import urlencode
0125
0126 def error_mapper(code, message, environ, global_conf, kw)
0127 if code in [404, 500]:
0128 params = urlencode({'message':message, 'code':code})
0129 url = '/error?'%(params)
0130 return url
0131 else:
0132 return None
0133
0134 app = RecursiveMiddleware(
0135 StatusBasedForward(app, mapper=error_mapper),
0136 )
0137
0138 """
0139
0140 def __init__(self, app, mapper, global_conf=None, **params):
0141 if global_conf is None:
0142 global_conf = {}
0143 # @@: global_conf shouldn't really come in here, only in a
0144 # separate make_status_based_forward function
0145 if global_conf:
0146 self.debug = converters.asbool(global_conf.get('debug', False))
0147 else:
0148 self.debug = False
0149 self.application = app
0150 self.mapper = mapper
0151 self.global_conf = global_conf
0152 self.params = params
0153
0154 def __call__(self, environ, start_response):
0155 url = []
0156
0157 def change_response(status, headers, exc_info=None):
0158 status_code = status.split(' ')
0159 try:
0160 code = int(status_code[0])
0161 except (ValueError, TypeError):
0162 raise Exception(
0163 'StatusBasedForward middleware '
0164 'received an invalid status code %s'%repr(status_code[0])
0165 )
0166 message = ' '.join(status_code[1:])
0167 new_url = self.mapper(
0168 code,
0169 message,
0170 environ,
0171 self.global_conf,
0172 **self.params
0173 )
0174 if not (new_url == None or isinstance(new_url, str)):
0175 raise TypeError(
0176 'Expected the url to internally '
0177 'redirect to in the StatusBasedForward mapper'
0178 'to be a string or None, not %s'%repr(new_url)
0179 )
0180 if new_url:
0181 url.append([new_url, status, headers])
0182 else:
0183 return start_response(status, headers, exc_info)
0184
0185 app_iter = self.application(environ, change_response)
0186 if url:
0187 if hasattr(app_iter, 'close'):
0188 app_iter.close()
0189
0190 def factory(app):
0191 return StatusKeeper(app, status=url[0][1], url=url[0][0],
0192 headers=url[0][2])
0193 raise ForwardRequestException(factory=factory)
0194 else:
0195 return app_iter
0196
0197def make_errordocument(app, global_conf, **kw):
0198 """
0199 Paste Deploy entry point to create a error document wrapper.
0200
0201 Use like::
0202
0203 [filter-app:main]
0204 use = egg:Paste#errordocument
0205 next = real-app
0206 500 = /lib/msg/500.html
0207 404 = /lib/msg/404.html
0208 """
0209 map = {}
0210 for status, redir_loc in kw.items():
0211 try:
0212 status = int(status)
0213 except ValueError:
0214 raise ValueError('Bad status code: %r' % status)
0215 map[status] = redir_loc
0216 forwarder = forward(app, map)
0217 return forwarder
0218
0219__pudge_all__ = [
0220 'forward',
0221 'make_errordocument',
0222 'empty_error',
0223 'make_empty_error',
0224 'StatusBasedForward',
0225]
0226
0227
0228###############################################################################
0229## Deprecated
0230###############################################################################
0231
0232def custom_forward(app, mapper, global_conf=None, **kw):
0233 """
0234 Deprectated; use StatusBasedForward instead.
0235 """
0236 warnings.warn(
0237 "errordocuments.custom_forward has been deprecated; please "
0238 "use errordocuments.StatusBasedForward",
0239 DeprecationWarning, 2)
0240 if global_conf is None:
0241 global_conf = {}
0242 return _StatusBasedRedirect(app, mapper, global_conf, **kw)
0243
0244class _StatusBasedRedirect(object):
0245 """
0246 Deprectated; use StatusBasedForward instead.
0247 """
0248 def __init__(self, app, mapper, global_conf=None, **kw):
0249
0250 warnings.warn(
0251 "errordocuments._StatusBasedRedirect has been deprecated; please "
0252 "use errordocuments.StatusBasedForward",
0253 DeprecationWarning, 2)
0254
0255 if global_conf is None:
0256 global_conf = {}
0257 self.application = app
0258 self.mapper = mapper
0259 self.global_conf = global_conf
0260 self.kw = kw
0261 self.fallback_template = """
0262 <html>
0263 <head>
0264 <title>Error %(code)s</title>
0265 </html>
0266 <body>
0267 <h1>Error %(code)s</h1>
0268 <p>%(message)s</p>
0269 <hr>
0270 <p>
0271 Additionally an error occurred trying to produce an
0272 error document. A description of the error was logged
0273 to <tt>wsgi.errors</tt>.
0274 </p>
0275 </body>
0276 </html>
0277 """
0278
0279 def __call__(self, environ, start_response):
0280 url = []
0281 code_message = []
0282 try:
0283 def change_response(status, headers, exc_info=None):
0284 new_url = None
0285 parts = status.split(' ')
0286 try:
0287 code = int(parts[0])
0288 except ValueError, TypeError:
0289 raise Exception(
0290 '_StatusBasedRedirect middleware '
0291 'received an invalid status code %s'%repr(parts[0])
0292 )
0293 message = ' '.join(parts[1:])
0294 new_url = self.mapper(
0295 code,
0296 message,
0297 environ,
0298 self.global_conf,
0299 self.kw
0300 )
0301 if not (new_url == None or isinstance(new_url, str)):
0302 raise TypeError(
0303 'Expected the url to internally '
0304 'redirect to in the _StatusBasedRedirect error_mapper'
0305 'to be a string or None, not %s'%repr(new_url)
0306 )
0307 if new_url:
0308 url.append(new_url)
0309 code_message.append([code, message])
0310 return start_response(status, headers, exc_info)
0311 app_iter = self.application(environ, change_response)
0312 except:
0313 try:
0314 import sys
0315 error = str(sys.exc_info()[1])
0316 except:
0317 error = ''
0318 try:
0319 code, message = code_message[0]
0320 except:
0321 code, message = ['', '']
0322 environ['wsgi.errors'].write(
0323 'Error occurred in _StatusBasedRedirect '
0324 'intercepting the response: '+str(error)
0325 )
0326 return [self.fallback_template
0327 % {'message': message, 'code': code}]
0328 else:
0329 if url:
0330 url_ = url[0]
0331 new_environ = {}
0332 for k, v in environ.items():
0333 if k != 'QUERY_STRING':
0334 new_environ['QUERY_STRING'] = urlparse(url_)[4]
0335 else:
0336 new_environ[k] = v
0337 class InvalidForward(Exception):
0338 pass
0339 def eat_start_response(status, headers, exc_info=None):
0340 """
0341 We don't want start_response to do anything since it
0342 has already been called
0343 """
0344 if status[:3] != '200':
0345 raise InvalidForward(
0346 "The URL %s to internally forward "
0347 "to in order to create an error document did not "
0348 "return a '200' status code." % url_
0349 )
0350 forward = environ['paste.recursive.forward']
0351 old_start_response = forward.start_response
0352 forward.start_response = eat_start_response
0353 try:
0354 app_iter = forward(url_, new_environ)
0355 except InvalidForward, e:
0356 code, message = code_message[0]
0357 environ['wsgi.errors'].write(
0358 'Error occurred in '
0359 '_StatusBasedRedirect redirecting '
0360 'to new URL: '+str(url[0])
0361 )
0362 return [
0363 self.fallback_template%{
0364 'message':message,
0365 'code':code,
0366 }
0367 ]
0368 else:
0369 forward.start_response = old_start_response
0370 return app_iter
0371 else:
0372 return app_iter