Chromium Code Reviews| Index: remoting/tools/remote_test_helper/jsonrpclib.py |
| diff --git a/remoting/tools/remote_test_helper/jsonrpclib.py b/remoting/tools/remote_test_helper/jsonrpclib.py |
| new file mode 100644 |
| index 0000000000000000000000000000000000000000..33326bb1ee511ee452277fec1bef614051d93797 |
| --- /dev/null |
| +++ b/remoting/tools/remote_test_helper/jsonrpclib.py |
| @@ -0,0 +1,347 @@ |
| +# 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 |
| + |
| +gzip_encode = _base.gzip_encode |
| + |
| + |
| +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 an 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 |
| + } |
|
andrewrs
2014/12/18 19:10:32
This trailing brace doesn't look quite aligned in
Mike Meade
2015/01/05 23:18:58
Actually this whole block was badly formatted. Tha
|
| + 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. |
| + """ |
| + |
|
andrewrs
2014/12/18 19:10:32
What do you think about setting Transport.user_age
Mike Meade
2015/01/05 23:18:58
Done.
|
| + 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) |
|
andrewrs
2014/12/18 19:10:32
Looking at the default implementation of xmlrpclib
Mike Meade
2015/01/05 23:18:58
Good catch on that one. Done.
|
| + |
| + response = h.getresponse(buffering=True) |
| + if response.status == 200: |
| + self.verbose = verbose |
| + |
| + 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) |
| + |
| + if transport is None: |
| + if 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 |