1# coding: utf-8
2
3from django.core.exceptions import ImproperlyConfigured
4from django.http.response import HttpResponse, HttpResponseForbidden
5from django.utils.decorators import method_decorator
6from django.utils.module_loading import import_string
7from django.views.decorators.csrf import csrf_exempt
8from django.views.generic.base import View, TemplateView
9
10from modernrpc.conf import settings
11from modernrpc.core import ALL, registry
12from modernrpc.exceptions import RPCInternalError, RPCException, AuthenticationFailed
13from modernrpc.utils import ensure_sequence, get_modernrpc_logger
14
15logger = get_modernrpc_logger(__name__)
16
17
18class RPCEntryPoint(TemplateView):
19    """
20    This is the main entry point class. It inherits standard Django View class.
21    """
22
23    template_name = 'modernrpc/doc_index.html'
24
25    entry_point = settings.MODERNRPC_DEFAULT_ENTRYPOINT_NAME
26    protocol = ALL
27    enable_doc = False
28    enable_rpc = True
29
30    def __init__(self, **kwargs):
31        super(RPCEntryPoint, self).__init__(**kwargs)
32
33        if not self.get_handler_classes():
34            raise ImproperlyConfigured("At least 1 handler must be instantiated.")
35
36        # Copy static list http_method_names locally (in instance), so we can dynamically customize it
37        self.http_method_names = list(View.http_method_names)
38
39        # Customize allowed HTTP methods name to forbid access to GET when this EntryPoint
40        # must not display docs...
41        if not self.enable_doc:
42            self.http_method_names.remove('get')
43
44        # ... and also forbid access to POST when this EntryPoint must not support RPC request (docs only view)
45        if not self.enable_rpc:
46            self.http_method_names.remove('post')
47
48        logger.debug('RPC entry point "{}" initialized'.format(self.entry_point))
49
50    # This disable CSRF validation for POST requests
51    @method_decorator(csrf_exempt)
52    def dispatch(self, request, *args, **kwargs):
53        """Overrides the default dispatch method, to disable CSRF validation on POST requests. This
54        is mandatory to ensure RPC calls wil be correctly handled"""
55        return super(RPCEntryPoint, self).dispatch(request, *args, **kwargs)
56
57    def get_handler_classes(self):
58        """Return the list of handlers to use when receiving RPC requests."""
59
60        handler_classes = [import_string(handler_cls) for handler_cls in settings.MODERNRPC_HANDLERS]
61
62        if self.protocol == ALL:
63            return handler_classes
64        else:
65            return [cls for cls in handler_classes if cls.protocol in ensure_sequence(self.protocol)]
66
67    def post(self, request, *args, **kwargs):
68        """
69        Handle a XML-RPC or JSON-RPC request.
70
71        :param request: Incoming request
72        :param args: Additional arguments
73        :param kwargs: Additional named arguments
74        :return: A HttpResponse containing XML-RPC or JSON-RPC response, depending on the incoming request
75        """
76
77        logger.debug('RPC request received...')
78
79        for handler_cls in self.get_handler_classes():
80
81            handler = handler_cls(request, self.entry_point)
82
83            try:
84                if not handler.can_handle():
85                    continue
86
87                logger.debug('Request will be handled by {}'.format(handler_cls.__name__))
88
89                result = handler.process_request()
90
91                return handler.result_success(result)
92
93            except AuthenticationFailed as e:
94                # Customize HttpResponse instance used when AuthenticationFailed was raised
95                logger.warning(e)
96                return handler.result_error(e, HttpResponseForbidden)
97
98            except RPCException as e:
99                logger.warning('RPC exception: {}'.format(e), exc_info=settings.MODERNRPC_LOG_EXCEPTIONS)
100                return handler.result_error(e)
101
102            except Exception as e:
103                logger.error('Exception raised from a RPC method: "{}"'.format(e),
104                             exc_info=settings.MODERNRPC_LOG_EXCEPTIONS)
105                return handler.result_error(RPCInternalError(str(e)))
106
107        logger.error('Unable to handle incoming request.')
108
109        return HttpResponse('Unable to handle your request. Please ensure you called the right entry point. If not, '
110                            'this could be a server error.')
111
112    def get_context_data(self, **kwargs):
113        """Update context data with list of RPC methods of the current entry point.
114        Will be used to display methods documentation page"""
115        kwargs.update({
116            'methods': registry.get_all_methods(self.entry_point, sort_methods=True),
117        })
118        return super(RPCEntryPoint, self).get_context_data(**kwargs)
119