OLD | NEW |
(Empty) | |
| 1 # Copyright (c) 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 gzip_encode = _base.gzip_encode |
| 22 |
| 23 |
| 24 class Error(Exception): |
| 25 |
| 26 def __str__(self): |
| 27 return repr(self) |
| 28 |
| 29 |
| 30 class ProtocolError(Error): |
| 31 """Indicates a JSON protocol error.""" |
| 32 |
| 33 def __init__(self, url, errcode, errmsg, headers): |
| 34 Error.__init__(self) |
| 35 self.url = url |
| 36 self.errcode = errcode |
| 37 self.errmsg = errmsg |
| 38 self.headers = headers |
| 39 |
| 40 def __repr__(self): |
| 41 return ( |
| 42 '<ProtocolError for %s: %s %s>' % |
| 43 (self.url, self.errcode, self.errmsg)) |
| 44 |
| 45 |
| 46 class ResponseError(Error): |
| 47 """Indicates a broken response package.""" |
| 48 pass |
| 49 |
| 50 |
| 51 class Fault(Error): |
| 52 """Indicates an JSON-RPC fault package.""" |
| 53 |
| 54 def __init__(self, code, message): |
| 55 Error.__init__(self) |
| 56 if not isinstance(code, int): |
| 57 raise ProtocolError('Fault code must be an integer.') |
| 58 self.code = code |
| 59 self.message = message |
| 60 |
| 61 def __repr__(self): |
| 62 return ( |
| 63 '<Fault %s: %s>' % |
| 64 (self.code, repr(self.message)) |
| 65 ) |
| 66 |
| 67 |
| 68 def CreateRequest(methodname, params, ident=''): |
| 69 """Create a valid JSON-RPC request. |
| 70 |
| 71 Args: |
| 72 methodname: The name of the remote method to invoke. |
| 73 params: The parameters to pass to the remote method. This should be a |
| 74 list or tuple and able to be encoded by the default JSON parser. |
| 75 |
| 76 Returns: |
| 77 A valid JSON-RPC request object. |
| 78 """ |
| 79 request = { |
| 80 'jsonrpc': '2.0', |
| 81 'method': methodname, |
| 82 'params': params, |
| 83 'id': ident |
| 84 } |
| 85 |
| 86 return request |
| 87 |
| 88 |
| 89 def CreateRequestString(methodname, params, ident=''): |
| 90 """Create a valid JSON-RPC request string. |
| 91 |
| 92 Args: |
| 93 methodname: The name of the remote method to invoke. |
| 94 params: The parameters to pass to the remote method. |
| 95 These parameters need to be encode-able by the default JSON parser. |
| 96 ident: The request identifier. |
| 97 |
| 98 Returns: |
| 99 A valid JSON-RPC request string. |
| 100 """ |
| 101 return json.dumps(CreateRequest(methodname, params, ident)) |
| 102 |
| 103 def CreateResponse(data, ident): |
| 104 """Create a JSON-RPC response. |
| 105 |
| 106 Args: |
| 107 data: The data to return. |
| 108 ident: The response identifier. |
| 109 |
| 110 Returns: |
| 111 A valid JSON-RPC response object. |
| 112 """ |
| 113 if isinstance(data, Fault): |
| 114 response = { |
| 115 'jsonrpc': '2.0', |
| 116 'error': { |
| 117 'code': data.code, |
| 118 'message': data.message}, |
| 119 'id': ident |
| 120 } |
| 121 else: |
| 122 response = { |
| 123 'jsonrpc': '2.0', |
| 124 'response': data, |
| 125 'id': ident |
| 126 } |
| 127 |
| 128 return response |
| 129 |
| 130 |
| 131 def CreateResponseString(data, ident): |
| 132 """Create a JSON-RPC response string. |
| 133 |
| 134 Args: |
| 135 data: The data to return. |
| 136 ident: The response identifier. |
| 137 |
| 138 Returns: |
| 139 A valid JSON-RPC response object. |
| 140 """ |
| 141 return json.dumps(CreateResponse(data, ident)) |
| 142 |
| 143 |
| 144 def ParseHTTPResponse(response): |
| 145 """Parse an HTTP response object and return the JSON object. |
| 146 |
| 147 Args: |
| 148 response: An HTTP response object. |
| 149 |
| 150 Returns: |
| 151 The returned JSON-RPC object. |
| 152 |
| 153 Raises: |
| 154 ProtocolError: if the object format is not correct. |
| 155 Fault: If a Fault error is returned from the server. |
| 156 """ |
| 157 # Check for new http response object, else it is a file object |
| 158 if hasattr(response, 'getheader'): |
| 159 if response.getheader('Content-Encoding', '') == 'gzip': |
| 160 stream = _base.GzipDecodedResponse(response) |
| 161 else: |
| 162 stream = response |
| 163 else: |
| 164 stream = response |
| 165 |
| 166 data = '' |
| 167 while 1: |
| 168 chunk = stream.read(1024) |
| 169 if not chunk: |
| 170 break |
| 171 data += chunk |
| 172 |
| 173 response = json.loads(data) |
| 174 ValidateBasicJSONRPCData(response) |
| 175 |
| 176 if 'response' in response: |
| 177 ValidateResponse(response) |
| 178 return response['response'] |
| 179 elif 'error' in response: |
| 180 ValidateError(response) |
| 181 code = response['error']['code'] |
| 182 message = response['error']['message'] |
| 183 raise Fault(code, message) |
| 184 else: |
| 185 raise ProtocolError('No valid JSON returned') |
| 186 |
| 187 |
| 188 def ValidateRequest(data): |
| 189 """Validate a JSON-RPC request object. |
| 190 |
| 191 Args: |
| 192 data: The JSON-RPC object (dict). |
| 193 |
| 194 Raises: |
| 195 ProtocolError: if the object format is not correct. |
| 196 """ |
| 197 ValidateBasicJSONRPCData(data) |
| 198 if 'method' not in data or 'params' not in data: |
| 199 raise ProtocolError('JSON is not a valid request') |
| 200 |
| 201 |
| 202 def ValidateResponse(data): |
| 203 """Validate a JSON-RPC response object. |
| 204 |
| 205 Args: |
| 206 data: The JSON-RPC object (dict). |
| 207 |
| 208 Raises: |
| 209 ProtocolError: if the object format is not correct. |
| 210 """ |
| 211 ValidateBasicJSONRPCData(data) |
| 212 if 'response' not in data: |
| 213 raise ProtocolError('JSON is not a valid response') |
| 214 |
| 215 |
| 216 def ValidateError(data): |
| 217 """Validate a JSON-RPC error object. |
| 218 |
| 219 Args: |
| 220 data: The JSON-RPC object (dict). |
| 221 |
| 222 Raises: |
| 223 ProtocolError: if the object format is not correct. |
| 224 """ |
| 225 ValidateBasicJSONRPCData(data) |
| 226 if ('error' not in data or |
| 227 'code' not in data['error'] or |
| 228 'message' not in data['error']): |
| 229 raise ProtocolError('JSON is not a valid error response') |
| 230 |
| 231 |
| 232 def ValidateBasicJSONRPCData(data): |
| 233 """Validate a basic JSON-RPC object. |
| 234 |
| 235 Args: |
| 236 data: The JSON-RPC object (dict). |
| 237 |
| 238 Raises: |
| 239 ProtocolError: if the object format is not correct. |
| 240 """ |
| 241 error = None |
| 242 if not isinstance(data, dict): |
| 243 error = 'JSON data is not a dictionary' |
| 244 elif 'jsonrpc' not in data or data['jsonrpc'] != '2.0': |
| 245 error = 'JSON is not a valid JSON RPC 2.0 message' |
| 246 elif 'id' not in data: |
| 247 error = 'JSON data missing required id entry' |
| 248 if error: |
| 249 raise ProtocolError(error) |
| 250 |
| 251 |
| 252 class Transport(_base.Transport): |
| 253 """RPC transport class. |
| 254 |
| 255 This class extends the functionality of xmlrpclib.Transport and only |
| 256 overrides the operations needed to change the protocol from XML-RPC to |
| 257 JSON-RPC. |
| 258 """ |
| 259 |
| 260 def single_request(self, host, handler, request_body, verbose=0): |
| 261 """Issue a single JSON-RPC request.""" |
| 262 |
| 263 h = self.make_connection(host) |
| 264 if verbose: |
| 265 h.set_debuglevel(1) |
| 266 try: |
| 267 self.send_request(h, handler, request_body) |
| 268 self.send_host(h, host) |
| 269 self.send_user_agent(h) |
| 270 self.send_content(h, request_body) |
| 271 |
| 272 response = h.getresponse(buffering=True) |
| 273 if response.status == 200: |
| 274 self.verbose = verbose |
| 275 |
| 276 return self.parse_response(response) |
| 277 |
| 278 except Fault: |
| 279 raise |
| 280 except Exception: |
| 281 # All unexpected errors leave connection in |
| 282 # a strange state, so we clear it. |
| 283 self.close() |
| 284 raise |
| 285 |
| 286 # discard any response data and raise exception |
| 287 if response.getheader('content-length', 0): |
| 288 response.read() |
| 289 raise ProtocolError( |
| 290 host + handler, |
| 291 response.status, response.reason, |
| 292 response.msg, |
| 293 ) |
| 294 |
| 295 def parse_response(self, response): |
| 296 """Parse the HTTP resoponse from the server.""" |
| 297 return ParseHTTPResponse(response) |
| 298 |
| 299 |
| 300 class SafeTransport(_base.SafeTransport): |
| 301 """Transport class for HTTPS servers. |
| 302 |
| 303 This class extends the functionality of xmlrpclib.SafeTransport and only |
| 304 overrides the operations needed to change the protocol from XML-RPC to |
| 305 JSON-RPC. |
| 306 """ |
| 307 |
| 308 def parse_response(self, response): |
| 309 return ParseHTTPResponse(response) |
| 310 |
| 311 |
| 312 class ServerProxy(_base.ServerProxy): |
| 313 """Proxy class to the RPC server. |
| 314 |
| 315 This class extends the functionality of xmlrpclib.ServerProxy and only |
| 316 overrides the operations needed to change the protocol from XML-RPC to |
| 317 JSON-RPC. |
| 318 """ |
| 319 |
| 320 def __init__(self, uri, transport=None, encoding=None, verbose=0, |
| 321 allow_none=0, use_datetime=0): |
| 322 urltype, _ = urllib.splittype(uri) |
| 323 if urltype not in ('http', 'https'): |
| 324 raise IOError('unsupported JSON-RPC protocol') |
| 325 |
| 326 _base.ServerProxy.__init__(self, uri, transport, encoding, verbose, |
| 327 allow_none, use_datetime) |
| 328 |
| 329 if transport is None: |
| 330 if type == 'https': |
| 331 transport = SafeTransport(use_datetime=use_datetime) |
| 332 else: |
| 333 transport = Transport(use_datetime=use_datetime) |
| 334 self.__transport = transport |
| 335 |
| 336 def __request(self, methodname, params): |
| 337 """Call a method on the remote server.""" |
| 338 request = CreateRequestString(methodname, params) |
| 339 |
| 340 response = self.__transport.request( |
| 341 self.__host, |
| 342 self.__handler, |
| 343 request, |
| 344 verbose=self.__verbose |
| 345 ) |
| 346 |
| 347 return response |
OLD | NEW |