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# (c) 2005 Ian Bicking, Clark C. Evans and contributors
4# This module is part of the Python Paste Project and is released under
5# the MIT License: http://www.opensource.org/licenses/mit-license.php
6# Some of this code was funded by http://prometheusresearch.com
7"""
8HTTP Exception Middleware
9
10This module processes Python exceptions that relate to HTTP exceptions
11by defining a set of exceptions, all subclasses of HTTPException, and a
12request handler (`middleware`) that catches these exceptions and turns
13them into proper responses.
14
15This module defines exceptions according to RFC 2068 [1]_ : codes with
16100-300 are not really errors; 400's are client errors, and 500's are
17server errors.  According to the WSGI specification [2]_ , the application
18can call ``start_response`` more then once only under two conditions:
19(a) the response has not yet been sent, or (b) if the second and
20subsequent invocations of ``start_response`` have a valid ``exc_info``
21argument obtained from ``sys.exc_info()``.  The WSGI specification then
22requires the server or gateway to handle the case where content has been
23sent and then an exception was encountered.
24
25Exceptions in the 5xx range and those raised after ``start_response``
26has been called are treated as serious errors and the ``exc_info`` is
27filled-in with information needed for a lower level module to generate a
28stack trace and log information.
29
30Exception
31  HTTPException
32    HTTPRedirection
33      * 300 - HTTPMultipleChoices
34      * 301 - HTTPMovedPermanently
35      * 302 - HTTPFound
36      * 303 - HTTPSeeOther
37      * 304 - HTTPNotModified
38      * 305 - HTTPUseProxy
39      * 306 - Unused (not implemented, obviously)
40      * 307 - HTTPTemporaryRedirect
41    HTTPError
42      HTTPClientError
43        * 400 - HTTPBadRequest
44        * 401 - HTTPUnauthorized
45        * 402 - HTTPPaymentRequired
46        * 403 - HTTPForbidden
47        * 404 - HTTPNotFound
48        * 405 - HTTPMethodNotAllowed
49        * 406 - HTTPNotAcceptable
50        * 407 - HTTPProxyAuthenticationRequired
51        * 408 - HTTPRequestTimeout
52        * 409 - HTTPConfict
53        * 410 - HTTPGone
54        * 411 - HTTPLengthRequired
55        * 412 - HTTPPreconditionFailed
56        * 413 - HTTPRequestEntityTooLarge
57        * 414 - HTTPRequestURITooLong
58        * 415 - HTTPUnsupportedMediaType
59        * 416 - HTTPRequestRangeNotSatisfiable
60        * 417 - HTTPExpectationFailed
61        * 429 - HTTPTooManyRequests
62      HTTPServerError
63        * 500 - HTTPInternalServerError
64        * 501 - HTTPNotImplemented
65        * 502 - HTTPBadGateway
66        * 503 - HTTPServiceUnavailable
67        * 504 - HTTPGatewayTimeout
68        * 505 - HTTPVersionNotSupported
69
70References:
71
72.. [1] http://www.python.org/peps/pep-0333.html#error-handling
73.. [2] http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.5
74
75"""
76
77import six
78from paste.wsgilib import catch_errors_app
79from paste.response import has_header, header_value, replace_header
80from paste.request import resolve_relative_url
81from paste.util.quoting import strip_html, html_quote, no_quote, comment_quote
82
83SERVER_NAME = 'WSGI Server'
84TEMPLATE = """\
85<html>\r
86  <head><title>%(title)s</title></head>\r
87  <body>\r
88    <h1>%(title)s</h1>\r
89    <p>%(body)s</p>\r
90    <hr noshade>\r
91    <div align="right">%(server)s</div>\r
92  </body>\r
93</html>\r
94"""
95
96class HTTPException(Exception):
97    """
98    the HTTP exception base class
99
100    This encapsulates an HTTP response that interrupts normal application
101    flow; but one which is not necessarly an error condition. For
102    example, codes in the 300's are exceptions in that they interrupt
103    normal processing; however, they are not considered errors.
104
105    This class is complicated by 4 factors:
106
107      1. The content given to the exception may either be plain-text or
108         as html-text.
109
110      2. The template may want to have string-substitutions taken from
111         the current ``environ`` or values from incoming headers. This
112         is especially troublesome due to case sensitivity.
113
114      3. The final output may either be text/plain or text/html
115         mime-type as requested by the client application.
116
117      4. Each exception has a default explanation, but those who
118         raise exceptions may want to provide additional detail.
119
120    Attributes:
121
122       ``code``
123           the HTTP status code for the exception
124
125       ``title``
126           remainder of the status line (stuff after the code)
127
128       ``explanation``
129           a plain-text explanation of the error message that is
130           not subject to environment or header substitutions;
131           it is accessible in the template via %(explanation)s
132
133       ``detail``
134           a plain-text message customization that is not subject
135           to environment or header substitutions; accessible in
136           the template via %(detail)s
137
138       ``template``
139           a content fragment (in HTML) used for environment and
140           header substitution; the default template includes both
141           the explanation and further detail provided in the
142           message
143
144       ``required_headers``
145           a sequence of headers which are required for proper
146           construction of the exception
147
148    Parameters:
149
150       ``detail``
151         a plain-text override of the default ``detail``
152
153       ``headers``
154         a list of (k,v) header pairs
155
156       ``comment``
157         a plain-text additional information which is
158         usually stripped/hidden for end-users
159
160    To override the template (which is HTML content) or the plain-text
161    explanation, one must subclass the given exception; or customize it
162    after it has been created.  This particular breakdown of a message
163    into explanation, detail and template allows both the creation of
164    plain-text and html messages for various clients as well as
165    error-free substitution of environment variables and headers.
166    """
167
168    code = None
169    title = None
170    explanation = ''
171    detail = ''
172    comment = ''
173    template = "%(explanation)s\r\n<br/>%(detail)s\r\n<!-- %(comment)s -->"
174    required_headers = ()
175
176    def __init__(self, detail=None, headers=None, comment=None):
177        assert self.code, "Do not directly instantiate abstract exceptions."
178        assert isinstance(headers, (type(None), list)), (
179            "headers must be None or a list: %r"
180            % headers)
181        assert isinstance(detail, (type(None), six.binary_type, six.text_type)), (
182            "detail must be None or a string: %r" % detail)
183        assert isinstance(comment, (type(None), six.binary_type, six.text_type)), (
184            "comment must be None or a string: %r" % comment)
185        self.headers = headers or tuple()
186        for req in self.required_headers:
187            assert headers and has_header(headers, req), (
188                "Exception %s must be passed the header %r "
189                "(got headers: %r)"
190                % (self.__class__.__name__, req, headers))
191        if detail is not None:
192            self.detail = detail
193        if comment is not None:
194            self.comment = comment
195        Exception.__init__(self,"%s %s\n%s\n%s\n" % (
196            self.code, self.title, self.explanation, self.detail))
197
198    def make_body(self, environ, template, escfunc, comment_escfunc=None):
199        comment_escfunc = comment_escfunc or escfunc
200        args = {'explanation': escfunc(self.explanation),
201                'detail': escfunc(self.detail),
202                'comment': comment_escfunc(self.comment)}
203        if HTTPException.template != self.template:
204            for (k, v) in environ.items():
205                args[k] = escfunc(v)
206            if self.headers:
207                for (k, v) in self.headers:
208                    args[k.lower()] = escfunc(v)
209        if six.PY2:
210            for key, value in args.items():
211                if isinstance(value, six.text_type):
212                    args[key] = value.encode('utf8', 'xmlcharrefreplace')
213        return template % args
214
215    def plain(self, environ):
216        """ text/plain representation of the exception """
217        body = self.make_body(environ, strip_html(self.template), no_quote, comment_quote)
218        return ('%s %s\r\n%s\r\n' % (self.code, self.title, body))
219
220    def html(self, environ):
221        """ text/html representation of the exception """
222        body = self.make_body(environ, self.template, html_quote, comment_quote)
223        return TEMPLATE % {
224                   'title': self.title,
225                   'code': self.code,
226                   'server': SERVER_NAME,
227                   'body': body }
228
229    def prepare_content(self, environ):
230        if self.headers:
231            headers = list(self.headers)
232        else:
233            headers = []
234        if 'html' in environ.get('HTTP_ACCEPT','') or \
235            '*/*' in environ.get('HTTP_ACCEPT',''):
236            replace_header(headers, 'content-type', 'text/html')
237            content = self.html(environ)
238        else:
239            replace_header(headers, 'content-type', 'text/plain')
240            content = self.plain(environ)
241        if isinstance(content, six.text_type):
242            content = content.encode('utf8')
243            cur_content_type = (
244                header_value(headers, 'content-type')
245                or 'text/html')
246            replace_header(
247                headers, 'content-type',
248                cur_content_type + '; charset=utf8')
249        return headers, content
250
251    def response(self, environ):
252        from paste.wsgiwrappers import WSGIResponse
253        headers, content = self.prepare_content(environ)
254        resp = WSGIResponse(code=self.code, content=content)
255        resp.headers = resp.headers.fromlist(headers)
256        return resp
257
258    def wsgi_application(self, environ, start_response, exc_info=None):
259        """
260        This exception as a WSGI application
261        """
262        headers, content = self.prepare_content(environ)
263        start_response('%s %s' % (self.code, self.title),
264                       headers,
265                       exc_info)
266        return [content]
267
268    __call__ = wsgi_application
269
270    def __repr__(self):
271        return '<%s %s; code=%s>' % (self.__class__.__name__,
272                                     self.title, self.code)
273
274class HTTPError(HTTPException):
275    """
276    base class for status codes in the 400's and 500's
277
278    This is an exception which indicates that an error has occurred,
279    and that any work in progress should not be committed.  These are
280    typically results in the 400's and 500's.
281    """
282
283#
284# 3xx Redirection
285#
286#  This class of status code indicates that further action needs to be
287#  taken by the user agent in order to fulfill the request. The action
288#  required MAY be carried out by the user agent without interaction with
289#  the user if and only if the method used in the second request is GET or
290#  HEAD. A client SHOULD detect infinite redirection loops, since such
291#  loops generate network traffic for each redirection.
292#
293
294class HTTPRedirection(HTTPException):
295    """
296    base class for 300's status code (redirections)
297
298    This is an abstract base class for 3xx redirection.  It indicates
299    that further action needs to be taken by the user agent in order
300    to fulfill the request.  It does not necessarly signal an error
301    condition.
302    """
303
304class _HTTPMove(HTTPRedirection):
305    """
306    redirections which require a Location field
307
308    Since a 'Location' header is a required attribute of 301, 302, 303,
309    305 and 307 (but not 304), this base class provides the mechanics to
310    make this easy.  While this has the same parameters as HTTPException,
311    if a location is not provided in the headers; it is assumed that the
312    detail _is_ the location (this for backward compatibility, otherwise
313    we'd add a new attribute).
314    """
315    required_headers = ('location',)
316    explanation = 'The resource has been moved to'
317    template = (
318        '%(explanation)s <a href="%(location)s">%(location)s</a>;\r\n'
319        'you should be redirected automatically.\r\n'
320        '%(detail)s\r\n<!-- %(comment)s -->')
321
322    def __init__(self, detail=None, headers=None, comment=None):
323        assert isinstance(headers, (type(None), list))
324        headers = headers or []
325        location = header_value(headers,'location')
326        if not location:
327            location = detail
328            detail = ''
329            headers.append(('location', location))
330        assert location, ("HTTPRedirection specified neither a "
331                          "location in the headers nor did it "
332                          "provide a detail argument.")
333        HTTPRedirection.__init__(self, location, headers, comment)
334        if detail is not None:
335            self.detail = detail
336
337    def relative_redirect(cls, dest_uri, environ, detail=None, headers=None, comment=None):
338        """
339        Create a redirect object with the dest_uri, which may be relative,
340        considering it relative to the uri implied by the given environ.
341        """
342        location = resolve_relative_url(dest_uri, environ)
343        headers = headers or []
344        headers.append(('Location', location))
345        return cls(detail=detail, headers=headers, comment=comment)
346
347    relative_redirect = classmethod(relative_redirect)
348
349    def location(self):
350        for name, value in self.headers:
351            if name.lower() == 'location':
352                return value
353        else:
354            raise KeyError("No location set for %s" % self)
355
356class HTTPMultipleChoices(_HTTPMove):
357    code = 300
358    title = 'Multiple Choices'
359
360class HTTPMovedPermanently(_HTTPMove):
361    code = 301
362    title = 'Moved Permanently'
363
364class HTTPFound(_HTTPMove):
365    code = 302
366    title = 'Found'
367    explanation = 'The resource was found at'
368
369# This one is safe after a POST (the redirected location will be
370# retrieved with GET):
371class HTTPSeeOther(_HTTPMove):
372    code = 303
373    title = 'See Other'
374
375class HTTPNotModified(HTTPRedirection):
376    # @@: but not always (HTTP section 14.18.1)...?
377    # @@: Removed 'date' requirement, as its not required for an ETag
378    # @@: FIXME: This should require either an ETag or a date header
379    code = 304
380    title = 'Not Modified'
381    message = ''
382    # @@: should include date header, optionally other headers
383    # @@: should not return a content body
384    def plain(self, environ):
385        return ''
386    def html(self, environ):
387        """ text/html representation of the exception """
388        return ''
389
390class HTTPUseProxy(_HTTPMove):
391    # @@: OK, not a move, but looks a little like one
392    code = 305
393    title = 'Use Proxy'
394    explanation = (
395        'The resource must be accessed through a proxy '
396        'located at')
397
398class HTTPTemporaryRedirect(_HTTPMove):
399    code = 307
400    title = 'Temporary Redirect'
401
402#
403# 4xx Client Error
404#
405#  The 4xx class of status code is intended for cases in which the client
406#  seems to have erred. Except when responding to a HEAD request, the
407#  server SHOULD include an entity containing an explanation of the error
408#  situation, and whether it is a temporary or permanent condition. These
409#  status codes are applicable to any request method. User agents SHOULD
410#  display any included entity to the user.
411#
412
413class HTTPClientError(HTTPError):
414    """
415    base class for the 400's, where the client is in-error
416
417    This is an error condition in which the client is presumed to be
418    in-error.  This is an expected problem, and thus is not considered
419    a bug.  A server-side traceback is not warranted.  Unless specialized,
420    this is a '400 Bad Request'
421    """
422    code = 400
423    title = 'Bad Request'
424    explanation = ('The server could not comply with the request since\r\n'
425                   'it is either malformed or otherwise incorrect.\r\n')
426
427class HTTPBadRequest(HTTPClientError):
428    pass
429
430class HTTPUnauthorized(HTTPClientError):
431    code = 401
432    title = 'Unauthorized'
433    explanation = (
434        'This server could not verify that you are authorized to\r\n'
435        'access the document you requested.  Either you supplied the\r\n'
436        'wrong credentials (e.g., bad password), or your browser\r\n'
437        'does not understand how to supply the credentials required.\r\n')
438
439class HTTPPaymentRequired(HTTPClientError):
440    code = 402
441    title = 'Payment Required'
442    explanation = ('Access was denied for financial reasons.')
443
444class HTTPForbidden(HTTPClientError):
445    code = 403
446    title = 'Forbidden'
447    explanation = ('Access was denied to this resource.')
448
449class HTTPNotFound(HTTPClientError):
450    code = 404
451    title = 'Not Found'
452    explanation = ('The resource could not be found.')
453
454class HTTPMethodNotAllowed(HTTPClientError):
455    required_headers = ('allow',)
456    code = 405
457    title = 'Method Not Allowed'
458    # override template since we need an environment variable
459    template = ('The method %(REQUEST_METHOD)s is not allowed for '
460                'this resource.\r\n%(detail)s')
461
462class HTTPNotAcceptable(HTTPClientError):
463    code = 406
464    title = 'Not Acceptable'
465    # override template since we need an environment variable
466    template = ('The resource could not be generated that was '
467                'acceptable to your browser (content\r\nof type '
468                '%(HTTP_ACCEPT)s).\r\n%(detail)s')
469
470class HTTPProxyAuthenticationRequired(HTTPClientError):
471    code = 407
472    title = 'Proxy Authentication Required'
473    explanation = ('Authentication /w a local proxy is needed.')
474
475class HTTPRequestTimeout(HTTPClientError):
476    code = 408
477    title = 'Request Timeout'
478    explanation = ('The server has waited too long for the request to '
479                   'be sent by the client.')
480
481class HTTPConflict(HTTPClientError):
482    code = 409
483    title = 'Conflict'
484    explanation = ('There was a conflict when trying to complete '
485                   'your request.')
486
487class HTTPGone(HTTPClientError):
488    code = 410
489    title = 'Gone'
490    explanation = ('This resource is no longer available.  No forwarding '
491                   'address is given.')
492
493class HTTPLengthRequired(HTTPClientError):
494    code = 411
495    title = 'Length Required'
496    explanation = ('Content-Length header required.')
497
498class HTTPPreconditionFailed(HTTPClientError):
499    code = 412
500    title = 'Precondition Failed'
501    explanation = ('Request precondition failed.')
502
503class HTTPRequestEntityTooLarge(HTTPClientError):
504    code = 413
505    title = 'Request Entity Too Large'
506    explanation = ('The body of your request was too large for this server.')
507
508class HTTPRequestURITooLong(HTTPClientError):
509    code = 414
510    title = 'Request-URI Too Long'
511    explanation = ('The request URI was too long for this server.')
512
513class HTTPUnsupportedMediaType(HTTPClientError):
514    code = 415
515    title = 'Unsupported Media Type'
516    # override template since we need an environment variable
517    template = ('The request media type %(CONTENT_TYPE)s is not '
518                'supported by this server.\r\n%(detail)s')
519
520class HTTPRequestRangeNotSatisfiable(HTTPClientError):
521    code = 416
522    title = 'Request Range Not Satisfiable'
523    explanation = ('The Range requested is not available.')
524
525class HTTPExpectationFailed(HTTPClientError):
526    code = 417
527    title = 'Expectation Failed'
528    explanation = ('Expectation failed.')
529
530class HTTPTooManyRequests(HTTPClientError):
531    code = 429
532    title = 'Too Many Requests'
533    explanation = ('The client has sent too many requests to the server.')
534
535#
536# 5xx Server Error
537#
538#  Response status codes beginning with the digit "5" indicate cases in
539#  which the server is aware that it has erred or is incapable of
540#  performing the request. Except when responding to a HEAD request, the
541#  server SHOULD include an entity containing an explanation of the error
542#  situation, and whether it is a temporary or permanent condition. User
543#  agents SHOULD display any included entity to the user. These response
544#  codes are applicable to any request method.
545#
546
547class HTTPServerError(HTTPError):
548    """
549    base class for the 500's, where the server is in-error
550
551    This is an error condition in which the server is presumed to be
552    in-error.  This is usually unexpected, and thus requires a traceback;
553    ideally, opening a support ticket for the customer. Unless specialized,
554    this is a '500 Internal Server Error'
555    """
556    code = 500
557    title = 'Internal Server Error'
558    explanation = (
559      'The server has either erred or is incapable of performing\r\n'
560      'the requested operation.\r\n')
561
562class HTTPInternalServerError(HTTPServerError):
563    pass
564
565class HTTPNotImplemented(HTTPServerError):
566    code = 501
567    title = 'Not Implemented'
568    # override template since we need an environment variable
569    template = ('The request method %(REQUEST_METHOD)s is not implemented '
570                'for this server.\r\n%(detail)s')
571
572class HTTPBadGateway(HTTPServerError):
573    code = 502
574    title = 'Bad Gateway'
575    explanation = ('Bad gateway.')
576
577class HTTPServiceUnavailable(HTTPServerError):
578    code = 503
579    title = 'Service Unavailable'
580    explanation = ('The server is currently unavailable. '
581                   'Please try again at a later time.')
582
583class HTTPGatewayTimeout(HTTPServerError):
584    code = 504
585    title = 'Gateway Timeout'
586    explanation = ('The gateway has timed out.')
587
588class HTTPVersionNotSupported(HTTPServerError):
589    code = 505
590    title = 'HTTP Version Not Supported'
591    explanation = ('The HTTP version is not supported.')
592
593# abstract HTTP related exceptions
594__all__ = ['HTTPException', 'HTTPRedirection', 'HTTPError' ]
595
596_exceptions = {}
597for name, value in six.iteritems(dict(globals())):
598    if (isinstance(value, (type, six.class_types)) and
599        issubclass(value, HTTPException) and
600        value.code):
601        _exceptions[value.code] = value
602        __all__.append(name)
603
604def get_exception(code):
605    return _exceptions[code]
606
607############################################################
608## Middleware implementation:
609############################################################
610
611class HTTPExceptionHandler(object):
612    """
613    catches exceptions and turns them into proper HTTP responses
614
615    This middleware catches any exceptions (which are subclasses of
616    ``HTTPException``) and turns them into proper HTTP responses.
617    Note if the headers have already been sent, the stack trace is
618    always maintained as this indicates a programming error.
619
620    Note that you must raise the exception before returning the
621    app_iter, and you cannot use this with generator apps that don't
622    raise an exception until after their app_iter is iterated over.
623    """
624
625    def __init__(self, application, warning_level=None):
626        assert not warning_level or ( warning_level > 99 and
627                                      warning_level < 600)
628        if warning_level is not None:
629            import warnings
630            warnings.warn('The warning_level parameter is not used or supported',
631                          DeprecationWarning, 2)
632        self.warning_level = warning_level or 500
633        self.application = application
634
635    def __call__(self, environ, start_response):
636        environ['paste.httpexceptions'] = self
637        environ.setdefault('paste.expected_exceptions',
638                           []).append(HTTPException)
639        try:
640            return self.application(environ, start_response)
641        except HTTPException as exc:
642            return exc(environ, start_response)
643
644def middleware(*args, **kw):
645    import warnings
646    # deprecated 13 dec 2005
647    warnings.warn('httpexceptions.middleware is deprecated; use '
648                  'make_middleware or HTTPExceptionHandler instead',
649                  DeprecationWarning, 2)
650    return make_middleware(*args, **kw)
651
652def make_middleware(app, global_conf=None, warning_level=None):
653    """
654    ``httpexceptions`` middleware; this catches any
655    ``paste.httpexceptions.HTTPException`` exceptions (exceptions like
656    ``HTTPNotFound``, ``HTTPMovedPermanently``, etc) and turns them
657    into proper HTTP responses.
658
659    ``warning_level`` can be an integer corresponding to an HTTP code.
660    Any code over that value will be passed 'up' the chain, potentially
661    reported on by another piece of middleware.
662    """
663    if warning_level:
664        warning_level = int(warning_level)
665    return HTTPExceptionHandler(app, warning_level=warning_level)
666
667__all__.extend(['HTTPExceptionHandler', 'get_exception'])
668