1 # Copyright 2014 The Chromium Authors. All rights reserved.
2 # Use of this source code is governed by a BSD-style license that can be
3 # found in the LICENSE file.
5 """Module to implement the JSON-RPC protocol.
7 This module uses xmlrpclib as the base and only overrides those
8 portions that implement the XML-RPC protocol. These portions are rewritten
9 to use the JSON-RPC protocol instead.
11 When large portions of code need to be rewritten the original code and
12 comments are preserved. The intention here is to keep the amount of code
15 This module only depends on default Python modules. No third party code is
16 required to use this module.
20 import xmlrpclib
as _base
23 gzip_encode
= _base
.gzip_encode
27 class Error(Exception):
33 class ProtocolError(Error
):
34 """Indicates a JSON protocol error."""
36 def __init__(self
, url
, errcode
, errmsg
, headers
):
39 self
.errcode
= errcode
41 self
.headers
= headers
45 '<ProtocolError for %s: %s %s>' %
46 (self
.url
, self
.errcode
, self
.errmsg
))
49 class ResponseError(Error
):
50 """Indicates a broken response package."""
55 """Indicates a JSON-RPC fault package."""
57 def __init__(self
, code
, message
):
59 if not isinstance(code
, int):
60 raise ProtocolError('Fault code must be an integer.')
62 self
.message
= message
67 (self
.code
, repr(self
.message
))
71 def CreateRequest(methodname
, params
, ident
=''):
72 """Create a valid JSON-RPC request.
75 methodname: The name of the remote method to invoke.
76 params: The parameters to pass to the remote method. This should be a
77 list or tuple and able to be encoded by the default JSON parser.
80 A valid JSON-RPC request object.
92 def CreateRequestString(methodname
, params
, ident
=''):
93 """Create a valid JSON-RPC request string.
96 methodname: The name of the remote method to invoke.
97 params: The parameters to pass to the remote method.
98 These parameters need to be encode-able by the default JSON parser.
99 ident: The request identifier.
102 A valid JSON-RPC request string.
104 return json
.dumps(CreateRequest(methodname
, params
, ident
))
107 def CreateResponse(data
, ident
):
108 """Create a JSON-RPC response.
111 data: The data to return.
112 ident: The response identifier.
115 A valid JSON-RPC response object.
117 if isinstance(data
, Fault
):
122 'message': data
.message
},
135 def CreateResponseString(data
, ident
):
136 """Create a JSON-RPC response string.
139 data: The data to return.
140 ident: The response identifier.
143 A valid JSON-RPC response object.
145 return json
.dumps(CreateResponse(data
, ident
))
148 def ParseHTTPResponse(response
):
149 """Parse an HTTP response object and return the JSON object.
152 response: An HTTP response object.
155 The returned JSON-RPC object.
158 ProtocolError: if the object format is not correct.
159 Fault: If a Fault error is returned from the server.
161 # Check for new http response object, else it is a file object
162 if hasattr(response
, 'getheader'):
163 if response
.getheader('Content-Encoding', '') == 'gzip':
164 stream
= _base
.GzipDecodedResponse(response
)
172 chunk
= stream
.read(1024)
177 response
= json
.loads(data
)
178 ValidateBasicJSONRPCData(response
)
180 if 'response' in response
:
181 ValidateResponse(response
)
182 return response
['response']
183 elif 'error' in response
:
184 ValidateError(response
)
185 code
= response
['error']['code']
186 message
= response
['error']['message']
187 raise Fault(code
, message
)
189 raise ProtocolError('No valid JSON returned')
192 def ValidateRequest(data
):
193 """Validate a JSON-RPC request object.
196 data: The JSON-RPC object (dict).
199 ProtocolError: if the object format is not correct.
201 ValidateBasicJSONRPCData(data
)
202 if 'method' not in data
or 'params' not in data
:
203 raise ProtocolError('JSON is not a valid request')
206 def ValidateResponse(data
):
207 """Validate a JSON-RPC response object.
210 data: The JSON-RPC object (dict).
213 ProtocolError: if the object format is not correct.
215 ValidateBasicJSONRPCData(data
)
216 if 'response' not in data
:
217 raise ProtocolError('JSON is not a valid response')
220 def ValidateError(data
):
221 """Validate a JSON-RPC error object.
224 data: The JSON-RPC object (dict).
227 ProtocolError: if the object format is not correct.
229 ValidateBasicJSONRPCData(data
)
230 if ('error' not in data
or
231 'code' not in data
['error'] or
232 'message' not in data
['error']):
233 raise ProtocolError('JSON is not a valid error response')
236 def ValidateBasicJSONRPCData(data
):
237 """Validate a basic JSON-RPC object.
240 data: The JSON-RPC object (dict).
243 ProtocolError: if the object format is not correct.
246 if not isinstance(data
, dict):
247 error
= 'JSON data is not a dictionary'
248 elif 'jsonrpc' not in data
or data
['jsonrpc'] != '2.0':
249 error
= 'JSON is not a valid JSON RPC 2.0 message'
250 elif 'id' not in data
:
251 error
= 'JSON data missing required id entry'
253 raise ProtocolError(error
)
256 class Transport(_base
.Transport
):
257 """RPC transport class.
259 This class extends the functionality of xmlrpclib.Transport and only
260 overrides the operations needed to change the protocol from XML-RPC to
264 user_agent
= 'jsonrpclib.py/' + __version__
266 def send_content(self
, connection
, request_body
):
267 """Send the request."""
268 connection
.putheader('Content-Type','application/json')
270 #optionally encode the request
271 if (self
.encode_threshold
is not None and
272 self
.encode_threshold
< len(request_body
) and
274 connection
.putheader('Content-Encoding', 'gzip')
275 request_body
= gzip_encode(request_body
)
277 connection
.putheader('Content-Length', str(len(request_body
)))
278 connection
.endheaders(request_body
)
280 def single_request(self
, host
, handler
, request_body
, verbose
=0):
281 """Issue a single JSON-RPC request."""
283 h
= self
.make_connection(host
)
287 self
.send_request(h
, handler
, request_body
)
288 self
.send_host(h
, host
)
289 self
.send_user_agent(h
)
290 self
.send_content(h
, request_body
)
292 response
= h
.getresponse(buffering
=True)
293 if response
.status
== 200:
294 self
.verbose
= verbose
#pylint: disable=attribute-defined-outside-init
296 return self
.parse_response(response
)
301 # All unexpected errors leave connection in
302 # a strange state, so we clear it.
306 # discard any response data and raise exception
307 if response
.getheader('content-length', 0):
311 response
.status
, response
.reason
,
315 def parse_response(self
, response
):
316 """Parse the HTTP resoponse from the server."""
317 return ParseHTTPResponse(response
)
320 class SafeTransport(_base
.SafeTransport
):
321 """Transport class for HTTPS servers.
323 This class extends the functionality of xmlrpclib.SafeTransport and only
324 overrides the operations needed to change the protocol from XML-RPC to
328 def parse_response(self
, response
):
329 return ParseHTTPResponse(response
)
332 class ServerProxy(_base
.ServerProxy
):
333 """Proxy class to the RPC server.
335 This class extends the functionality of xmlrpclib.ServerProxy and only
336 overrides the operations needed to change the protocol from XML-RPC to
340 def __init__(self
, uri
, transport
=None, encoding
=None, verbose
=0,
341 allow_none
=0, use_datetime
=0):
342 urltype
, _
= urllib
.splittype(uri
)
343 if urltype
not in ('http', 'https'):
344 raise IOError('unsupported JSON-RPC protocol')
346 _base
.ServerProxy
.__init
__(self
, uri
, transport
, encoding
, verbose
,
347 allow_none
, use_datetime
)
348 transport_type
, uri
= urllib
.splittype(uri
)
349 if transport
is None:
350 if transport_type
== 'https':
351 transport
= SafeTransport(use_datetime
=use_datetime
)
353 transport
= Transport(use_datetime
=use_datetime
)
354 self
.__transport
= transport
356 def __request(self
, methodname
, params
):
357 """Call a method on the remote server."""
358 request
= CreateRequestString(methodname
, params
)
360 response
= self
.__transport
.request(
364 verbose
=self
.__verbose