2 # -- Content-Encoding: UTF-8 --
\r
4 Defines a request dispatcher, a HTTP request handler, a HTTP server and a
\r
7 :authors: Josh Marshall, Thomas Calmant
\r
8 :copyright: Copyright 2015, isandlaTech
\r
9 :license: Apache License 2.0
\r
14 Copyright 2015 isandlaTech
\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
20 http://www.apache.org/licenses/LICENSE-2.0
\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
30 __version_info__ = (0, 2, 5)
\r
31 __version__ = ".".join(str(x) for x in __version_info__)
\r
33 # Documentation strings format
\r
34 __docformat__ = "restructuredtext en"
\r
36 # ------------------------------------------------------------------------------
\r
38 from jsonrpclib import Fault
\r
39 import jsonrpclib.config
\r
40 import jsonrpclib.utils as utils
\r
41 import jsonrpclib.threadpool
\r
49 # Prepare the logger
\r
50 _logger = logging.getLogger(__name__)
\r
54 # pylint: disable=F0401,E0611
\r
55 import xmlrpc.server as xmlrpcserver
\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
68 # pylint: disable=C0103
\r
71 # ------------------------------------------------------------------------------
\r
74 def get_version(request):
\r
76 Computes the JSON-RPC version
\r
78 :param request: A request dictionary
\r
79 :return: The JSON-RPC version or None
\r
81 if 'jsonrpc' in request:
\r
83 elif 'id' in request:
\r
89 def validate_request(request, json_config):
\r
91 Validates the format of a request dictionary
\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
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
105 # Get the request ID
\r
106 rpcid = request.get('id', None)
\r
108 # Check request version
\r
109 version = get_version(request)
\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
116 # Default parameters: empty list
\r
117 request.setdefault('params', [])
\r
120 method = request.get('method', None)
\r
121 params = request.get('params')
\r
122 param_types = (utils.ListType, utils.DictType, utils.TupleType)
\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
135 # ------------------------------------------------------------------------------
\r
138 class NoMulticallResult(Exception):
\r
140 No result in multicall
\r
145 class SimpleJSONRPCDispatcher(xmlrpcserver.SimpleXMLRPCDispatcher, object):
\r
147 Mix-in class that dispatches JSON-RPC requests.
\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
153 def __init__(self, encoding=None, config=jsonrpclib.config.DEFAULT):
\r
155 Sets up the dispatcher with the given encoding.
\r
156 None values are allowed.
\r
158 xmlrpcserver.SimpleXMLRPCDispatcher.__init__(
\r
159 self, allow_none=True, encoding=encoding or "UTF-8")
\r
160 self.json_config = config
\r
162 # Notification thread pool
\r
163 self.__notification_pool = None
\r
165 def set_notification_pool(self, thread_pool):
\r
167 Sets the thread pool to use to handle notifications
\r
169 self.__notification_pool = thread_pool
\r
171 def _unmarshaled_dispatch(self, request, dispatch_method=None):
\r
173 Loads the request dictionary (unmarshaled), calls the method(s)
\r
174 accordingly and returns a JSON-RPC dictionary (not marshaled)
\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
180 :raise NoMulticallResult: No result in batch
\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
189 if isinstance(request, utils.ListType):
\r
190 # This SHOULD be a batch, by spec
\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
200 resp_entry = self._marshaled_single_dispatch(req_entry,
\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
211 # No non-None result
\r
212 _logger.error("No result in Multicall")
\r
213 raise NoMulticallResult("No result")
\r
219 result = validate_request(request, self.json_config)
\r
220 if isinstance(result, Fault):
\r
221 return result.dump()
\r
224 response = self._marshaled_single_dispatch(request,
\r
226 if isinstance(response, Fault):
\r
227 # pylint: disable=E1103
\r
228 return response.dump()
\r
232 def _marshaled_dispatch(self, data, dispatch_method=None, path=None):
\r
234 Parses the request data (marshaled), calls method(s) and returns a
\r
235 JSON string (marshaled)
\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
242 # Parse the request
\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
253 # Get the response dictionary
\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
260 # No result (notification)
\r
262 except NoMulticallResult:
\r
263 # Return an empty string (jsonrpclib internal behaviour)
\r
266 def _marshaled_single_dispatch(self, request, dispatch_method=None):
\r
268 Dispatches a single method call
\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
275 method = request.get('method')
\r
276 params = request.get('params')
\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
285 # Keep server configuration as is
\r
286 config = self.json_config
\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
296 self.__notification_pool.enqueue(self._dispatch,
\r
297 method, params, config)
\r
299 # Return immediately
\r
305 if dispatch_method is not None:
\r
306 response = dispatch_method(method, params)
\r
308 response = self._dispatch(method, params, config)
\r
309 except Exception as ex:
\r
311 fault = Fault(-32603, '{0}:{1}'.format(type(ex).__name__, ex),
\r
313 _logger.error("Error calling method %s: %s", method, fault)
\r
314 return fault.dump()
\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
321 # Prepare a JSON-RPC dictionary
\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
329 _logger.error("Error preparing JSON-RPC result: %s", fault)
\r
330 return fault.dump()
\r
332 def _dispatch(self, method, params, config=None):
\r
334 Default method resolver and caller
\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
341 config = config or self.json_config
\r
345 # Look into registered methods
\r
346 func = self.funcs[method]
\r
348 if self.instance is not None:
\r
349 # Try with the registered instance
\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
356 func = xmlrpcserver.resolve_dotted_attribute(
\r
357 self.instance, method, True)
\r
358 except AttributeError:
\r
362 if func is not None:
\r
365 if isinstance(params, utils.ListType):
\r
366 return func(*params)
\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
373 _logger.warning("Invalid call parameters: %s", fault)
\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
381 _logger.exception("Server-side exception: %s", fault)
\r
385 fault = Fault(-32601, 'Method {0} not supported.'.format(method),
\r
387 _logger.warning("Unknown method: %s", fault)
\r
390 # ------------------------------------------------------------------------------
\r
393 class SimpleJSONRPCRequestHandler(xmlrpcserver.SimpleXMLRPCRequestHandler):
\r
395 HTTP request handler.
\r
397 The server that receives the requests must have a json_config member,
\r
398 containing a JSONRPClib Config instance
\r
402 Handles POST requests
\r
404 if not self.is_rpc_path_valid():
\r
408 # Retrieve the configuration
\r
409 config = getattr(self.server, 'json_config', jsonrpclib.config.DEFAULT)
\r
412 # Read the request body
\r
413 max_chunk_size = 10 * 1024 * 1024
\r
414 size_remaining = int(self.headers["content-length"])
\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
421 chunks.append(utils.from_bytes(raw_chunk))
\r
422 size_remaining -= len(chunks[-1])
\r
423 data = ''.join(chunks)
\r
427 data = self.decode_request_content(data)
\r
429 # Unknown encoding, response has been sent
\r
431 except AttributeError:
\r
432 # Available since Python 2.7
\r
435 # Execute the method
\r
436 response = self.server._marshaled_dispatch(
\r
437 data, getattr(self, '_dispatch', None), self.path)
\r
439 # No exception: send a 200 OK
\r
440 self.send_response(200)
\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
451 if response is None:
\r
452 # Avoid to send None
\r
455 # Convert the response to the valid string format
\r
456 response = utils.to_bytes(response)
\r
459 self.send_header("Content-type", config.content_type)
\r
460 self.send_header("Content-length", str(len(response)))
\r
463 self.wfile.write(response)
\r
465 # ------------------------------------------------------------------------------
\r
468 class SimpleJSONRPCServer(socketserver.TCPServer, SimpleJSONRPCDispatcher):
\r
470 JSON-RPC server (and dispatcher)
\r
472 # This simplifies server restart after error
\r
473 allow_reuse_address = True
\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
481 Sets up the server and the dispatcher
\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
491 # Set up the dispatcher fields
\r
492 SimpleJSONRPCDispatcher.__init__(self, encoding, config)
\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
500 # Work on the request handler
\r
501 class RequestHandlerWrapper(requestHandler, object):
\r
503 Wraps the request handle to have access to the configuration
\r
505 def __init__(self, *args, **kwargs):
\r
507 Constructs the wrapper after having stored the configuration
\r
509 self.config = config
\r
510 super(RequestHandlerWrapper, self).__init__(*args, **kwargs)
\r
512 # Set up the server
\r
513 socketserver.TCPServer.__init__(self, addr, requestHandler,
\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
522 # ------------------------------------------------------------------------------
\r
525 class PooledJSONRPCServer(SimpleJSONRPCServer, socketserver.ThreadingMixIn):
\r
527 JSON-RPC server based on a thread pool
\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
534 Sets up the server and the dispatcher
\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
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
552 # Store the thread pool
\r
553 self.__request_pool = thread_pool
\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
560 def process_request(self, request, client_address):
\r
562 Handle a client request: queue it in the thread pool
\r
564 self.__request_pool.enqueue(self.process_request_thread,
\r
565 request, client_address)
\r
567 def server_close(self):
\r
569 Clean up the server
\r
571 SimpleJSONRPCServer.server_close(self)
\r
572 self.__request_pool.stop()
\r
574 # ------------------------------------------------------------------------------
\r
577 class CGIJSONRPCRequestHandler(SimpleJSONRPCDispatcher):
\r
579 JSON-RPC CGI handler (and dispatcher)
\r
581 def __init__(self, encoding=None, config=jsonrpclib.config.DEFAULT):
\r
583 Sets up the dispatcher
\r
585 :param encoding: Dispatcher encoding
\r
586 :param config: A JSONRPClib Config instance
\r
588 SimpleJSONRPCDispatcher.__init__(self, encoding, config)
\r
590 def handle_jsonrpc(self, request_text):
\r
592 Handle a JSON-RPC request
\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
602 handle_xmlrpc = handle_jsonrpc
\r