OLD | NEW |
(Empty) | |
| 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. |
| 5 |
| 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. |
| 9 |
| 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 |
| 12 change to a minimum. |
| 13 |
| 14 This module only depends on default Python modules. No third party code is |
| 15 required to use this module. |
| 16 """ |
| 17 import json |
| 18 import urllib |
| 19 import xmlrpclib as _base |
| 20 |
| 21 __version__ = '1.0.0' |
| 22 gzip_encode = _base.gzip_encode |
| 23 |
| 24 |
| 25 class Error(Exception): |
| 26 |
| 27 def __str__(self): |
| 28 return repr(self) |
| 29 |
| 30 |
| 31 class ProtocolError(Error): |
| 32 """Indicates a JSON protocol error.""" |
| 33 |
| 34 def __init__(self, url, errcode, errmsg, headers): |
| 35 Error.__init__(self) |
| 36 self.url = url |
| 37 self.errcode = errcode |
| 38 self.errmsg = errmsg |
| 39 self.headers = headers |
| 40 |
| 41 def __repr__(self): |
| 42 return ( |
| 43 '<ProtocolError for %s: %s %s>' % |
| 44 (self.url, self.errcode, self.errmsg)) |
| 45 |
| 46 |
| 47 class ResponseError(Error): |
| 48 """Indicates a broken response package.""" |
| 49 pass |
| 50 |
| 51 |
| 52 class Fault(Error): |
| 53 """Indicates an JSON-RPC fault package.""" |
| 54 |
| 55 def __init__(self, code, message): |
| 56 Error.__init__(self) |
| 57 if not isinstance(code, int): |
| 58 raise ProtocolError('Fault code must be an integer.') |
| 59 self.code = code |
| 60 self.message = message |
| 61 |
| 62 def __repr__(self): |
| 63 return ( |
| 64 '<Fault %s: %s>' % |
| 65 (self.code, repr(self.message)) |
| 66 ) |
| 67 |
| 68 |
| 69 def CreateRequest(methodname, params, ident=''): |
| 70 """Create a valid JSON-RPC request. |
| 71 |
| 72 Args: |
| 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. |
| 76 |
| 77 Returns: |
| 78 A valid JSON-RPC request object. |
| 79 """ |
| 80 request = { |
| 81 'jsonrpc': '2.0', |
| 82 'method': methodname, |
| 83 'params': params, |
| 84 'id': ident |
| 85 } |
| 86 |
| 87 return request |
| 88 |
| 89 |
| 90 def CreateRequestString(methodname, params, ident=''): |
| 91 """Create a valid JSON-RPC request string. |
| 92 |
| 93 Args: |
| 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. |
| 98 |
| 99 Returns: |
| 100 A valid JSON-RPC request string. |
| 101 """ |
| 102 return json.dumps(CreateRequest(methodname, params, ident)) |
| 103 |
| 104 def CreateResponse(data, ident): |
| 105 """Create a JSON-RPC response. |
| 106 |
| 107 Args: |
| 108 data: The data to return. |
| 109 ident: The response identifier. |
| 110 |
| 111 Returns: |
| 112 A valid JSON-RPC response object. |
| 113 """ |
| 114 if isinstance(data, Fault): |
| 115 response = { |
| 116 'jsonrpc': '2.0', |
| 117 'error': { |
| 118 'code': data.code, |
| 119 'message': data.message}, |
| 120 'id': ident |
| 121 } |
| 122 else: |
| 123 response = { |
| 124 'jsonrpc': '2.0', |
| 125 'response': data, |
| 126 'id': ident |
| 127 } |
| 128 |
| 129 return response |
| 130 |
| 131 |
| 132 def CreateResponseString(data, ident): |
| 133 """Create a JSON-RPC response string. |
| 134 |
| 135 Args: |
| 136 data: The data to return. |
| 137 ident: The response identifier. |
| 138 |
| 139 Returns: |
| 140 A valid JSON-RPC response object. |
| 141 """ |
| 142 return json.dumps(CreateResponse(data, ident)) |
| 143 |
| 144 |
| 145 def ParseHTTPResponse(response): |
| 146 """Parse an HTTP response object and return the JSON object. |
| 147 |
| 148 Args: |
| 149 response: An HTTP response object. |
| 150 |
| 151 Returns: |
| 152 The returned JSON-RPC object. |
| 153 |
| 154 Raises: |
| 155 ProtocolError: if the object format is not correct. |
| 156 Fault: If a Fault error is returned from the server. |
| 157 """ |
| 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) |
| 162 else: |
| 163 stream = response |
| 164 else: |
| 165 stream = response |
| 166 |
| 167 data = '' |
| 168 while 1: |
| 169 chunk = stream.read(1024) |
| 170 if not chunk: |
| 171 break |
| 172 data += chunk |
| 173 |
| 174 response = json.loads(data) |
| 175 ValidateBasicJSONRPCData(response) |
| 176 |
| 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) |
| 185 else: |
| 186 raise ProtocolError('No valid JSON returned') |
| 187 |
| 188 |
| 189 def ValidateRequest(data): |
| 190 """Validate a JSON-RPC request object. |
| 191 |
| 192 Args: |
| 193 data: The JSON-RPC object (dict). |
| 194 |
| 195 Raises: |
| 196 ProtocolError: if the object format is not correct. |
| 197 """ |
| 198 ValidateBasicJSONRPCData(data) |
| 199 if 'method' not in data or 'params' not in data: |
| 200 raise ProtocolError('JSON is not a valid request') |
| 201 |
| 202 |
| 203 def ValidateResponse(data): |
| 204 """Validate a JSON-RPC response object. |
| 205 |
| 206 Args: |
| 207 data: The JSON-RPC object (dict). |
| 208 |
| 209 Raises: |
| 210 ProtocolError: if the object format is not correct. |
| 211 """ |
| 212 ValidateBasicJSONRPCData(data) |
| 213 if 'response' not in data: |
| 214 raise ProtocolError('JSON is not a valid response') |
| 215 |
| 216 |
| 217 def ValidateError(data): |
| 218 """Validate a JSON-RPC error object. |
| 219 |
| 220 Args: |
| 221 data: The JSON-RPC object (dict). |
| 222 |
| 223 Raises: |
| 224 ProtocolError: if the object format is not correct. |
| 225 """ |
| 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') |
| 231 |
| 232 |
| 233 def ValidateBasicJSONRPCData(data): |
| 234 """Validate a basic JSON-RPC object. |
| 235 |
| 236 Args: |
| 237 data: The JSON-RPC object (dict). |
| 238 |
| 239 Raises: |
| 240 ProtocolError: if the object format is not correct. |
| 241 """ |
| 242 error = None |
| 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' |
| 249 if error: |
| 250 raise ProtocolError(error) |
| 251 |
| 252 |
| 253 class Transport(_base.Transport): |
| 254 """RPC transport class. |
| 255 |
| 256 This class extends the functionality of xmlrpclib.Transport and only |
| 257 overrides the operations needed to change the protocol from XML-RPC to |
| 258 JSON-RPC. |
| 259 """ |
| 260 |
| 261 user_agent = 'jsonrpclib.py/' + __version__ |
| 262 |
| 263 def send_content(self, connection, request_body): |
| 264 """Send the request.""" |
| 265 connection.putheader('Content-Type','application/json') |
| 266 |
| 267 #optionally encode the request |
| 268 if (self.encode_threshold is not None and |
| 269 self.encode_threshold < len(request_body) and |
| 270 gzip): |
| 271 connection.putheader('Content-Encoding', 'gzip') |
| 272 request_body = gzip_encode(request_body) |
| 273 |
| 274 connection.putheader('Content-Length', str(len(request_body))) |
| 275 connection.endheaders(request_body) |
| 276 |
| 277 def single_request(self, host, handler, request_body, verbose=0): |
| 278 """Issue a single JSON-RPC request.""" |
| 279 |
| 280 h = self.make_connection(host) |
| 281 if verbose: |
| 282 h.set_debuglevel(1) |
| 283 try: |
| 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) |
| 288 |
| 289 response = h.getresponse(buffering=True) |
| 290 if response.status == 200: |
| 291 self.verbose = verbose |
| 292 |
| 293 return self.parse_response(response) |
| 294 |
| 295 except Fault: |
| 296 raise |
| 297 except Exception: |
| 298 # All unexpected errors leave connection in |
| 299 # a strange state, so we clear it. |
| 300 self.close() |
| 301 raise |
| 302 |
| 303 # discard any response data and raise exception |
| 304 if response.getheader('content-length', 0): |
| 305 response.read() |
| 306 raise ProtocolError( |
| 307 host + handler, |
| 308 response.status, response.reason, |
| 309 response.msg, |
| 310 ) |
| 311 |
| 312 def parse_response(self, response): |
| 313 """Parse the HTTP resoponse from the server.""" |
| 314 return ParseHTTPResponse(response) |
| 315 |
| 316 |
| 317 class SafeTransport(_base.SafeTransport): |
| 318 """Transport class for HTTPS servers. |
| 319 |
| 320 This class extends the functionality of xmlrpclib.SafeTransport and only |
| 321 overrides the operations needed to change the protocol from XML-RPC to |
| 322 JSON-RPC. |
| 323 """ |
| 324 |
| 325 def parse_response(self, response): |
| 326 return ParseHTTPResponse(response) |
| 327 |
| 328 |
| 329 class ServerProxy(_base.ServerProxy): |
| 330 """Proxy class to the RPC server. |
| 331 |
| 332 This class extends the functionality of xmlrpclib.ServerProxy and only |
| 333 overrides the operations needed to change the protocol from XML-RPC to |
| 334 JSON-RPC. |
| 335 """ |
| 336 |
| 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') |
| 342 |
| 343 _base.ServerProxy.__init__(self, uri, transport, encoding, verbose, |
| 344 allow_none, use_datetime) |
| 345 |
| 346 if transport is None: |
| 347 if type == 'https': |
| 348 transport = SafeTransport(use_datetime=use_datetime) |
| 349 else: |
| 350 transport = Transport(use_datetime=use_datetime) |
| 351 self.__transport = transport |
| 352 |
| 353 def __request(self, methodname, params): |
| 354 """Call a method on the remote server.""" |
| 355 request = CreateRequestString(methodname, params) |
| 356 |
| 357 response = self.__transport.request( |
| 358 self.__host, |
| 359 self.__handler, |
| 360 request, |
| 361 verbose=self.__verbose |
| 362 ) |
| 363 |
| 364 return response |
OLD | NEW |