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.

1286 lines
38 KiB

4 years ago
  1. #!/usr/bin/python
  2. # -- Content-Encoding: UTF-8 --
  3. """
  4. ============================
  5. JSONRPC Library (jsonrpclib)
  6. ============================
  7. This library is a JSON-RPC v.2 (proposed) implementation which
  8. follows the xmlrpclib API for portability between clients. It
  9. uses the same Server / ServerProxy, loads, dumps, etc. syntax,
  10. while providing features not present in XML-RPC like:
  11. * Keyword arguments
  12. * Notifications
  13. * Versioning
  14. * Batches and batch notifications
  15. Eventually, I'll add a SimpleXMLRPCServer compatible library,
  16. and other things to tie the thing off nicely. :)
  17. For a quick-start, just open a console and type the following,
  18. replacing the server address, method, and parameters
  19. appropriately.
  20. >>> import jsonrpclib
  21. >>> server = jsonrpclib.Server('http://localhost:8181')
  22. >>> server.add(5, 6)
  23. 11
  24. >>> server._notify.add(5, 6)
  25. >>> batch = jsonrpclib.MultiCall(server)
  26. >>> batch.add(3, 50)
  27. >>> batch.add(2, 3)
  28. >>> batch._notify.add(3, 5)
  29. >>> batch()
  30. [53, 5]
  31. See https://github.com/tcalmant/jsonrpclib for more info.
  32. :authors: Josh Marshall, Thomas Calmant
  33. :copyright: Copyright 2018, Thomas Calmant
  34. :license: Apache License 2.0
  35. :version: 0.3.2
  36. ..
  37. Copyright 2018 Thomas Calmant
  38. Licensed under the Apache License, Version 2.0 (the "License");
  39. you may not use this file except in compliance with the License.
  40. You may obtain a copy of the License at
  41. http://www.apache.org/licenses/LICENSE-2.0
  42. Unless required by applicable law or agreed to in writing, software
  43. distributed under the License is distributed on an "AS IS" BASIS,
  44. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  45. See the License for the specific language governing permissions and
  46. limitations under the License.
  47. """
  48. # Standard library
  49. import contextlib
  50. import logging
  51. import sys
  52. import uuid
  53. try:
  54. # Python 3
  55. # pylint: disable=F0401,E0611
  56. from urllib.parse import splittype, splithost
  57. from xmlrpc.client import Transport as XMLTransport
  58. from xmlrpc.client import SafeTransport as XMLSafeTransport
  59. from xmlrpc.client import ServerProxy as XMLServerProxy
  60. from xmlrpc.client import _Method as XML_Method
  61. except ImportError:
  62. # Python 2
  63. # pylint: disable=F0401,E0611
  64. from urllib import splittype, splithost
  65. from xmlrpclib import Transport as XMLTransport
  66. from xmlrpclib import SafeTransport as XMLSafeTransport
  67. from xmlrpclib import ServerProxy as XMLServerProxy
  68. from xmlrpclib import _Method as XML_Method
  69. try:
  70. # Check GZip support
  71. import gzip
  72. except ImportError:
  73. # Python can be built without zlib/gzip support
  74. # pylint: disable=C0103
  75. gzip = None
  76. # Library includes
  77. import jsonrpclib.config
  78. import jsonrpclib.jsonclass as jsonclass
  79. import jsonrpclib.utils as utils
  80. # ------------------------------------------------------------------------------
  81. # Module version
  82. __version_info__ = (0, 3, 2)
  83. __version__ = ".".join(str(x) for x in __version_info__)
  84. # Documentation strings format
  85. __docformat__ = "restructuredtext en"
  86. # Create the logger
  87. _logger = logging.getLogger(__name__)
  88. # ------------------------------------------------------------------------------
  89. # JSON library import
  90. try:
  91. # pylint: disable=F0401,E0611
  92. # Using cjson
  93. import cjson
  94. _logger.debug("Using cjson as JSON library")
  95. # Declare cjson methods
  96. def jdumps(obj, encoding='utf-8'):
  97. """
  98. Serializes ``obj`` to a JSON formatted string, using cjson.
  99. """
  100. return cjson.encode(obj)
  101. def jloads(json_string):
  102. """
  103. Deserializes ``json_string`` (a string containing a JSON document)
  104. to a Python object, using cjson.
  105. """
  106. return cjson.decode(json_string)
  107. except ImportError:
  108. # pylint: disable=F0401,E0611
  109. # Use json or simplejson
  110. try:
  111. import json
  112. _logger.debug("Using json as JSON library")
  113. except ImportError:
  114. try:
  115. import simplejson as json
  116. _logger.debug("Using simplejson as JSON library")
  117. except ImportError:
  118. _logger.error("No supported JSON library found")
  119. raise ImportError('You must have the cjson, json, or simplejson '
  120. 'module(s) available.')
  121. # Declare json methods
  122. if sys.version_info[0] < 3:
  123. def jdumps(obj, encoding='utf-8'):
  124. """
  125. Serializes ``obj`` to a JSON formatted string.
  126. """
  127. # Python 2 (explicit encoding)
  128. return json.dumps(obj, encoding=encoding)
  129. else:
  130. # Python 3
  131. def jdumps(obj, encoding='utf-8'):
  132. """
  133. Serializes ``obj`` to a JSON formatted string.
  134. """
  135. # Python 3 (the encoding parameter has been removed)
  136. return json.dumps(obj)
  137. def jloads(json_string):
  138. """
  139. Deserializes ``json_string`` (a string containing a JSON document)
  140. to a Python object.
  141. """
  142. return json.loads(json_string)
  143. # ------------------------------------------------------------------------------
  144. # XMLRPClib re-implementations
  145. class ProtocolError(Exception):
  146. """
  147. JSON-RPC error
  148. ProtocolError.args[0] can be:
  149. * an error message (string)
  150. * a (code, message) tuple
  151. """
  152. pass
  153. class AppError(ProtocolError):
  154. """
  155. Application error: the error code is not in the pre-defined ones
  156. AppError.args[0][0]: Error code
  157. AppError.args[0][1]: Error message or trace
  158. AppError.args[0][2]: Associated data
  159. """
  160. def data(self):
  161. """
  162. Retrieves the value found in the 'data' entry of the error, or None
  163. :return: The data associated to the error, or None
  164. """
  165. return self.args[0][2]
  166. class JSONParser(object):
  167. """
  168. Default JSON parser
  169. """
  170. def __init__(self, target):
  171. """
  172. Associates the target loader to the parser
  173. :param target: a JSONTarget instance
  174. """
  175. self.target = target
  176. def feed(self, data):
  177. """
  178. Feeds the associated target with the given data
  179. """
  180. self.target.feed(data)
  181. @staticmethod
  182. def close():
  183. """
  184. Does nothing
  185. """
  186. pass
  187. class JSONTarget(object):
  188. """
  189. Unmarshalls stream data to a string
  190. """
  191. def __init__(self):
  192. """
  193. Sets up the unmarshaller
  194. """
  195. self.data = []
  196. def feed(self, data):
  197. """
  198. Stores the given raw data into a buffer
  199. """
  200. # Store raw data as it might not contain whole wide-character
  201. self.data.append(data)
  202. def close(self):
  203. """
  204. Unmarshalls the buffered data
  205. """
  206. if not self.data:
  207. return ''
  208. else:
  209. # Use type to have a valid join (str vs. bytes)
  210. data = type(self.data[0])().join(self.data)
  211. try:
  212. # Convert the whole final string
  213. data = utils.from_bytes(data)
  214. except:
  215. # Try a pass-through
  216. pass
  217. return data
  218. class TransportMixIn(object):
  219. """ Just extends the XML-RPC transport where necessary. """
  220. # for Python 2.7 support
  221. _connection = None
  222. # List of non-overridable headers
  223. # Use the configuration to change the content-type
  224. readonly_headers = ('content-length', 'content-type')
  225. def __init__(self, config=jsonrpclib.config.DEFAULT, context=None):
  226. """
  227. Sets up the transport
  228. :param config: A JSONRPClib Config instance
  229. """
  230. # Store the configuration
  231. self._config = config
  232. # Store the SSL context
  233. self.context = context
  234. # Set up the user agent
  235. self.user_agent = config.user_agent
  236. # Additional headers: list of dictionaries
  237. self.additional_headers = []
  238. # Avoid a pep-8 error
  239. self.accept_gzip_encoding = True
  240. self.verbose = False
  241. def push_headers(self, headers):
  242. """
  243. Adds a dictionary of headers to the additional headers list
  244. :param headers: A dictionary
  245. """
  246. self.additional_headers.append(headers)
  247. def pop_headers(self, headers):
  248. """
  249. Removes the given dictionary from the additional headers list.
  250. Also validates that given headers are on top of the stack
  251. :param headers: Headers to remove
  252. :raise AssertionError: The given dictionary is not on the latest stored
  253. in the additional headers list
  254. """
  255. assert self.additional_headers[-1] == headers
  256. self.additional_headers.pop()
  257. def emit_additional_headers(self, connection):
  258. """
  259. Puts headers as is in the request, filtered read only headers
  260. :param connection: The request connection
  261. :return: The dictionary of headers added to the connection
  262. """
  263. additional_headers = {}
  264. # Setup extra headers
  265. # (list of tuples, inherited from xmlrpclib.client.Transport)
  266. # Authentication headers are stored there
  267. try:
  268. extra_headers = self._extra_headers or []
  269. except AttributeError:
  270. # Not available this version of Python (should not happen)
  271. pass
  272. else:
  273. for (key, value) in extra_headers:
  274. additional_headers[key] = value
  275. # Prepare the merged dictionary
  276. for headers in self.additional_headers:
  277. additional_headers.update(headers)
  278. # Normalize keys and values
  279. additional_headers = dict(
  280. (str(key).lower(), str(value))
  281. for key, value in additional_headers.items())
  282. # Remove forbidden keys
  283. for forbidden in self.readonly_headers:
  284. additional_headers.pop(forbidden, None)
  285. # Reversed order: in the case of multiple headers value definition,
  286. # the latest pushed has priority
  287. for key, value in additional_headers.items():
  288. connection.putheader(key, value)
  289. return additional_headers
  290. def single_request(self, host, handler, request_body, verbose=0):
  291. """
  292. Send a complete request, and parse the response.
  293. From xmlrpclib in Python 2.7
  294. :param host: Target host.
  295. :param handler: Target RPC handler.
  296. :param request_body: JSON-RPC request body.
  297. :param verbose: Debugging flag.
  298. :return: Parsed response.
  299. """
  300. connection = self.make_connection(host)
  301. try:
  302. self.send_request(connection, handler, request_body, verbose)
  303. self.send_content(connection, request_body)
  304. response = connection.getresponse()
  305. if response.status == 200:
  306. self.verbose = verbose
  307. return self.parse_response(response)
  308. except:
  309. # All unexpected errors leave connection in
  310. # a strange state, so we clear it.
  311. self.close()
  312. raise
  313. # Discard any response data and raise exception
  314. if response.getheader("content-length", 0):
  315. response.read()
  316. raise ProtocolError(host + handler,
  317. response.status, response.reason,
  318. response.msg)
  319. def send_request(self, connection, handler, request_body, debug=0):
  320. """
  321. Send HTTP request.
  322. From xmlrpc.client in Python 3.4
  323. :param connection: Connection handle.
  324. :param handler: Target RPC handler (a path relative to host)
  325. :param request_body: The JSON-RPC request body
  326. :param debug: Enable debugging if debug is true.
  327. :return: An HTTPConnection.
  328. """
  329. if debug:
  330. connection.set_debuglevel(1)
  331. if self.accept_gzip_encoding and gzip:
  332. connection.putrequest("POST", handler, skip_accept_encoding=True)
  333. connection.putheader("Accept-Encoding", "gzip")
  334. else:
  335. connection.putrequest("POST", handler)
  336. return connection
  337. def send_content(self, connection, request_body):
  338. """
  339. Completes the request headers and sends the request body of a JSON-RPC
  340. request over a HTTPConnection
  341. :param connection: An HTTPConnection object
  342. :param request_body: JSON-RPC request body
  343. """
  344. # Convert the body first
  345. request_body = utils.to_bytes(request_body)
  346. # "static" headers
  347. connection.putheader("Content-Type", self._config.content_type)
  348. connection.putheader("Content-Length", str(len(request_body)))
  349. # Emit additional headers here in order not to override content-length
  350. additional_headers = self.emit_additional_headers(connection)
  351. # Add the user agent, if not overridden
  352. if "user-agent" not in additional_headers:
  353. connection.putheader("User-Agent", self.user_agent)
  354. connection.endheaders()
  355. if request_body:
  356. connection.send(request_body)
  357. @staticmethod
  358. def getparser():
  359. """
  360. Create an instance of the parser, and attach it to an unmarshalling
  361. object. Return both objects.
  362. :return: The parser and unmarshaller instances
  363. """
  364. target = JSONTarget()
  365. return JSONParser(target), target
  366. class Transport(TransportMixIn, XMLTransport):
  367. """
  368. Mixed-in HTTP transport
  369. """
  370. def __init__(self, config):
  371. TransportMixIn.__init__(self, config)
  372. XMLTransport.__init__(self)
  373. class SafeTransport(TransportMixIn, XMLSafeTransport):
  374. """
  375. Mixed-in HTTPS transport
  376. """
  377. def __init__(self, config, context):
  378. TransportMixIn.__init__(self, config, context)
  379. XMLSafeTransport.__init__(self)
  380. # ------------------------------------------------------------------------------
  381. class ServerProxy(XMLServerProxy):
  382. """
  383. Unfortunately, much more of this class has to be copied since
  384. so much of it does the serialization.
  385. """
  386. def __init__(self, uri, transport=None, encoding=None,
  387. verbose=0, version=None, headers=None, history=None,
  388. config=jsonrpclib.config.DEFAULT, context=None):
  389. """
  390. Sets up the server proxy
  391. :param uri: Request URI
  392. :param transport: Custom transport handler
  393. :param encoding: Specified encoding
  394. :param verbose: Log verbosity level
  395. :param version: JSON-RPC specification version
  396. :param headers: Custom additional headers for each request
  397. :param history: History object (for tests)
  398. :param config: A JSONRPClib Config instance
  399. :param context: The optional SSLContext to use
  400. """
  401. # Store the configuration
  402. self._config = config
  403. self.__version = version or config.version
  404. schema, uri = splittype(uri)
  405. if schema not in ('http', 'https'):
  406. _logger.error("jsonrpclib only support http(s) URIs, not %s",
  407. schema)
  408. raise IOError('Unsupported JSON-RPC protocol.')
  409. self.__host, self.__handler = splithost(uri)
  410. if not self.__handler:
  411. # Not sure if this is in the JSON spec?
  412. self.__handler = '/'
  413. if transport is None:
  414. if schema == 'https':
  415. transport = SafeTransport(config=config, context=context)
  416. else:
  417. transport = Transport(config=config)
  418. self.__transport = transport
  419. self.__encoding = encoding
  420. self.__verbose = verbose
  421. self.__history = history
  422. # Global custom headers are injected into Transport
  423. self.__transport.push_headers(headers or {})
  424. def _request(self, methodname, params, rpcid=None):
  425. """
  426. Calls a method on the remote server
  427. :param methodname: Name of the method to call
  428. :param params: Method parameters
  429. :param rpcid: ID of the remote call
  430. :return: The parsed result of the call
  431. """
  432. request = dumps(params, methodname, encoding=self.__encoding,
  433. rpcid=rpcid, version=self.__version,
  434. config=self._config)
  435. response = self._run_request(request)
  436. check_for_errors(response)
  437. return response['result']
  438. def _request_notify(self, methodname, params, rpcid=None):
  439. """
  440. Calls a method as a notification
  441. :param methodname: Name of the method to call
  442. :param params: Method parameters
  443. :param rpcid: ID of the remote call
  444. """
  445. request = dumps(params, methodname, encoding=self.__encoding,
  446. rpcid=rpcid, version=self.__version, notify=True,
  447. config=self._config)
  448. response = self._run_request(request, notify=True)
  449. check_for_errors(response)
  450. def _run_request(self, request, notify=False):
  451. """
  452. Sends the given request to the remote server
  453. :param request: The request to send
  454. :param notify: Notification request flag (unused)
  455. :return: The response as a parsed JSON object
  456. """
  457. if self.__history is not None:
  458. self.__history.add_request(request)
  459. response = self.__transport.request(
  460. self.__host,
  461. self.__handler,
  462. request,
  463. verbose=self.__verbose
  464. )
  465. # Here, the XMLRPC library translates a single list
  466. # response to the single value -- should we do the
  467. # same, and require a tuple / list to be passed to
  468. # the response object, or expect the Server to be
  469. # outputting the response appropriately?
  470. if self.__history is not None:
  471. self.__history.add_response(response)
  472. if not response:
  473. return None
  474. else:
  475. return_obj = loads(response, self._config)
  476. return return_obj
  477. def __getattr__(self, name):
  478. """
  479. Returns a callable object to call the remote service
  480. """
  481. if name.startswith("__") and name.endswith("__"):
  482. # Don't proxy special methods.
  483. raise AttributeError("ServerProxy has no attribute '%s'" % name)
  484. # Same as original, just with new _Method reference
  485. return _Method(self._request, name)
  486. def __close(self):
  487. """
  488. Closes the transport layer
  489. """
  490. self.__transport.close()
  491. def __call__(self, attr):
  492. """
  493. A workaround to get special attributes on the ServerProxy
  494. without interfering with the magic __getattr__
  495. (code from xmlrpclib in Python 2.7)
  496. """
  497. if attr == "close":
  498. return self.__close
  499. elif attr == "transport":
  500. return self.__transport
  501. raise AttributeError("Attribute {0} not found".format(attr))
  502. @property
  503. def _notify(self):
  504. """
  505. Like __getattr__, but sending a notification request instead of a call
  506. """
  507. return _Notify(self._request_notify)
  508. @contextlib.contextmanager
  509. def _additional_headers(self, headers):
  510. """
  511. Allows to specify additional headers, to be added inside the with
  512. block.
  513. Example of usage:
  514. >>> with client._additional_headers({'X-Test' : 'Test'}) as new_client:
  515. ... new_client.method()
  516. ...
  517. >>> # Here old headers are restored
  518. """
  519. self.__transport.push_headers(headers)
  520. yield self
  521. self.__transport.pop_headers(headers)
  522. # ------------------------------------------------------------------------------
  523. class _Method(XML_Method):
  524. """
  525. Some magic to bind an JSON-RPC method to an RPC server.
  526. """
  527. def __call__(self, *args, **kwargs):
  528. """
  529. Sends an RPC request and returns the unmarshalled result
  530. """
  531. if args and kwargs:
  532. raise ProtocolError("Cannot use both positional and keyword "
  533. "arguments (according to JSON-RPC spec.)")
  534. if args:
  535. return self.__send(self.__name, args)
  536. else:
  537. return self.__send(self.__name, kwargs)
  538. def __getattr__(self, name):
  539. """
  540. Returns a Method object for nested calls
  541. """
  542. if name == "__name__":
  543. return self.__name
  544. return _Method(self.__send, "{0}.{1}".format(self.__name, name))
  545. def __repr__(self):
  546. """
  547. Returns a string representation of the method
  548. """
  549. # Must use __class__ here because the base class is old-style.
  550. return "<{0} {1}>".format(self.__class__, self.__name)
  551. class _Notify(object):
  552. """
  553. Same as _Method, but to send notifications
  554. """
  555. def __init__(self, request):
  556. """
  557. Sets the method to call to send a request to the server
  558. """
  559. self._request = request
  560. def __getattr__(self, name):
  561. """
  562. Returns a Method object, to be called as a notification
  563. """
  564. return _Method(self._request, name)
  565. # ------------------------------------------------------------------------------
  566. # Batch implementation
  567. class MultiCallMethod(object):
  568. """
  569. Stores calls made to a MultiCall object for batch execution
  570. """
  571. def __init__(self, method, notify=False, config=jsonrpclib.config.DEFAULT):
  572. """
  573. Sets up the store
  574. :param method: Name of the method to call
  575. :param notify: Notification flag
  576. :param config: Request configuration
  577. """
  578. self.method = method
  579. self.params = []
  580. self.notify = notify
  581. self._config = config
  582. def __call__(self, *args, **kwargs):
  583. """
  584. Normalizes call parameters
  585. """
  586. if kwargs and args:
  587. raise ProtocolError('JSON-RPC does not support both ' +
  588. 'positional and keyword arguments.')
  589. if kwargs:
  590. self.params = kwargs
  591. else:
  592. self.params = args
  593. def request(self, encoding=None, rpcid=None):
  594. """
  595. Returns the request object as JSON-formatted string
  596. """
  597. return dumps(self.params, self.method, version=2.0,
  598. encoding=encoding, rpcid=rpcid, notify=self.notify,
  599. config=self._config)
  600. def __repr__(self):
  601. """
  602. String representation
  603. """
  604. return str(self.request())
  605. def __getattr__(self, method):
  606. """
  607. Updates the object for a nested call
  608. """
  609. self.method = "{0}.{1}".format(self.method, method)
  610. return self
  611. class MultiCallNotify(object):
  612. """
  613. Same as MultiCallMethod but for notifications
  614. """
  615. def __init__(self, multicall, config=jsonrpclib.config.DEFAULT):
  616. """
  617. Sets ip the store
  618. :param multicall: The parent MultiCall instance
  619. :param config: Request configuration
  620. """
  621. self.multicall = multicall
  622. self._config = config
  623. def __getattr__(self, name):
  624. """
  625. Returns the MultiCallMethod to use as a notification
  626. """
  627. new_job = MultiCallMethod(name, notify=True, config=self._config)
  628. self.multicall._job_list.append(new_job)
  629. return new_job
  630. class MultiCallIterator(object):
  631. """
  632. Iterates over the results of a MultiCall.
  633. Exceptions are raised in response to JSON-RPC faults
  634. """
  635. def __init__(self, results):
  636. """
  637. Sets up the results store
  638. """
  639. self.results = results
  640. @staticmethod
  641. def __get_result(item):
  642. """
  643. Checks for error and returns the "real" result stored in a MultiCall
  644. result.
  645. """
  646. check_for_errors(item)
  647. return item['result']
  648. def __iter__(self):
  649. """
  650. Iterates over all results
  651. """
  652. for item in self.results:
  653. yield self.__get_result(item)
  654. raise StopIteration
  655. def __getitem__(self, i):
  656. """
  657. Returns the i-th object of the results
  658. """
  659. return self.__get_result(self.results[i])
  660. def __len__(self):
  661. """
  662. Returns the number of results stored
  663. """
  664. return len(self.results)
  665. class MultiCall(object):
  666. """
  667. server -> a object used to boxcar method calls, where server should be a
  668. ServerProxy object.
  669. Methods can be added to the MultiCall using normal
  670. method call syntax e.g.:
  671. multicall = MultiCall(server_proxy)
  672. multicall.add(2,3)
  673. multicall.get_address("Guido")
  674. To execute the multicall, call the MultiCall object e.g.:
  675. add_result, address = multicall()
  676. """
  677. def __init__(self, server, config=jsonrpclib.config.DEFAULT):
  678. """
  679. Sets up the multicall
  680. :param server: A ServerProxy object
  681. :param config: Request configuration
  682. """
  683. self._server = server
  684. self._job_list = []
  685. self._config = config
  686. def _request(self):
  687. """
  688. Sends the request to the server and returns the responses
  689. :return: A MultiCallIterator object
  690. """
  691. if len(self._job_list) < 1:
  692. # Should we alert? This /is/ pretty obvious.
  693. return
  694. request_body = "[ {0} ]".format(
  695. ','.join(job.request() for job in self._job_list))
  696. responses = self._server._run_request(request_body)
  697. del self._job_list[:]
  698. if not responses:
  699. responses = []
  700. return MultiCallIterator(responses)
  701. @property
  702. def _notify(self):
  703. """
  704. Prepares a notification call
  705. """
  706. return MultiCallNotify(self, self._config)
  707. def __getattr__(self, name):
  708. """
  709. Registers a method call
  710. """
  711. new_job = MultiCallMethod(name, config=self._config)
  712. self._job_list.append(new_job)
  713. return new_job
  714. __call__ = _request
  715. # These lines conform to xmlrpclib's "compatibility" line.
  716. # Not really sure if we should include these, but oh well.
  717. Server = ServerProxy
  718. # ------------------------------------------------------------------------------
  719. class Fault(object):
  720. """
  721. JSON-RPC error class
  722. """
  723. def __init__(self, code=-32000, message='Server error', rpcid=None,
  724. config=jsonrpclib.config.DEFAULT, data=None):
  725. """
  726. Sets up the error description
  727. :param code: Fault code
  728. :param message: Associated message
  729. :param rpcid: Request ID
  730. :param config: A JSONRPClib Config instance
  731. :param data: Extra information added to an error description
  732. """
  733. self.faultCode = code
  734. self.faultString = message
  735. self.rpcid = rpcid
  736. self.config = config
  737. self.data = data
  738. def error(self):
  739. """
  740. Returns the error as a dictionary
  741. :returns: A {'code', 'message'} dictionary
  742. """
  743. return {'code': self.faultCode, 'message': self.faultString,
  744. 'data': self.data}
  745. def response(self, rpcid=None, version=None):
  746. """
  747. Returns the error as a JSON-RPC response string
  748. :param rpcid: Forced request ID
  749. :param version: JSON-RPC version
  750. :return: A JSON-RPC response string
  751. """
  752. if not version:
  753. version = self.config.version
  754. if rpcid:
  755. self.rpcid = rpcid
  756. return dumps(self, methodresponse=True, rpcid=self.rpcid,
  757. version=version, config=self.config)
  758. def dump(self, rpcid=None, version=None):
  759. """
  760. Returns the error as a JSON-RPC response dictionary
  761. :param rpcid: Forced request ID
  762. :param version: JSON-RPC version
  763. :return: A JSON-RPC response dictionary
  764. """
  765. if not version:
  766. version = self.config.version
  767. if rpcid:
  768. self.rpcid = rpcid
  769. return dump(self, is_response=True, rpcid=self.rpcid,
  770. version=version, config=self.config)
  771. def __repr__(self):
  772. """
  773. String representation
  774. """
  775. return '<Fault {0}: {1}>'.format(self.faultCode, self.faultString)
  776. class Payload(object):
  777. """
  778. JSON-RPC content handler
  779. """
  780. def __init__(self, rpcid=None, version=None,
  781. config=jsonrpclib.config.DEFAULT):
  782. """
  783. Sets up the JSON-RPC handler
  784. :param rpcid: Request ID
  785. :param version: JSON-RPC version
  786. :param config: A JSONRPClib Config instance
  787. """
  788. if not version:
  789. version = config.version
  790. self.id = rpcid
  791. self.version = float(version)
  792. def request(self, method, params=None):
  793. """
  794. Prepares a method call request
  795. :param method: Method name
  796. :param params: Method parameters
  797. :return: A JSON-RPC request dictionary
  798. """
  799. if not isinstance(method, utils.STRING_TYPES):
  800. raise ValueError('Method name must be a string.')
  801. if not self.id:
  802. # Generate a request ID
  803. self.id = str(uuid.uuid4())
  804. request = {'id': self.id, 'method': method}
  805. if params or self.version < 1.1:
  806. request['params'] = params or []
  807. if self.version >= 2:
  808. request['jsonrpc'] = str(self.version)
  809. return request
  810. def notify(self, method, params=None):
  811. """
  812. Prepares a notification request
  813. :param method: Notification name
  814. :param params: Notification parameters
  815. :return: A JSON-RPC notification dictionary
  816. """
  817. # Prepare the request dictionary
  818. request = self.request(method, params)
  819. # Remove the request ID, as it's a notification
  820. if self.version >= 2:
  821. del request['id']
  822. else:
  823. request['id'] = None
  824. return request
  825. def response(self, result=None):
  826. """
  827. Prepares a response dictionary
  828. :param result: The result of method call
  829. :return: A JSON-RPC response dictionary
  830. """
  831. response = {'result': result, 'id': self.id}
  832. if self.version >= 2:
  833. response['jsonrpc'] = str(self.version)
  834. else:
  835. response['error'] = None
  836. return response
  837. def error(self, code=-32000, message='Server error.', data=None):
  838. """
  839. Prepares an error dictionary
  840. :param code: Error code
  841. :param message: Error message
  842. :param data: Extra data to associate to the error
  843. :return: A JSON-RPC error dictionary
  844. """
  845. error = self.response()
  846. if self.version >= 2:
  847. del error['result']
  848. else:
  849. error['result'] = None
  850. error['error'] = {'code': code, 'message': message}
  851. if data is not None:
  852. error['error']['data'] = data
  853. return error
  854. # ------------------------------------------------------------------------------
  855. def dump(params=None, methodname=None, rpcid=None, version=None,
  856. is_response=None, is_notify=None, config=jsonrpclib.config.DEFAULT):
  857. """
  858. Prepares a JSON-RPC dictionary (request, notification, response or error)
  859. :param params: Method parameters (if a method name is given) or a Fault
  860. :param methodname: Method name
  861. :param rpcid: Request ID
  862. :param version: JSON-RPC version
  863. :param is_response: If True, this is a response dictionary
  864. :param is_notify: If True, this is a notification request
  865. :param config: A JSONRPClib Config instance
  866. :return: A JSON-RPC dictionary
  867. """
  868. # Default version
  869. if not version:
  870. version = config.version
  871. if not is_response and params is None:
  872. params = []
  873. # Validate method name and parameters
  874. valid_params = [utils.TupleType, utils.ListType, utils.DictType, Fault]
  875. if is_response:
  876. valid_params.append(type(None))
  877. if isinstance(methodname, utils.STRING_TYPES) and \
  878. not isinstance(params, tuple(valid_params)):
  879. """
  880. If a method, and params are not in a listish or a Fault,
  881. error out.
  882. """
  883. raise TypeError("Params must be a dict, list, tuple "
  884. "or Fault instance.")
  885. # Prepares the JSON-RPC content
  886. payload = Payload(rpcid=rpcid, version=version)
  887. if isinstance(params, Fault):
  888. # Prepare an error dictionary
  889. # pylint: disable=E1103
  890. return payload.error(params.faultCode, params.faultString, params.data)
  891. if not isinstance(methodname, utils.STRING_TYPES) and not is_response:
  892. # Neither a request nor a response
  893. raise ValueError('Method name must be a string, or is_response '
  894. 'must be set to True.')
  895. if config.use_jsonclass:
  896. # Use jsonclass to convert the parameters
  897. params = jsonclass.dump(params, config=config)
  898. if is_response:
  899. # Prepare a response dictionary
  900. if rpcid is None:
  901. # A response must have a request ID
  902. raise ValueError('A method response must have an rpcid.')
  903. return payload.response(params)
  904. if is_notify:
  905. # Prepare a notification dictionary
  906. return payload.notify(methodname, params)
  907. else:
  908. # Prepare a method call dictionary
  909. return payload.request(methodname, params)
  910. def dumps(params=None, methodname=None, methodresponse=None,
  911. encoding=None, rpcid=None, version=None, notify=None,
  912. config=jsonrpclib.config.DEFAULT):
  913. """
  914. Prepares a JSON-RPC request/response string
  915. :param params: Method parameters (if a method name is given) or a Fault
  916. :param methodname: Method name
  917. :param methodresponse: If True, this is a response dictionary
  918. :param encoding: Result string encoding
  919. :param rpcid: Request ID
  920. :param version: JSON-RPC version
  921. :param notify: If True, this is a notification request
  922. :param config: A JSONRPClib Config instance
  923. :return: A JSON-RPC dictionary
  924. """
  925. # Prepare the dictionary
  926. request = dump(params, methodname, rpcid, version, methodresponse, notify,
  927. config)
  928. # Returns it as a JSON string
  929. return jdumps(request, encoding=encoding or "UTF-8")
  930. def load(data, config=jsonrpclib.config.DEFAULT):
  931. """
  932. Loads a JSON-RPC request/response dictionary. Calls jsonclass to load beans
  933. :param data: A JSON-RPC dictionary
  934. :param config: A JSONRPClib Config instance (or None for default values)
  935. :return: A parsed dictionary or None
  936. """
  937. if data is None:
  938. # Notification
  939. return None
  940. # if the above raises an error, the implementing server code
  941. # should return something like the following:
  942. # { 'jsonrpc':'2.0', 'error': fault.error(), id: None }
  943. if config.use_jsonclass:
  944. # Convert beans
  945. data = jsonclass.load(data, config.classes)
  946. return data
  947. def loads(data, config=jsonrpclib.config.DEFAULT):
  948. """
  949. Loads a JSON-RPC request/response string. Calls jsonclass to load beans
  950. :param data: A JSON-RPC string
  951. :param config: A JSONRPClib Config instance (or None for default values)
  952. :return: A parsed dictionary or None
  953. """
  954. if data == '':
  955. # Notification
  956. return None
  957. # Parse the JSON dictionary
  958. result = jloads(data)
  959. # Load the beans
  960. return load(result, config)
  961. # ------------------------------------------------------------------------------
  962. def check_for_errors(result):
  963. """
  964. Checks if a result dictionary signals an error
  965. :param result: A result dictionary
  966. :raise TypeError: Invalid parameter
  967. :raise NotImplementedError: Unknown JSON-RPC version
  968. :raise ValueError: Invalid dictionary content
  969. :raise ProtocolError: An error occurred on the server side
  970. :return: The result parameter
  971. """
  972. if not result:
  973. # Notification
  974. return result
  975. if not isinstance(result, utils.DictType):
  976. # Invalid argument
  977. raise TypeError('Response is not a dict.')
  978. if 'jsonrpc' in result and float(result['jsonrpc']) > 2.0:
  979. # Unknown JSON-RPC version
  980. raise NotImplementedError('JSON-RPC version not yet supported.')
  981. if 'result' not in result and 'error' not in result:
  982. # Invalid dictionary content
  983. raise ValueError('Response does not have a result or error key.')
  984. if 'error' in result and result['error']:
  985. # Server-side error
  986. if 'code' in result['error']:
  987. # Code + Message
  988. code = result['error']['code']
  989. try:
  990. # Get the message (jsonrpclib)
  991. message = result['error']['message']
  992. except KeyError:
  993. # Get the trace (jabsorb)
  994. message = result['error'].get('trace', '<no error message>')
  995. if -32700 <= code <= -32000:
  996. # Pre-defined errors
  997. # See http://www.jsonrpc.org/specification#error_object
  998. raise ProtocolError((code, message))
  999. else:
  1000. # Application error
  1001. data = result['error'].get('data', None)
  1002. raise AppError((code, message, data))
  1003. elif isinstance(result['error'], dict) and len(result['error']) == 1:
  1004. # Error with a single entry ('reason', ...): use its content
  1005. error_key = result['error'].keys()[0]
  1006. raise ProtocolError(result['error'][error_key])
  1007. else:
  1008. # Use the raw error content
  1009. raise ProtocolError(result['error'])
  1010. return result
  1011. def isbatch(request):
  1012. """
  1013. Tests if the given request is a batch call, i.e. a list of multiple calls
  1014. :param request: a JSON-RPC request object
  1015. :return: True if the request is a batch call
  1016. """
  1017. if not isinstance(request, (utils.ListType, utils.TupleType)):
  1018. # Not a list: not a batch call
  1019. return False
  1020. elif len(request) < 1:
  1021. # Only one request: not a batch call
  1022. return False
  1023. elif not isinstance(request[0], utils.DictType):
  1024. # One of the requests is not a dictionary, i.e. a JSON Object
  1025. # therefore it is not a valid JSON-RPC request
  1026. return False
  1027. elif 'jsonrpc' not in request[0].keys():
  1028. # No "jsonrpc" version in the JSON object: not a request
  1029. return False
  1030. try:
  1031. version = float(request[0]['jsonrpc'])
  1032. except ValueError:
  1033. # Bad version of JSON-RPC
  1034. raise ProtocolError('"jsonrpc" key must be a float(able) value.')
  1035. if version < 2:
  1036. # Batch call were not supported before JSON-RPC 2.0
  1037. return False
  1038. return True
  1039. def isnotification(request):
  1040. """
  1041. Tests if the given request is a notification
  1042. :param request: A request dictionary
  1043. :return: True if the request is a notification
  1044. """
  1045. if 'id' not in request:
  1046. # 2.0 notification
  1047. return True
  1048. if request['id'] is None:
  1049. # 1.0 notification
  1050. return True
  1051. return False