"""System handlers.
Requests are processed through a chain of handlers. This module contains
the "system" handlers. These are handlers that always run in a specific
order.
Most of the system handlers *always* run. They can't be turned off, but
you can swap in different implementations via settings. Take a look at
:file:`tangled/web/defaults.ini` to see how you would do this.
Some handlers are only enabled when certain settings are enabled or when
certain configuration takes place. For example, to enable CSRF
protection, the ``tangled.app.csrf.enabled`` setting needs to be set to
``True``. Another example: the static files handlers is only enabled
when at least one static directory has been mounted.
If an auth handler is enabled, it will run directly before any (other)
handlers added by the application developer.
All added handlers are called in the order they were added. The last
handler to run is always the :func:`main` handler; it calls into
application code (i.e., it calls a resource method to get data or
a response).
"""
import logging
import os
import pdb
import time
import traceback
from webob.exc import WSGIHTTPException, HTTPInternalServerError
from tangled.util import load_object
from . import csrf
from .abcs import AMountedResourceTree
from .events import NewRequest, ResourceFound, NewResponse
from .exc import DebugHTTPInternalServerError
from .representations import Representation
from .response import Response
log = logging.getLogger(__name__)
def exc_handler(app, request, next_handler):
try:
return next_handler(app, request)
except WSGIHTTPException as exc:
response = exc
except Exception as exc:
app.log_exc(request, exc)
if app.debug:
if app.settings.get('debug.pdb', False):
pdb.post_mortem(exc.__traceback__)
response = DebugHTTPInternalServerError(traceback.format_exc())
else:
response = HTTPInternalServerError()
if response.status_code < 400:
return response
main_handler = app.get_setting('handler.main')
main_handler = HandlerWrapper(main_handler, None)
return error_handler(app, request, main_handler, response)
[docs]def error_handler(app, request, main_handler, original_response):
"""Handle error response.
If an error resource is configured, its ``GET`` method will be
called to get the final response. This is accomplished by setting
the error resource as the resource for the request and then passing
the request back into the main handler.
If CORS is enabled, the main handler will be wrapped in the CORS
handler so that error responses will have the appropriate headers.
If no error resource is configured, the original error response will
be returned as is.
"""
try:
error_resource = app.get_setting('error_resource')
if error_resource:
resource = error_resource(app, request)
request.method = 'GET'
request.resource = resource
request.resource_method = 'GET'
request.response = original_response
del request.response_content_type
del request.resource_config
if app.get_setting('cors.enabled'):
handler = app.get_setting('handler.cors')
handler = HandlerWrapper(handler, main_handler)
else:
handler = main_handler
try:
return handler(app, request)
except WSGIHTTPException as exc:
app.log_exc(request, exc)
return exc
except Exception as exc:
app.log_exc(request, exc)
# If there's an exception in the error resource (or there's no error
# resource configured), the original exception response will be
# returned, which is better than nothing.
return original_response
[docs]def request_finished_handler(app, request, _):
"""Call request finished callbacks in exc handling context.
This calls the request finished callbacks in the same exception
handling context as the request. This way, if exceptions occur in
finished callbacks, they can be logged and displayed as usual.
.. note:: Finished callbacks are not called for static requests.
"""
if not getattr(request, 'is_static', False):
request._call_finished_callbacks(request.response)
return request.response
def static_files(app, request, next_handler):
prefix, rel_path = app._find_static_directory(request.path_info)
if prefix is not None:
request.is_static = True
environ = request.environ.copy()
environ['PATH_INFO'] = '/' + '/'.join(rel_path)
static_request = app.make_request(environ)
directory_app = app.get('static_directory', prefix)
return directory_app(static_request)
return next_handler(app, request)
[docs]def tweaker(app, request, next_handler):
"""Tweak the request based on special request parameters."""
specials = {
'$method': None,
'$accept': None,
}
for k in request.params:
if k in specials:
specials[k] = request.params[k]
for k in specials:
if k in request.GET:
del request.GET[k]
if k in request.POST:
del request.POST[k]
if specials['$method']:
method = specials['$method']
tunneled_methods = app.get_setting('tunnel_over_post')
if method == 'DELETE':
# Changing request.method to DELETE makes request.POST
# inaccessible.
if csrf.KEY in request.POST and csrf.HEADER not in request.headers:
request.headers[csrf.HEADER] = request.POST[csrf.KEY]
if request.method == 'POST' and method in tunneled_methods:
request.method = method
elif app.debug:
request.method = method
else:
request.abort(
400, detail="Can't tunnel {} over POST".format(method))
if specials['$accept']:
request.accept = specials['$accept']
elif app.settings['tangled.app.set_accept_from_ext']:
root, ext = os.path.splitext(request.path_info)
if ext:
repr_type = app.get(Representation, ext.lstrip('.'))
if repr_type is not None:
request.accept = repr_type.content_type
request.path_info = root
return next_handler(app, request)
def notifier(app, request, next_handler):
app.notify_subscribers(NewRequest, app, request)
response = next_handler(app, request)
app.notify_subscribers(NewResponse, app, request, response)
return response
[docs]def resource_finder(app, request, next_handler):
"""Find resource for request.
Sets ``request.resource`` and notifies :class:`ResourceFound`
subscribers.
If a resource isn't found, a 404 response is immediatley returned.
If a resource is found but doesn't respond to the request's method,
a ``405 Method Not Allowed`` response is returned.
"""
tree = app.get_required(AMountedResourceTree)
match = tree.find_for_request(request)
if match is None:
request.abort(404)
mounted_resource = match.mounted_resource
urlvars = match.urlvars
if mounted_resource.add_slash and not request.path_info.endswith('/'):
request.path_info += '/'
request.abort(303, location=request.url)
resource = mounted_resource.factory(
app, request, mounted_resource.name, urlvars)
method_name = mounted_resource.method_name or request.method
if not hasattr(resource, method_name):
request.abort(405)
request.resource = resource
request.resource_method = method_name
app.notify_subscribers(ResourceFound, app, request, mounted_resource)
return next_handler(app, request)
# csrf handler will be inserted here if enabled
# auth handler will be inserted here if enabled
# non-system handlers will be inserted here
# cors handler will be inserted here if enabled
[docs]def timer(app, request, next_handler):
"""Log time taken to handle a request."""
start_time = time.time()
response = next_handler(app, request)
elapsed_time = (time.time() - start_time) * 1000
log.debug('Request to {} took {:.2f}ms'.format(request.url, elapsed_time))
return response
[docs]def main(app, request, _):
"""Get data from resource method and return response.
If the resource method returns a response object (an instance of
:class:`Response`), that response will be returned without further
processing.
If the status of ``request.response`` has been set to 3xx (either
via @config or in the body of the resource method) AND the resource
method returns no data, the response will will be returned as is
without further processing.
Otherwise, a representation will be generated based on the request's
Accept header (unless a representation type has been set via
@config, in which case that type will be used instead of doing
a best match guess).
If the representation returns a response object as its content, that
response will be returned without further processing.
Otherwise, `request.response` will be updated according to the
representation type (the response's content_type, charset, and body
are set from the representation).
"""
method = getattr(request.resource, request.resource_method)
data = method()
if isinstance(data, Response):
return data
response = request.response
if 300 <= response.status_code < 400 and data is None:
return response
info = request.resource_config
log.debug(info)
if info.type:
differentiator = info.type
else:
differentiator = request.response_content_type
repr_type = app.get_required(Representation, differentiator)
kwargs = info.representation_args
representation = repr_type(app, request, data, **kwargs)
if isinstance(representation.content, Response):
return representation.content
response.content_type = representation.content_type
response.charset = representation.encoding
response.text = representation.content
return response
class HandlerWrapper:
# An internal class used for wrapping handler callables.
def __init__(self, callable_, next_handler):
if isinstance(callable_, str):
callable_ = load_object(callable_)
self.callable_ = callable_
self.next = next_handler
def __call__(self, app, request):
response = self.callable_(app, request, self.next)
if response is None:
raise ValueError('Handler returned None')
return response