1"""\
2RPC request handler Django.  Exposed RPC interface functions should be
3defined in rpc_interface.py.
4"""
5
6__author__ = 'showard@google.com (Steve Howard)'
7
8import traceback, pydoc, re, urllib, logging, logging.handlers, inspect
9from autotest_lib.frontend.afe.json_rpc import serviceHandler
10from autotest_lib.frontend.afe import models, rpc_utils
11from autotest_lib.client.common_lib import global_config
12from autotest_lib.frontend.afe import rpcserver_logging
13
14LOGGING_REGEXPS = [r'.*add_.*',
15                   r'delete_.*',
16                   r'.*remove_.*',
17                   r'modify_.*',
18                   r'create.*',
19                   r'set_.*']
20FULL_REGEXP = '(' + '|'.join(LOGGING_REGEXPS) + ')'
21COMPILED_REGEXP = re.compile(FULL_REGEXP)
22
23
24def should_log_message(name):
25    return COMPILED_REGEXP.match(name)
26
27
28class RpcMethodHolder(object):
29    'Dummy class to hold RPC interface methods as attributes.'
30
31
32class RpcHandler(object):
33    def __init__(self, rpc_interface_modules, document_module=None):
34        self._rpc_methods = RpcMethodHolder()
35        self._dispatcher = serviceHandler.ServiceHandler(self._rpc_methods)
36
37        # store all methods from interface modules
38        for module in rpc_interface_modules:
39            self._grab_methods_from(module)
40
41        # get documentation for rpc_interface we can send back to the
42        # user
43        if document_module is None:
44            document_module = rpc_interface_modules[0]
45        self.html_doc = pydoc.html.document(document_module)
46
47
48    def get_rpc_documentation(self):
49        return rpc_utils.raw_http_response(self.html_doc)
50
51
52    def raw_request_data(self, request):
53        if request.method == 'POST':
54            return request.raw_post_data
55        return urllib.unquote(request.META['QUERY_STRING'])
56
57
58    def execute_request(self, json_request):
59        return self._dispatcher.handleRequest(json_request)
60
61
62    def decode_request(self, json_request):
63        return self._dispatcher.translateRequest(json_request)
64
65
66    def dispatch_request(self, decoded_request):
67        return self._dispatcher.dispatchRequest(decoded_request)
68
69
70    def log_request(self, user, decoded_request, decoded_result,
71                    remote_ip, log_all=False):
72        if log_all or should_log_message(decoded_request['method']):
73            msg = '%s| %s:%s %s'  % (remote_ip, decoded_request['method'],
74                                     user, decoded_request['params'])
75            if decoded_result['err']:
76                msg += '\n' + decoded_result['err_traceback']
77                rpcserver_logging.rpc_logger.error(msg)
78            else:
79                rpcserver_logging.rpc_logger.info(msg)
80
81
82    def encode_result(self, results):
83        return self._dispatcher.translateResult(results)
84
85
86    def handle_rpc_request(self, request):
87        remote_ip = self._get_remote_ip(request)
88        user = models.User.current_user()
89        json_request = self.raw_request_data(request)
90        decoded_request = self.decode_request(json_request)
91
92        decoded_request['remote_ip'] = remote_ip
93        decoded_result = self.dispatch_request(decoded_request)
94        result = self.encode_result(decoded_result)
95        if rpcserver_logging.LOGGING_ENABLED:
96            self.log_request(user, decoded_request, decoded_result,
97                             remote_ip)
98        return rpc_utils.raw_http_response(result)
99
100
101    def handle_jsonp_rpc_request(self, request):
102        request_data = request.GET['request']
103        callback_name = request.GET['callback']
104        # callback_name must be a simple identifier
105        assert re.search(r'^\w+$', callback_name)
106
107        result = self.execute_request(request_data)
108        padded_result = '%s(%s)' % (callback_name, result)
109        return rpc_utils.raw_http_response(padded_result,
110                                           content_type='text/javascript')
111
112
113    @staticmethod
114    def _allow_keyword_args(f):
115        """\
116        Decorator to allow a function to take keyword args even though
117        the RPC layer doesn't support that.  The decorated function
118        assumes its last argument is a dictionary of keyword args and
119        passes them to the original function as keyword args.
120        """
121        def new_fn(*args):
122            assert args
123            keyword_args = args[-1]
124            args = args[:-1]
125            return f(*args, **keyword_args)
126        new_fn.func_name = f.func_name
127        return new_fn
128
129
130    def _grab_methods_from(self, module):
131        for name in dir(module):
132            if name.startswith('_'):
133                continue
134            attribute = getattr(module, name)
135            if not inspect.isfunction(attribute):
136                continue
137            decorated_function = RpcHandler._allow_keyword_args(attribute)
138            setattr(self._rpc_methods, name, decorated_function)
139
140
141    def _get_remote_ip(self, request):
142        """Get the ip address of a RPC caller.
143
144        Returns the IP of the request, accounting for the possibility of
145        being behind a proxy.
146        If a Django server is behind a proxy, request.META["REMOTE_ADDR"] will
147        return the proxy server's IP, not the client's IP.
148        The proxy server would provide the client's IP in the
149        HTTP_X_FORWARDED_FOR header.
150
151        @param request: django.core.handlers.wsgi.WSGIRequest object.
152
153        @return: IP address of remote host as a string.
154                 Empty string if the IP cannot be found.
155        """
156        remote = request.META.get('HTTP_X_FORWARDED_FOR', None)
157        if remote:
158            # X_FORWARDED_FOR returns client1, proxy1, proxy2,...
159            remote = remote.split(',')[0].strip()
160        else:
161            remote = request.META.get('REMOTE_ADDR', '')
162        return remote
163