f7a7b65242b0d0fa5c0d6feacc601a6d399d9da9
[trex.git] /
1 #!/usr/bin/python\r
2 # -- Content-Encoding: UTF-8 --\r
3 """\r
4 Defines a request dispatcher, a HTTP request handler, a HTTP server and a\r
5 CGI request handler.\r
6 \r
7 :authors: Josh Marshall, Thomas Calmant\r
8 :copyright: Copyright 2015, isandlaTech\r
9 :license: Apache License 2.0\r
10 :version: 0.2.5\r
11 \r
12 ..\r
13 \r
14     Copyright 2015 isandlaTech\r
15 \r
16     Licensed under the Apache License, Version 2.0 (the "License");\r
17     you may not use this file except in compliance with the License.\r
18     You may obtain a copy of the License at\r
19 \r
20         http://www.apache.org/licenses/LICENSE-2.0\r
21 \r
22     Unless required by applicable law or agreed to in writing, software\r
23     distributed under the License is distributed on an "AS IS" BASIS,\r
24     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r
25     See the License for the specific language governing permissions and\r
26     limitations under the License.\r
27 """\r
28 \r
29 # Module version\r
30 __version_info__ = (0, 2, 5)\r
31 __version__ = ".".join(str(x) for x in __version_info__)\r
32 \r
33 # Documentation strings format\r
34 __docformat__ = "restructuredtext en"\r
35 \r
36 # ------------------------------------------------------------------------------\r
37 # Local modules\r
38 from jsonrpclib import Fault\r
39 import jsonrpclib.config\r
40 import jsonrpclib.utils as utils\r
41 import jsonrpclib.threadpool\r
42 \r
43 # Standard library\r
44 import logging\r
45 import socket\r
46 import sys\r
47 import traceback\r
48 \r
49 # Prepare the logger\r
50 _logger = logging.getLogger(__name__)\r
51 \r
52 try:\r
53     # Python 3\r
54     # pylint: disable=F0401,E0611\r
55     import xmlrpc.server as xmlrpcserver\r
56     import socketserver\r
57 except (ImportError, AttributeError):\r
58     # Python 2 or IronPython\r
59     # pylint: disable=F0401,E0611\r
60     import SimpleXMLRPCServer as xmlrpcserver\r
61     import SocketServer as socketserver\r
62 \r
63 try:\r
64     # Windows\r
65     import fcntl\r
66 except ImportError:\r
67     # Other systems\r
68     # pylint: disable=C0103\r
69     fcntl = None\r
70 \r
71 # ------------------------------------------------------------------------------\r
72 \r
73 \r
74 def get_version(request):\r
75     """\r
76     Computes the JSON-RPC version\r
77 \r
78     :param request: A request dictionary\r
79     :return: The JSON-RPC version or None\r
80     """\r
81     if 'jsonrpc' in request:\r
82         return 2.0\r
83     elif 'id' in request:\r
84         return 1.0\r
85 \r
86     return None\r
87 \r
88 \r
89 def validate_request(request, json_config):\r
90     """\r
91     Validates the format of a request dictionary\r
92 \r
93     :param request: A request dictionary\r
94     :param json_config: A JSONRPClib Config instance\r
95     :return: True if the dictionary is valid, else a Fault object\r
96     """\r
97     if not isinstance(request, utils.DictType):\r
98         # Invalid request type\r
99         fault = Fault(-32600, 'Request must be a dict, not {0}'\r
100                       .format(type(request).__name__),\r
101                       config=json_config)\r
102         _logger.warning("Invalid request content: %s", fault)\r
103         return fault\r
104 \r
105     # Get the request ID\r
106     rpcid = request.get('id', None)\r
107 \r
108     # Check request version\r
109     version = get_version(request)\r
110     if not version:\r
111         fault = Fault(-32600, 'Request {0} invalid.'.format(request),\r
112                       rpcid=rpcid, config=json_config)\r
113         _logger.warning("No version in request: %s", fault)\r
114         return fault\r
115 \r
116     # Default parameters: empty list\r
117     request.setdefault('params', [])\r
118 \r
119     # Check parameters\r
120     method = request.get('method', None)\r
121     params = request.get('params')\r
122     param_types = (utils.ListType, utils.DictType, utils.TupleType)\r
123 \r
124     if not method or not isinstance(method, utils.string_types) or \\r
125             not isinstance(params, param_types):\r
126         # Invalid type of method name or parameters\r
127         fault = Fault(-32600, 'Invalid request parameters or method.',\r
128                       rpcid=rpcid, config=json_config)\r
129         _logger.warning("Invalid request content: %s", fault)\r
130         return fault\r
131 \r
132     # Valid request\r
133     return True\r
134 \r
135 # ------------------------------------------------------------------------------\r
136 \r
137 \r
138 class NoMulticallResult(Exception):\r
139     """\r
140     No result in multicall\r
141     """\r
142     pass\r
143 \r
144 \r
145 class SimpleJSONRPCDispatcher(xmlrpcserver.SimpleXMLRPCDispatcher, object):\r
146     """\r
147     Mix-in class that dispatches JSON-RPC requests.\r
148 \r
149     This class is used to register JSON-RPC method handlers\r
150     and then to dispatch them. This class doesn't need to be\r
151     instanced directly when used by SimpleJSONRPCServer.\r
152     """\r
153     def __init__(self, encoding=None, config=jsonrpclib.config.DEFAULT):\r
154         """\r
155         Sets up the dispatcher with the given encoding.\r
156         None values are allowed.\r
157         """\r
158         xmlrpcserver.SimpleXMLRPCDispatcher.__init__(\r
159             self, allow_none=True, encoding=encoding or "UTF-8")\r
160         self.json_config = config\r
161 \r
162         # Notification thread pool\r
163         self.__notification_pool = None\r
164 \r
165     def set_notification_pool(self, thread_pool):\r
166         """\r
167         Sets the thread pool to use to handle notifications\r
168         """\r
169         self.__notification_pool = thread_pool\r
170 \r
171     def _unmarshaled_dispatch(self, request, dispatch_method=None):\r
172         """\r
173         Loads the request dictionary (unmarshaled), calls the method(s)\r
174         accordingly and returns a JSON-RPC dictionary (not marshaled)\r
175 \r
176         :param request: JSON-RPC request dictionary (or list of)\r
177         :param dispatch_method: Custom dispatch method (for method resolution)\r
178         :return: A JSON-RPC dictionary (or an array of) or None if the request\r
179                  was a notification\r
180         :raise NoMulticallResult: No result in batch\r
181         """\r
182         if not request:\r
183             # Invalid request dictionary\r
184             fault = Fault(-32600, 'Request invalid -- no request data.',\r
185                           config=self.json_config)\r
186             _logger.warning("Invalid request: %s", fault)\r
187             return fault.dump()\r
188 \r
189         if isinstance(request, utils.ListType):\r
190             # This SHOULD be a batch, by spec\r
191             responses = []\r
192             for req_entry in request:\r
193                 # Validate the request\r
194                 result = validate_request(req_entry, self.json_config)\r
195                 if isinstance(result, Fault):\r
196                     responses.append(result.dump())\r
197                     continue\r
198 \r
199                 # Call the method\r
200                 resp_entry = self._marshaled_single_dispatch(req_entry,\r
201                                                              dispatch_method)\r
202 \r
203                 # Store its result\r
204                 if isinstance(resp_entry, Fault):\r
205                     # pylint: disable=E1103\r
206                     responses.append(resp_entry.dump())\r
207                 elif resp_entry is not None:\r
208                     responses.append(resp_entry)\r
209 \r
210             if not responses:\r
211                 # No non-None result\r
212                 _logger.error("No result in Multicall")\r
213                 raise NoMulticallResult("No result")\r
214 \r
215             return responses\r
216 \r
217         else:\r
218             # Single call\r
219             result = validate_request(request, self.json_config)\r
220             if isinstance(result, Fault):\r
221                 return result.dump()\r
222 \r
223             # Call the method\r
224             response = self._marshaled_single_dispatch(request,\r
225                                                        dispatch_method)\r
226             if isinstance(response, Fault):\r
227                 # pylint: disable=E1103\r
228                 return response.dump()\r
229 \r
230             return response\r
231 \r
232     def _marshaled_dispatch(self, data, dispatch_method=None, path=None):\r
233         """\r
234         Parses the request data (marshaled), calls method(s) and returns a\r
235         JSON string (marshaled)\r
236 \r
237         :param data: A JSON request string\r
238         :param dispatch_method: Custom dispatch method (for method resolution)\r
239         :param path: Unused parameter, to keep compatibility with xmlrpclib\r
240         :return: A JSON-RPC response string (marshaled)\r
241         """\r
242         # Parse the request\r
243         try:\r
244             request = jsonrpclib.loads(data, self.json_config)\r
245         except Exception as ex:\r
246             # Parsing/loading error\r
247             fault = Fault(-32700, 'Request {0} invalid. ({1}:{2})'\r
248                           .format(data, type(ex).__name__, ex),\r
249                           config=self.json_config)\r
250             _logger.warning("Error parsing request: %s", fault)\r
251             return fault.response()\r
252 \r
253         # Get the response dictionary\r
254         try:\r
255             response = self._unmarshaled_dispatch(request, dispatch_method)\r
256             if response is not None:\r
257                 # Compute the string representation of the dictionary/list\r
258                 return jsonrpclib.jdumps(response, self.encoding)\r
259             else:\r
260                 # No result (notification)\r
261                 return ''\r
262         except NoMulticallResult:\r
263             # Return an empty string (jsonrpclib internal behaviour)\r
264             return ''\r
265 \r
266     def _marshaled_single_dispatch(self, request, dispatch_method=None):\r
267         """\r
268         Dispatches a single method call\r
269 \r
270         :param request: A validated request dictionary\r
271         :param dispatch_method: Custom dispatch method (for method resolution)\r
272         :return: A JSON-RPC response dictionary, or None if it was a\r
273                  notification request\r
274         """\r
275         method = request.get('method')\r
276         params = request.get('params')\r
277 \r
278         # Prepare a request-specific configuration\r
279         if 'jsonrpc' not in request and self.json_config.version >= 2:\r
280             # JSON-RPC 1.0 request on a JSON-RPC 2.0\r
281             # => compatibility needed\r
282             config = self.json_config.copy()\r
283             config.version = 1.0\r
284         else:\r
285             # Keep server configuration as is\r
286             config = self.json_config\r
287 \r
288         # Test if this is a notification request\r
289         is_notification = 'id' not in request or request['id'] in (None, '')\r
290         if is_notification and self.__notification_pool is not None:\r
291             # Use the thread pool for notifications\r
292             if dispatch_method is not None:\r
293                 self.__notification_pool.enqueue(dispatch_method,\r
294                                                  method, params)\r
295             else:\r
296                 self.__notification_pool.enqueue(self._dispatch,\r
297                                                  method, params, config)\r
298 \r
299             # Return immediately\r
300             return None\r
301         else:\r
302             # Synchronous call\r
303             try:\r
304                 # Call the method\r
305                 if dispatch_method is not None:\r
306                     response = dispatch_method(method, params)\r
307                 else:\r
308                     response = self._dispatch(method, params, config)\r
309             except Exception as ex:\r
310                 # Return a fault\r
311                 fault = Fault(-32603, '{0}:{1}'.format(type(ex).__name__, ex),\r
312                               config=config)\r
313                 _logger.error("Error calling method %s: %s", method, fault)\r
314                 return fault.dump()\r
315 \r
316             if is_notification:\r
317                 # It's a notification, no result needed\r
318                 # Do not use 'not id' as it might be the integer 0\r
319                 return None\r
320 \r
321         # Prepare a JSON-RPC dictionary\r
322         try:\r
323             return jsonrpclib.dump(response, rpcid=request['id'],\r
324                                    is_response=True, config=config)\r
325         except Exception as ex:\r
326             # JSON conversion exception\r
327             fault = Fault(-32603, '{0}:{1}'.format(type(ex).__name__, ex),\r
328                           config=config)\r
329             _logger.error("Error preparing JSON-RPC result: %s", fault)\r
330             return fault.dump()\r
331 \r
332     def _dispatch(self, method, params, config=None):\r
333         """\r
334         Default method resolver and caller\r
335 \r
336         :param method: Name of the method to call\r
337         :param params: List of arguments to give to the method\r
338         :param config: Request-specific configuration\r
339         :return: The result of the method\r
340         """\r
341         config = config or self.json_config\r
342 \r
343         func = None\r
344         try:\r
345             # Look into registered methods\r
346             func = self.funcs[method]\r
347         except KeyError:\r
348             if self.instance is not None:\r
349                 # Try with the registered instance\r
350                 try:\r
351                     # Instance has a custom dispatcher\r
352                     return getattr(self.instance, '_dispatch')(method, params)\r
353                 except AttributeError:\r
354                     # Resolve the method name in the instance\r
355                     try:\r
356                         func = xmlrpcserver.resolve_dotted_attribute(\r
357                             self.instance, method, True)\r
358                     except AttributeError:\r
359                         # Unknown method\r
360                         pass\r
361 \r
362         if func is not None:\r
363             try:\r
364                 # Call the method\r
365                 if isinstance(params, utils.ListType):\r
366                     return func(*params)\r
367                 else:\r
368                     return func(**params)\r
369             except TypeError as ex:\r
370                 # Maybe the parameters are wrong\r
371                 fault = Fault(-32602, 'Invalid parameters: {0}'.format(ex),\r
372                               config=config)\r
373                 _logger.warning("Invalid call parameters: %s", fault)\r
374                 return fault\r
375             except:\r
376                 # Method exception\r
377                 err_lines = traceback.format_exc().splitlines()\r
378                 trace_string = '{0} | {1}'.format(err_lines[-3], err_lines[-1])\r
379                 fault = Fault(-32603, 'Server error: {0}'.format(trace_string),\r
380                               config=config)\r
381                 _logger.exception("Server-side exception: %s", fault)\r
382                 return fault\r
383         else:\r
384             # Unknown method\r
385             fault = Fault(-32601, 'Method {0} not supported.'.format(method),\r
386                           config=config)\r
387             _logger.warning("Unknown method: %s", fault)\r
388             return fault\r
389 \r
390 # ------------------------------------------------------------------------------\r
391 \r
392 \r
393 class SimpleJSONRPCRequestHandler(xmlrpcserver.SimpleXMLRPCRequestHandler):\r
394     """\r
395     HTTP request handler.\r
396 \r
397     The server that receives the requests must have a json_config member,\r
398     containing a JSONRPClib Config instance\r
399     """\r
400     def do_POST(self):\r
401         """\r
402         Handles POST requests\r
403         """\r
404         if not self.is_rpc_path_valid():\r
405             self.report_404()\r
406             return\r
407 \r
408         # Retrieve the configuration\r
409         config = getattr(self.server, 'json_config', jsonrpclib.config.DEFAULT)\r
410 \r
411         try:\r
412             # Read the request body\r
413             max_chunk_size = 10 * 1024 * 1024\r
414             size_remaining = int(self.headers["content-length"])\r
415             chunks = []\r
416             while size_remaining:\r
417                 chunk_size = min(size_remaining, max_chunk_size)\r
418                 raw_chunk = self.rfile.read(chunk_size)\r
419                 if not raw_chunk:\r
420                     break\r
421                 chunks.append(utils.from_bytes(raw_chunk))\r
422                 size_remaining -= len(chunks[-1])\r
423             data = ''.join(chunks)\r
424 \r
425             try:\r
426                 # Decode content\r
427                 data = self.decode_request_content(data)\r
428                 if data is None:\r
429                     # Unknown encoding, response has been sent\r
430                     return\r
431             except AttributeError:\r
432                 # Available since Python 2.7\r
433                 pass\r
434 \r
435             # Execute the method\r
436             response = self.server._marshaled_dispatch(\r
437                 data, getattr(self, '_dispatch', None), self.path)\r
438 \r
439             # No exception: send a 200 OK\r
440             self.send_response(200)\r
441         except:\r
442             # Exception: send 500 Server Error\r
443             self.send_response(500)\r
444             err_lines = traceback.format_exc().splitlines()\r
445             trace_string = '{0} | {1}'.format(err_lines[-3], err_lines[-1])\r
446             fault = jsonrpclib.Fault(-32603, 'Server error: {0}'\r
447                                      .format(trace_string), config=config)\r
448             _logger.exception("Server-side error: %s", fault)\r
449             response = fault.response()\r
450 \r
451         if response is None:\r
452             # Avoid to send None\r
453             response = ''\r
454 \r
455         # Convert the response to the valid string format\r
456         response = utils.to_bytes(response)\r
457 \r
458         # Send it\r
459         self.send_header("Content-type", config.content_type)\r
460         self.send_header("Content-length", str(len(response)))\r
461         self.end_headers()\r
462         if response:\r
463             self.wfile.write(response)\r
464 \r
465 # ------------------------------------------------------------------------------\r
466 \r
467 \r
468 class SimpleJSONRPCServer(socketserver.TCPServer, SimpleJSONRPCDispatcher):\r
469     """\r
470     JSON-RPC server (and dispatcher)\r
471     """\r
472     # This simplifies server restart after error\r
473     allow_reuse_address = True\r
474 \r
475     # pylint: disable=C0103\r
476     def __init__(self, addr, requestHandler=SimpleJSONRPCRequestHandler,\r
477                  logRequests=True, encoding=None, bind_and_activate=True,\r
478                  address_family=socket.AF_INET,\r
479                  config=jsonrpclib.config.DEFAULT):\r
480         """\r
481         Sets up the server and the dispatcher\r
482 \r
483         :param addr: The server listening address\r
484         :param requestHandler: Custom request handler\r
485         :param logRequests: Flag to(de)activate requests logging\r
486         :param encoding: The dispatcher request encoding\r
487         :param bind_and_activate: If True, starts the server immediately\r
488         :param address_family: The server listening address family\r
489         :param config: A JSONRPClib Config instance\r
490         """\r
491         # Set up the dispatcher fields\r
492         SimpleJSONRPCDispatcher.__init__(self, encoding, config)\r
493 \r
494         # Prepare the server configuration\r
495         # logRequests is used by SimpleXMLRPCRequestHandler\r
496         self.logRequests = logRequests\r
497         self.address_family = address_family\r
498         self.json_config = config\r
499 \r
500         # Work on the request handler\r
501         class RequestHandlerWrapper(requestHandler, object):\r
502             """\r
503             Wraps the request handle to have access to the configuration\r
504             """\r
505             def __init__(self, *args, **kwargs):\r
506                 """\r
507                 Constructs the wrapper after having stored the configuration\r
508                 """\r
509                 self.config = config\r
510                 super(RequestHandlerWrapper, self).__init__(*args, **kwargs)\r
511 \r
512         # Set up the server\r
513         socketserver.TCPServer.__init__(self, addr, requestHandler,\r
514                                         bind_and_activate)\r
515 \r
516         # Windows-specific\r
517         if fcntl is not None and hasattr(fcntl, 'FD_CLOEXEC'):\r
518             flags = fcntl.fcntl(self.fileno(), fcntl.F_GETFD)\r
519             flags |= fcntl.FD_CLOEXEC\r
520             fcntl.fcntl(self.fileno(), fcntl.F_SETFD, flags)\r
521 \r
522 # ------------------------------------------------------------------------------\r
523 \r
524 \r
525 class PooledJSONRPCServer(SimpleJSONRPCServer, socketserver.ThreadingMixIn):\r
526     """\r
527     JSON-RPC server based on a thread pool\r
528     """\r
529     def __init__(self, addr, requestHandler=SimpleJSONRPCRequestHandler,\r
530                  logRequests=True, encoding=None, bind_and_activate=True,\r
531                  address_family=socket.AF_INET,\r
532                  config=jsonrpclib.config.DEFAULT, thread_pool=None):\r
533         """\r
534         Sets up the server and the dispatcher\r
535 \r
536         :param addr: The server listening address\r
537         :param requestHandler: Custom request handler\r
538         :param logRequests: Flag to(de)activate requests logging\r
539         :param encoding: The dispatcher request encoding\r
540         :param bind_and_activate: If True, starts the server immediately\r
541         :param address_family: The server listening address family\r
542         :param config: A JSONRPClib Config instance\r
543         :param thread_pool: A ThreadPool object. The pool must be started.\r
544         """\r
545         # Normalize the thread pool\r
546         if thread_pool is None:\r
547             # Start a thread pool with  30 threads max, 0 thread min\r
548             thread_pool = jsonrpclib.threadpool.ThreadPool(\r
549                 30, 0, logname="PooledJSONRPCServer")\r
550             thread_pool.start()\r
551 \r
552         # Store the thread pool\r
553         self.__request_pool = thread_pool\r
554 \r
555         # Prepare the server\r
556         SimpleJSONRPCServer.__init__(self, addr, requestHandler, logRequests,\r
557                                      encoding, bind_and_activate,\r
558                                      address_family, config)\r
559 \r
560     def process_request(self, request, client_address):\r
561         """\r
562         Handle a client request: queue it in the thread pool\r
563         """\r
564         self.__request_pool.enqueue(self.process_request_thread,\r
565                                     request, client_address)\r
566 \r
567     def server_close(self):\r
568         """\r
569         Clean up the server\r
570         """\r
571         SimpleJSONRPCServer.server_close(self)\r
572         self.__request_pool.stop()\r
573 \r
574 # ------------------------------------------------------------------------------\r
575 \r
576 \r
577 class CGIJSONRPCRequestHandler(SimpleJSONRPCDispatcher):\r
578     """\r
579     JSON-RPC CGI handler (and dispatcher)\r
580     """\r
581     def __init__(self, encoding=None, config=jsonrpclib.config.DEFAULT):\r
582         """\r
583         Sets up the dispatcher\r
584 \r
585         :param encoding: Dispatcher encoding\r
586         :param config: A JSONRPClib Config instance\r
587         """\r
588         SimpleJSONRPCDispatcher.__init__(self, encoding, config)\r
589 \r
590     def handle_jsonrpc(self, request_text):\r
591         """\r
592         Handle a JSON-RPC request\r
593         """\r
594         response = self._marshaled_dispatch(request_text)\r
595         sys.stdout.write('Content-Type: {0}\r\n'\r
596                          .format(self.json_config.content_type))\r
597         sys.stdout.write('Content-Length: {0:d}\r\n'.format(len(response)))\r
598         sys.stdout.write('\r\n')\r
599         sys.stdout.write(response)\r
600 \r
601     # XML-RPC alias\r
602     handle_xmlrpc = handle_jsonrpc\r