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.
4 """Module to implement the JSON-RPC protocol.
6 This module uses xmlrpclib as the base and only overrides those
7 portions that implement the XML-RPC protocol. These portions are rewritten
8 to use the JSON-RPC protocol instead.
10 When large portions of code need to be rewritten the original code and
11 comments are preserved. The intention here is to keep the amount of code
14 This module only depends on default Python modules. No third party code is
15 required to use this module.
19 import xmlrpclib
as _base
22 gzip_encode
= _base
.gzip_encode
25 class Error(Exception):
31 class ProtocolError(Error
):
32 """Indicates a JSON protocol error."""
34 def __init__(self
, url
, errcode
, errmsg
, headers
):
37 self
.errcode
= errcode
39 self
.headers
= headers
43 '<ProtocolError for %s: %s %s>' %
44 (self
.url
, self
.errcode
, self
.errmsg
))
47 class ResponseError(Error
):
48 """Indicates a broken response package."""
53 """Indicates an JSON-RPC fault package."""
55 def __init__(self
, code
, message
):
57 if not isinstance(code
, int):
58 raise ProtocolError('Fault code must be an integer.')
60 self
.message
= message
65 (self
.code
, repr(self
.message
))
69 def CreateRequest(methodname
, params
, ident
=''):
70 """Create a valid JSON-RPC request.
73 methodname: The name of the remote method to invoke.
74 params: The parameters to pass to the remote method. This should be a
75 list or tuple and able to be encoded by the default JSON parser.
78 A valid JSON-RPC request object.
90 def CreateRequestString(methodname
, params
, ident
=''):
91 """Create a valid JSON-RPC request string.
94 methodname: The name of the remote method to invoke.
95 params: The parameters to pass to the remote method.
96 These parameters need to be encode-able by the default JSON parser.
97 ident: The request identifier.
100 A valid JSON-RPC request string.
102 return json
.dumps(CreateRequest(methodname
, params
, ident
))
104 def CreateResponse(data
, ident
):
105 """Create a JSON-RPC response.
108 data: The data to return.
109 ident: The response identifier.
112 A valid JSON-RPC response object.
114 if isinstance(data
, Fault
):
119 'message': data
.message
},
132 def CreateResponseString(data
, ident
):
133 """Create a JSON-RPC response string.
136 data: The data to return.
137 ident: The response identifier.
140 A valid JSON-RPC response object.
142 return json
.dumps(CreateResponse(data
, ident
))
145 def ParseHTTPResponse(response
):
146 """Parse an HTTP response object and return the JSON object.
149 response: An HTTP response object.
152 The returned JSON-RPC object.
155 ProtocolError: if the object format is not correct.
156 Fault: If a Fault error is returned from the server.
158 # Check for new http response object, else it is a file object
159 if hasattr(response
, 'getheader'):
160 if response
.getheader('Content-Encoding', '') == 'gzip':
161 stream
= _base
.GzipDecodedResponse(response
)
169 chunk
= stream
.read(1024)
174 response
= json
.loads(data
)
175 ValidateBasicJSONRPCData(response
)
177 if 'response' in response
:
178 ValidateResponse(response
)
179 return response
['response']
180 elif 'error' in response
:
181 ValidateError(response
)
182 code
= response
['error']['code']
183 message
= response
['error']['message']
184 raise Fault(code
, message
)
186 raise ProtocolError('No valid JSON returned')
189 def ValidateRequest(data
):
190 """Validate a JSON-RPC request object.
193 data: The JSON-RPC object (dict).
196 ProtocolError: if the object format is not correct.
198 ValidateBasicJSONRPCData(data
)
199 if 'method' not in data
or 'params' not in data
:
200 raise ProtocolError('JSON is not a valid request')
203 def ValidateResponse(data
):
204 """Validate a JSON-RPC response object.
207 data: The JSON-RPC object (dict).
210 ProtocolError: if the object format is not correct.
212 ValidateBasicJSONRPCData(data
)
213 if 'response' not in data
:
214 raise ProtocolError('JSON is not a valid response')
217 def ValidateError(data
):
218 """Validate a JSON-RPC error object.
221 data: The JSON-RPC object (dict).
224 ProtocolError: if the object format is not correct.
226 ValidateBasicJSONRPCData(data
)
227 if ('error' not in data
or
228 'code' not in data
['error'] or
229 'message' not in data
['error']):
230 raise ProtocolError('JSON is not a valid error response')
233 def ValidateBasicJSONRPCData(data
):
234 """Validate a basic JSON-RPC object.
237 data: The JSON-RPC object (dict).
240 ProtocolError: if the object format is not correct.
243 if not isinstance(data
, dict):
244 error
= 'JSON data is not a dictionary'
245 elif 'jsonrpc' not in data
or data
['jsonrpc'] != '2.0':
246 error
= 'JSON is not a valid JSON RPC 2.0 message'
247 elif 'id' not in data
:
248 error
= 'JSON data missing required id entry'
250 raise ProtocolError(error
)
253 class Transport(_base
.Transport
):
254 """RPC transport class.
256 This class extends the functionality of xmlrpclib.Transport and only
257 overrides the operations needed to change the protocol from XML-RPC to
261 user_agent
= 'jsonrpclib.py/' + __version__
263 def send_content(self
, connection
, request_body
):
264 """Send the request."""
265 connection
.putheader('Content-Type','application/json')
267 #optionally encode the request
268 if (self
.encode_threshold
is not None and
269 self
.encode_threshold
< len(request_body
) and
271 connection
.putheader('Content-Encoding', 'gzip')
272 request_body
= gzip_encode(request_body
)
274 connection
.putheader('Content-Length', str(len(request_body
)))
275 connection
.endheaders(request_body
)
277 def single_request(self
, host
, handler
, request_body
, verbose
=0):
278 """Issue a single JSON-RPC request."""
280 h
= self
.make_connection(host
)
284 self
.send_request(h
, handler
, request_body
)
285 self
.send_host(h
, host
)
286 self
.send_user_agent(h
)
287 self
.send_content(h
, request_body
)
289 response
= h
.getresponse(buffering
=True)
290 if response
.status
== 200:
291 self
.verbose
= verbose
293 return self
.parse_response(response
)
298 # All unexpected errors leave connection in
299 # a strange state, so we clear it.
303 # discard any response data and raise exception
304 if response
.getheader('content-length', 0):
308 response
.status
, response
.reason
,
312 def parse_response(self
, response
):
313 """Parse the HTTP resoponse from the server."""
314 return ParseHTTPResponse(response
)
317 class SafeTransport(_base
.SafeTransport
):
318 """Transport class for HTTPS servers.
320 This class extends the functionality of xmlrpclib.SafeTransport and only
321 overrides the operations needed to change the protocol from XML-RPC to
325 def parse_response(self
, response
):
326 return ParseHTTPResponse(response
)
329 class ServerProxy(_base
.ServerProxy
):
330 """Proxy class to the RPC server.
332 This class extends the functionality of xmlrpclib.ServerProxy and only
333 overrides the operations needed to change the protocol from XML-RPC to
337 def __init__(self
, uri
, transport
=None, encoding
=None, verbose
=0,
338 allow_none
=0, use_datetime
=0):
339 urltype
, _
= urllib
.splittype(uri
)
340 if urltype
not in ('http', 'https'):
341 raise IOError('unsupported JSON-RPC protocol')
343 _base
.ServerProxy
.__init
__(self
, uri
, transport
, encoding
, verbose
,
344 allow_none
, use_datetime
)
346 if transport
is None:
348 transport
= SafeTransport(use_datetime
=use_datetime
)
350 transport
= Transport(use_datetime
=use_datetime
)
351 self
.__transport
= transport
353 def __request(self
, methodname
, params
):
354 """Call a method on the remote server."""
355 request
= CreateRequestString(methodname
, params
)
357 response
= self
.__transport
.request(
361 verbose
=self
.__verbose