|  | # Copyright 2014 The Chromium Authors. All rights reserved. | 
|  | # Use of this source code is governed by a BSD-style license that can be | 
|  | # found in the LICENSE file. | 
|  |  | 
|  | """Module to implement the JSON-RPC protocol. | 
|  |  | 
|  | This module uses xmlrpclib as the base and only overrides those | 
|  | portions that implement the XML-RPC protocol. These portions are rewritten | 
|  | to use the JSON-RPC protocol instead. | 
|  |  | 
|  | When large portions of code need to be rewritten the original code and | 
|  | comments are preserved. The intention here is to keep the amount of code | 
|  | change to a minimum. | 
|  |  | 
|  | This module only depends on default Python modules. No third party code is | 
|  | required to use this module. | 
|  | """ | 
|  | import json | 
|  | import urllib | 
|  | import xmlrpclib as _base | 
|  |  | 
|  | __version__ = '1.0.0' | 
|  | gzip_encode = _base.gzip_encode | 
|  | gzip = _base.gzip | 
|  |  | 
|  |  | 
|  | class Error(Exception): | 
|  |  | 
|  | def __str__(self): | 
|  | return repr(self) | 
|  |  | 
|  |  | 
|  | class ProtocolError(Error): | 
|  | """Indicates a JSON protocol error.""" | 
|  |  | 
|  | def __init__(self, url, errcode, errmsg, headers): | 
|  | Error.__init__(self) | 
|  | self.url = url | 
|  | self.errcode = errcode | 
|  | self.errmsg = errmsg | 
|  | self.headers = headers | 
|  |  | 
|  | def __repr__(self): | 
|  | return ( | 
|  | '<ProtocolError for %s: %s %s>' % | 
|  | (self.url, self.errcode, self.errmsg)) | 
|  |  | 
|  |  | 
|  | class ResponseError(Error): | 
|  | """Indicates a broken response package.""" | 
|  | pass | 
|  |  | 
|  |  | 
|  | class Fault(Error): | 
|  | """Indicates a JSON-RPC fault package.""" | 
|  |  | 
|  | def __init__(self, code, message): | 
|  | Error.__init__(self) | 
|  | if not isinstance(code, int): | 
|  | raise ProtocolError('Fault code must be an integer.') | 
|  | self.code = code | 
|  | self.message = message | 
|  |  | 
|  | def __repr__(self): | 
|  | return ( | 
|  | '<Fault %s: %s>' % | 
|  | (self.code, repr(self.message)) | 
|  | ) | 
|  |  | 
|  |  | 
|  | def CreateRequest(methodname, params, ident=''): | 
|  | """Create a valid JSON-RPC request. | 
|  |  | 
|  | Args: | 
|  | methodname: The name of the remote method to invoke. | 
|  | params: The parameters to pass to the remote method. This should be a | 
|  | list or tuple and able to be encoded by the default JSON parser. | 
|  |  | 
|  | Returns: | 
|  | A valid JSON-RPC request object. | 
|  | """ | 
|  | request = { | 
|  | 'jsonrpc': '2.0', | 
|  | 'method': methodname, | 
|  | 'params': params, | 
|  | 'id': ident | 
|  | } | 
|  |  | 
|  | return request | 
|  |  | 
|  |  | 
|  | def CreateRequestString(methodname, params, ident=''): | 
|  | """Create a valid JSON-RPC request string. | 
|  |  | 
|  | Args: | 
|  | methodname: The name of the remote method to invoke. | 
|  | params: The parameters to pass to the remote method. | 
|  | These parameters need to be encode-able by the default JSON parser. | 
|  | ident: The request identifier. | 
|  |  | 
|  | Returns: | 
|  | A valid JSON-RPC request string. | 
|  | """ | 
|  | return json.dumps(CreateRequest(methodname, params, ident)) | 
|  |  | 
|  |  | 
|  | def CreateResponse(data, ident): | 
|  | """Create a JSON-RPC response. | 
|  |  | 
|  | Args: | 
|  | data: The data to return. | 
|  | ident: The response identifier. | 
|  |  | 
|  | Returns: | 
|  | A valid JSON-RPC response object. | 
|  | """ | 
|  | if isinstance(data, Fault): | 
|  | response = { | 
|  | 'jsonrpc': '2.0', | 
|  | 'error': { | 
|  | 'code': data.code, | 
|  | 'message': data.message}, | 
|  | 'id': ident | 
|  | } | 
|  | else: | 
|  | response = { | 
|  | 'jsonrpc': '2.0', | 
|  | 'response': data, | 
|  | 'id': ident | 
|  | } | 
|  |  | 
|  | return response | 
|  |  | 
|  |  | 
|  | def CreateResponseString(data, ident): | 
|  | """Create a JSON-RPC response string. | 
|  |  | 
|  | Args: | 
|  | data: The data to return. | 
|  | ident: The response identifier. | 
|  |  | 
|  | Returns: | 
|  | A valid JSON-RPC response object. | 
|  | """ | 
|  | return json.dumps(CreateResponse(data, ident)) | 
|  |  | 
|  |  | 
|  | def ParseHTTPResponse(response): | 
|  | """Parse an HTTP response object and return the JSON object. | 
|  |  | 
|  | Args: | 
|  | response: An HTTP response object. | 
|  |  | 
|  | Returns: | 
|  | The returned JSON-RPC object. | 
|  |  | 
|  | Raises: | 
|  | ProtocolError: if the object format is not correct. | 
|  | Fault: If a Fault error is returned from the server. | 
|  | """ | 
|  | # Check for new http response object, else it is a file object | 
|  | if hasattr(response, 'getheader'): | 
|  | if response.getheader('Content-Encoding', '') == 'gzip': | 
|  | stream = _base.GzipDecodedResponse(response) | 
|  | else: | 
|  | stream = response | 
|  | else: | 
|  | stream = response | 
|  |  | 
|  | data = '' | 
|  | while 1: | 
|  | chunk = stream.read(1024) | 
|  | if not chunk: | 
|  | break | 
|  | data += chunk | 
|  |  | 
|  | response = json.loads(data) | 
|  | ValidateBasicJSONRPCData(response) | 
|  |  | 
|  | if 'response' in response: | 
|  | ValidateResponse(response) | 
|  | return response['response'] | 
|  | elif 'error' in response: | 
|  | ValidateError(response) | 
|  | code = response['error']['code'] | 
|  | message = response['error']['message'] | 
|  | raise Fault(code, message) | 
|  | else: | 
|  | raise ProtocolError('No valid JSON returned') | 
|  |  | 
|  |  | 
|  | def ValidateRequest(data): | 
|  | """Validate a JSON-RPC request object. | 
|  |  | 
|  | Args: | 
|  | data: The JSON-RPC object (dict). | 
|  |  | 
|  | Raises: | 
|  | ProtocolError: if the object format is not correct. | 
|  | """ | 
|  | ValidateBasicJSONRPCData(data) | 
|  | if 'method' not in data or 'params' not in data: | 
|  | raise ProtocolError('JSON is not a valid request') | 
|  |  | 
|  |  | 
|  | def ValidateResponse(data): | 
|  | """Validate a JSON-RPC response object. | 
|  |  | 
|  | Args: | 
|  | data: The JSON-RPC object (dict). | 
|  |  | 
|  | Raises: | 
|  | ProtocolError: if the object format is not correct. | 
|  | """ | 
|  | ValidateBasicJSONRPCData(data) | 
|  | if 'response' not in data: | 
|  | raise ProtocolError('JSON is not a valid response') | 
|  |  | 
|  |  | 
|  | def ValidateError(data): | 
|  | """Validate a JSON-RPC error object. | 
|  |  | 
|  | Args: | 
|  | data: The JSON-RPC object (dict). | 
|  |  | 
|  | Raises: | 
|  | ProtocolError: if the object format is not correct. | 
|  | """ | 
|  | ValidateBasicJSONRPCData(data) | 
|  | if ('error' not in data or | 
|  | 'code' not in data['error'] or | 
|  | 'message' not in data['error']): | 
|  | raise ProtocolError('JSON is not a valid error response') | 
|  |  | 
|  |  | 
|  | def ValidateBasicJSONRPCData(data): | 
|  | """Validate a basic JSON-RPC object. | 
|  |  | 
|  | Args: | 
|  | data: The JSON-RPC object (dict). | 
|  |  | 
|  | Raises: | 
|  | ProtocolError: if the object format is not correct. | 
|  | """ | 
|  | error = None | 
|  | if not isinstance(data, dict): | 
|  | error = 'JSON data is not a dictionary' | 
|  | elif 'jsonrpc' not in data or data['jsonrpc'] != '2.0': | 
|  | error = 'JSON is not a valid JSON RPC 2.0 message' | 
|  | elif 'id' not in data: | 
|  | error = 'JSON data missing required id entry' | 
|  | if error: | 
|  | raise ProtocolError(error) | 
|  |  | 
|  |  | 
|  | class Transport(_base.Transport): | 
|  | """RPC transport class. | 
|  |  | 
|  | This class extends the functionality of xmlrpclib.Transport and only | 
|  | overrides the operations needed to change the protocol from XML-RPC to | 
|  | JSON-RPC. | 
|  | """ | 
|  |  | 
|  | user_agent = 'jsonrpclib.py/' + __version__ | 
|  |  | 
|  | def send_content(self, connection, request_body): | 
|  | """Send the request.""" | 
|  | connection.putheader('Content-Type','application/json') | 
|  |  | 
|  | #optionally encode the request | 
|  | if (self.encode_threshold is not None and | 
|  | self.encode_threshold < len(request_body) and | 
|  | gzip): | 
|  | connection.putheader('Content-Encoding', 'gzip') | 
|  | request_body = gzip_encode(request_body) | 
|  |  | 
|  | connection.putheader('Content-Length', str(len(request_body))) | 
|  | connection.endheaders(request_body) | 
|  |  | 
|  | def single_request(self, host, handler, request_body, verbose=0): | 
|  | """Issue a single JSON-RPC request.""" | 
|  |  | 
|  | h = self.make_connection(host) | 
|  | if verbose: | 
|  | h.set_debuglevel(1) | 
|  | try: | 
|  | self.send_request(h, handler, request_body) | 
|  | self.send_host(h, host) | 
|  | self.send_user_agent(h) | 
|  | self.send_content(h, request_body) | 
|  |  | 
|  | response = h.getresponse(buffering=True) | 
|  | if response.status == 200: | 
|  | self.verbose = verbose  #pylint: disable=attribute-defined-outside-init | 
|  |  | 
|  | return self.parse_response(response) | 
|  |  | 
|  | except Fault: | 
|  | raise | 
|  | except Exception: | 
|  | # All unexpected errors leave connection in | 
|  | # a strange state, so we clear it. | 
|  | self.close() | 
|  | raise | 
|  |  | 
|  | # discard any response data and raise exception | 
|  | if response.getheader('content-length', 0): | 
|  | response.read() | 
|  | raise ProtocolError( | 
|  | host + handler, | 
|  | response.status, response.reason, | 
|  | response.msg, | 
|  | ) | 
|  |  | 
|  | def parse_response(self, response): | 
|  | """Parse the HTTP resoponse from the server.""" | 
|  | return ParseHTTPResponse(response) | 
|  |  | 
|  |  | 
|  | class SafeTransport(_base.SafeTransport): | 
|  | """Transport class for HTTPS servers. | 
|  |  | 
|  | This class extends the functionality of xmlrpclib.SafeTransport and only | 
|  | overrides the operations needed to change the protocol from XML-RPC to | 
|  | JSON-RPC. | 
|  | """ | 
|  |  | 
|  | def parse_response(self, response): | 
|  | return ParseHTTPResponse(response) | 
|  |  | 
|  |  | 
|  | class ServerProxy(_base.ServerProxy): | 
|  | """Proxy class to the RPC server. | 
|  |  | 
|  | This class extends the functionality of xmlrpclib.ServerProxy and only | 
|  | overrides the operations needed to change the protocol from XML-RPC to | 
|  | JSON-RPC. | 
|  | """ | 
|  |  | 
|  | def __init__(self, uri, transport=None, encoding=None, verbose=0, | 
|  | allow_none=0, use_datetime=0): | 
|  | urltype, _ = urllib.splittype(uri) | 
|  | if urltype not in ('http', 'https'): | 
|  | raise IOError('unsupported JSON-RPC protocol') | 
|  |  | 
|  | _base.ServerProxy.__init__(self, uri, transport, encoding, verbose, | 
|  | allow_none, use_datetime) | 
|  | transport_type, uri = urllib.splittype(uri) | 
|  | if transport is None: | 
|  | if transport_type == 'https': | 
|  | transport = SafeTransport(use_datetime=use_datetime) | 
|  | else: | 
|  | transport = Transport(use_datetime=use_datetime) | 
|  | self.__transport = transport | 
|  |  | 
|  | def __request(self, methodname, params): | 
|  | """Call a method on the remote server.""" | 
|  | request = CreateRequestString(methodname, params) | 
|  |  | 
|  | response = self.__transport.request( | 
|  | self.__host, | 
|  | self.__handler, | 
|  | request, | 
|  | verbose=self.__verbose | 
|  | ) | 
|  |  | 
|  | return response | 
|  |  | 
|  |  | 
|  | Server = ServerProxy |