You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

613 lines
22 KiB

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