|
|
- #!/usr/bin/python
- # -- Content-Encoding: UTF-8 --
- """
- Defines a request dispatcher, a HTTP request handler, a HTTP server and a
- CGI request handler.
-
- :authors: Josh Marshall, Thomas Calmant
- :copyright: Copyright 2018, Thomas Calmant
- :license: Apache License 2.0
- :version: 0.3.2
-
- ..
-
- Copyright 2018 Thomas Calmant
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
- """
-
- # Standard library
- import logging
- import socket
- import sys
- import traceback
-
- try:
- # Python 3
- # pylint: disable=F0401,E0611
- import xmlrpc.server as xmlrpcserver
- # Make sure the module is complete.
- # The "future" package under python2.7 provides an incomplete
- # variant of this package.
- SimpleXMLRPCDispatcher = xmlrpcserver.SimpleXMLRPCDispatcher
- SimpleXMLRPCRequestHandler = xmlrpcserver.SimpleXMLRPCRequestHandler
- resolve_dotted_attribute = xmlrpcserver.resolve_dotted_attribute
- import socketserver
- except (ImportError, AttributeError):
- # Python 2 or IronPython
- # pylint: disable=F0401,E0611
- import SimpleXMLRPCServer as xmlrpcserver
- SimpleXMLRPCDispatcher = xmlrpcserver.SimpleXMLRPCDispatcher
- SimpleXMLRPCRequestHandler = xmlrpcserver.SimpleXMLRPCRequestHandler
- resolve_dotted_attribute = xmlrpcserver.resolve_dotted_attribute
- import SocketServer as socketserver
-
- try:
- # Windows
- import fcntl
- except ImportError:
- # Other systems
- # pylint: disable=C0103
- fcntl = None
-
- # Local modules
- from jsonrpclib import Fault
- import jsonrpclib.config
- import jsonrpclib.utils as utils
- import jsonrpclib.threadpool
-
- # ------------------------------------------------------------------------------
-
- # Module version
- __version_info__ = (0, 3, 2)
- __version__ = ".".join(str(x) for x in __version_info__)
-
- # Documentation strings format
- __docformat__ = "restructuredtext en"
-
- # Prepare the logger
- _logger = logging.getLogger(__name__)
-
- # ------------------------------------------------------------------------------
-
-
- def get_version(request):
- """
- Computes the JSON-RPC version
-
- :param request: A request dictionary
- :return: The JSON-RPC version or None
- """
- if 'jsonrpc' in request:
- return 2.0
- elif 'id' in request:
- return 1.0
-
- return None
-
-
- def validate_request(request, json_config):
- """
- Validates the format of a request dictionary
-
- :param request: A request dictionary
- :param json_config: A JSONRPClib Config instance
- :return: True if the dictionary is valid, else a Fault object
- """
- if not isinstance(request, utils.DictType):
- # Invalid request type
- fault = Fault(-32600, 'Request must be a dict, not {0}'
- .format(type(request).__name__),
- config=json_config)
- _logger.warning("Invalid request content: %s", fault)
- return fault
-
- # Get the request ID
- rpcid = request.get('id', None)
-
- # Check request version
- version = get_version(request)
- if not version:
- fault = Fault(-32600, 'Request {0} invalid.'.format(request),
- rpcid=rpcid, config=json_config)
- _logger.warning("No version in request: %s", fault)
- return fault
-
- # Default parameters: empty list
- request.setdefault('params', [])
-
- # Check parameters
- method = request.get('method', None)
- params = request.get('params')
- param_types = (utils.ListType, utils.DictType, utils.TupleType)
-
- if not method or not isinstance(method, utils.STRING_TYPES) or \
- not isinstance(params, param_types):
- # Invalid type of method name or parameters
- fault = Fault(-32600, 'Invalid request parameters or method.',
- rpcid=rpcid, config=json_config)
- _logger.warning("Invalid request content: %s", fault)
- return fault
-
- # Valid request
- return True
-
- # ------------------------------------------------------------------------------
-
-
- class NoMulticallResult(Exception):
- """
- No result in multicall
- """
- pass
-
-
- class SimpleJSONRPCDispatcher(SimpleXMLRPCDispatcher, object):
- """
- Mix-in class that dispatches JSON-RPC requests.
-
- This class is used to register JSON-RPC method handlers
- and then to dispatch them. This class doesn't need to be
- instanced directly when used by SimpleJSONRPCServer.
- """
- def __init__(self, encoding=None, config=jsonrpclib.config.DEFAULT):
- """
- Sets up the dispatcher with the given encoding.
- None values are allowed.
- """
- SimpleXMLRPCDispatcher.__init__(
- self, allow_none=True, encoding=encoding or "UTF-8")
- self.json_config = config
-
- # Notification thread pool
- self.__notification_pool = None
-
- def set_notification_pool(self, thread_pool):
- """
- Sets the thread pool to use to handle notifications
- """
- self.__notification_pool = thread_pool
-
- def _unmarshaled_dispatch(self, request, dispatch_method=None):
- """
- Loads the request dictionary (unmarshaled), calls the method(s)
- accordingly and returns a JSON-RPC dictionary (not marshaled)
-
- :param request: JSON-RPC request dictionary (or list of)
- :param dispatch_method: Custom dispatch method (for method resolution)
- :return: A JSON-RPC dictionary (or an array of) or None if the request
- was a notification
- :raise NoMulticallResult: No result in batch
- """
- if not request:
- # Invalid request dictionary
- fault = Fault(-32600, 'Request invalid -- no request data.',
- config=self.json_config)
- _logger.warning("Invalid request: %s", fault)
- return fault.dump()
-
- if isinstance(request, utils.ListType):
- # This SHOULD be a batch, by spec
- responses = []
- for req_entry in request:
- # Validate the request
- result = validate_request(req_entry, self.json_config)
- if isinstance(result, Fault):
- responses.append(result.dump())
- continue
-
- # Call the method
- resp_entry = self._marshaled_single_dispatch(
- req_entry, dispatch_method)
-
- # Store its result
- if isinstance(resp_entry, Fault):
- # pylint: disable=E1103
- responses.append(resp_entry.dump())
- elif resp_entry is not None:
- responses.append(resp_entry)
-
- if not responses:
- # No non-None result
- _logger.error("No result in Multicall")
- raise NoMulticallResult("No result")
-
- return responses
-
- else:
- # Single call
- result = validate_request(request, self.json_config)
- if isinstance(result, Fault):
- return result.dump()
-
- # Call the method
- response = self._marshaled_single_dispatch(
- request, dispatch_method)
- if isinstance(response, Fault):
- # pylint: disable=E1103
- return response.dump()
-
- return response
-
- def _marshaled_dispatch(self, data, dispatch_method=None, path=None):
- """
- Parses the request data (marshaled), calls method(s) and returns a
- JSON string (marshaled)
-
- :param data: A JSON request string
- :param dispatch_method: Custom dispatch method (for method resolution)
- :param path: Unused parameter, to keep compatibility with xmlrpclib
- :return: A JSON-RPC response string (marshaled)
- """
- # Parse the request
- try:
- request = jsonrpclib.loads(data, self.json_config)
- except Exception as ex:
- # Parsing/loading error
- fault = Fault(-32700, 'Request {0} invalid. ({1}:{2})'
- .format(data, type(ex).__name__, ex),
- config=self.json_config)
- _logger.warning("Error parsing request: %s", fault)
- return fault.response()
-
- # Get the response dictionary
- try:
- response = self._unmarshaled_dispatch(request, dispatch_method)
- if response is not None:
- # Compute the string representation of the dictionary/list
- return jsonrpclib.jdumps(response, self.encoding)
- else:
- # No result (notification)
- return ''
- except NoMulticallResult:
- # Return an empty string (jsonrpclib internal behaviour)
- return ''
-
- def _marshaled_single_dispatch(self, request, dispatch_method=None):
- """
- Dispatches a single method call
-
- :param request: A validated request dictionary
- :param dispatch_method: Custom dispatch method (for method resolution)
- :return: A JSON-RPC response dictionary, or None if it was a
- notification request
- """
- method = request.get('method')
- params = request.get('params')
-
- # Prepare a request-specific configuration
- if 'jsonrpc' not in request and self.json_config.version >= 2:
- # JSON-RPC 1.0 request on a JSON-RPC 2.0
- # => compatibility needed
- config = self.json_config.copy()
- config.version = 1.0
- else:
- # Keep server configuration as is
- config = self.json_config
-
- # Test if this is a notification request
- is_notification = 'id' not in request or request['id'] in (None, '')
- if is_notification and self.__notification_pool is not None:
- # Use the thread pool for notifications
- if dispatch_method is not None:
- self.__notification_pool.enqueue(
- dispatch_method, method, params)
- else:
- self.__notification_pool.enqueue(
- self._dispatch, method, params, config)
-
- # Return immediately
- return None
- else:
- # Synchronous call
- try:
- # Call the method
- if dispatch_method is not None:
- response = dispatch_method(method, params)
- else:
- response = self._dispatch(method, params, config)
- except Exception as ex:
- # Return a fault
- fault = Fault(-32603, '{0}:{1}'.format(type(ex).__name__, ex),
- config=config)
- _logger.error("Error calling method %s: %s", method, fault)
- return fault.dump()
-
- if is_notification:
- # It's a notification, no result needed
- # Do not use 'not id' as it might be the integer 0
- return None
-
- # Prepare a JSON-RPC dictionary
- try:
- return jsonrpclib.dump(response, rpcid=request['id'],
- is_response=True, config=config)
- except Exception as ex:
- # JSON conversion exception
- fault = Fault(-32603, '{0}:{1}'.format(type(ex).__name__, ex),
- config=config)
- _logger.error("Error preparing JSON-RPC result: %s", fault)
- return fault.dump()
-
- def _dispatch(self, method, params, config=None):
- """
- Default method resolver and caller
-
- :param method: Name of the method to call
- :param params: List of arguments to give to the method
- :param config: Request-specific configuration
- :return: The result of the method
- """
- config = config or self.json_config
-
- func = None
- try:
- # Look into registered methods
- func = self.funcs[method]
- except KeyError:
- if self.instance is not None:
- # Try with the registered instance
- try:
- # Instance has a custom dispatcher
- return getattr(self.instance, '_dispatch')(method, params)
- except AttributeError:
- # Resolve the method name in the instance
- try:
- func = resolve_dotted_attribute(
- self.instance, method, True)
- except AttributeError:
- # Unknown method
- pass
-
- if func is not None:
- try:
- # Call the method
- if isinstance(params, utils.ListType):
- return func(*params)
- else:
- return func(**params)
- except TypeError as ex:
- # Maybe the parameters are wrong
- fault = Fault(-32602, 'Invalid parameters: {0}'.format(ex),
- config=config)
- _logger.warning("Invalid call parameters: %s", fault)
- return fault
- except:
- # Method exception
- err_lines = traceback.format_exception(*sys.exc_info())
- trace_string = '{0} | {1}'.format(err_lines[-2].splitlines()[0].strip(), err_lines[-1])
- fault = Fault(-32603, 'Server error: {0}'.format(trace_string),
- config=config)
- _logger.exception("Server-side exception: %s", fault)
- return fault
- else:
- # Unknown method
- fault = Fault(-32601, 'Method {0} not supported.'.format(method),
- config=config)
- _logger.warning("Unknown method: %s", fault)
- return fault
-
- # ------------------------------------------------------------------------------
-
-
- class SimpleJSONRPCRequestHandler(SimpleXMLRPCRequestHandler):
- """
- HTTP request handler.
-
- The server that receives the requests must have a json_config member,
- containing a JSONRPClib Config instance
- """
- def do_POST(self):
- """
- Handles POST requests
- """
- if not self.is_rpc_path_valid():
- self.report_404()
- return
-
- # Retrieve the configuration
- config = getattr(self.server, 'json_config', jsonrpclib.config.DEFAULT)
-
- try:
- # Read the request body
- max_chunk_size = 10 * 1024 * 1024
- size_remaining = int(self.headers["content-length"])
- chunks = []
- while size_remaining:
- chunk_size = min(size_remaining, max_chunk_size)
- raw_chunk = self.rfile.read(chunk_size)
- if not raw_chunk:
- break
- chunks.append(utils.from_bytes(raw_chunk))
- size_remaining -= len(chunks[-1])
- data = ''.join(chunks)
-
- try:
- # Decode content
- data = self.decode_request_content(data)
- if data is None:
- # Unknown encoding, response has been sent
- return
- except AttributeError:
- # Available since Python 2.7
- pass
-
- # Execute the method
- response = self.server._marshaled_dispatch(
- data, getattr(self, '_dispatch', None), self.path)
-
- # No exception: send a 200 OK
- self.send_response(200)
- except:
- # Exception: send 500 Server Error
- self.send_response(500)
- err_lines = traceback.format_exception(*sys.exc_info())
- trace_string = '{0} | {1}'.format(err_lines[-2].splitlines()[0].strip(), err_lines[-1])
- fault = jsonrpclib.Fault(-32603, 'Server error: {0}'
- .format(trace_string), config=config)
- _logger.exception("Server-side error: %s", fault)
- response = fault.response()
-
- if response is None:
- # Avoid to send None
- response = ''
-
- # Convert the response to the valid string format
- response = utils.to_bytes(response)
-
- # Send it
- self.send_header("Content-type", config.content_type)
- self.send_header("Content-length", str(len(response)))
- self.end_headers()
- if response:
- self.wfile.write(response)
-
- # ------------------------------------------------------------------------------
-
-
- class SimpleJSONRPCServer(socketserver.TCPServer, SimpleJSONRPCDispatcher):
- """
- JSON-RPC server (and dispatcher)
- """
- # This simplifies server restart after error
- allow_reuse_address = True
-
- # pylint: disable=C0103
- def __init__(self, addr, requestHandler=SimpleJSONRPCRequestHandler,
- logRequests=True, encoding=None, bind_and_activate=True,
- address_family=socket.AF_INET,
- config=jsonrpclib.config.DEFAULT):
- """
- Sets up the server and the dispatcher
-
- :param addr: The server listening address
- :param requestHandler: Custom request handler
- :param logRequests: Flag to(de)activate requests logging
- :param encoding: The dispatcher request encoding
- :param bind_and_activate: If True, starts the server immediately
- :param address_family: The server listening address family
- :param config: A JSONRPClib Config instance
- """
- # Set up the dispatcher fields
- SimpleJSONRPCDispatcher.__init__(self, encoding, config)
-
- # Prepare the server configuration
- # logRequests is used by SimpleXMLRPCRequestHandler
- self.logRequests = logRequests
- self.address_family = address_family
- self.json_config = config
-
- # Work on the request handler
- class RequestHandlerWrapper(requestHandler, object):
- """
- Wraps the request handle to have access to the configuration
- """
- def __init__(self, *args, **kwargs):
- """
- Constructs the wrapper after having stored the configuration
- """
- self.config = config
- super(RequestHandlerWrapper, self).__init__(*args, **kwargs)
-
- # Set up the server
- socketserver.TCPServer.__init__(self, addr, requestHandler,
- bind_and_activate)
-
- # Windows-specific
- if fcntl is not None and hasattr(fcntl, 'FD_CLOEXEC'):
- flags = fcntl.fcntl(self.fileno(), fcntl.F_GETFD)
- flags |= fcntl.FD_CLOEXEC
- fcntl.fcntl(self.fileno(), fcntl.F_SETFD, flags)
-
- # ------------------------------------------------------------------------------
-
-
- class PooledJSONRPCServer(SimpleJSONRPCServer, socketserver.ThreadingMixIn):
- """
- JSON-RPC server based on a thread pool
- """
- def __init__(self, addr, requestHandler=SimpleJSONRPCRequestHandler,
- logRequests=True, encoding=None, bind_and_activate=True,
- address_family=socket.AF_INET,
- config=jsonrpclib.config.DEFAULT, thread_pool=None):
- """
- Sets up the server and the dispatcher
-
- :param addr: The server listening address
- :param requestHandler: Custom request handler
- :param logRequests: Flag to(de)activate requests logging
- :param encoding: The dispatcher request encoding
- :param bind_and_activate: If True, starts the server immediately
- :param address_family: The server listening address family
- :param config: A JSONRPClib Config instance
- :param thread_pool: A ThreadPool object. The pool must be started.
- """
- # Normalize the thread pool
- if thread_pool is None:
- # Start a thread pool with 30 threads max, 0 thread min
- thread_pool = jsonrpclib.threadpool.ThreadPool(
- 30, 0, logname="PooledJSONRPCServer")
- thread_pool.start()
-
- # Store the thread pool
- self.__request_pool = thread_pool
-
- # Prepare the server
- SimpleJSONRPCServer.__init__(self, addr, requestHandler, logRequests,
- encoding, bind_and_activate,
- address_family, config)
-
- def process_request(self, request, client_address):
- """
- Handle a client request: queue it in the thread pool
- """
- self.__request_pool.enqueue(self.process_request_thread,
- request, client_address)
-
- def server_close(self):
- """
- Clean up the server
- """
- SimpleJSONRPCServer.shutdown(self)
- SimpleJSONRPCServer.server_close(self)
- self.__request_pool.stop()
-
- # ------------------------------------------------------------------------------
-
-
- class CGIJSONRPCRequestHandler(SimpleJSONRPCDispatcher):
- """
- JSON-RPC CGI handler (and dispatcher)
- """
- def __init__(self, encoding=None, config=jsonrpclib.config.DEFAULT):
- """
- Sets up the dispatcher
-
- :param encoding: Dispatcher encoding
- :param config: A JSONRPClib Config instance
- """
- SimpleJSONRPCDispatcher.__init__(self, encoding, config)
-
- def handle_jsonrpc(self, request_text):
- """
- Handle a JSON-RPC request
- """
- response = self._marshaled_dispatch(request_text)
- sys.stdout.write('Content-Type: {0}\r\n'
- .format(self.json_config.content_type))
- sys.stdout.write('Content-Length: {0:d}\r\n'.format(len(response)))
- sys.stdout.write('\r\n')
- sys.stdout.write(response)
-
- # XML-RPC alias
- handle_xmlrpc = handle_jsonrpc
|