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 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 } | |
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
| |
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 | |
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.
| |
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) | |
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.
| |
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 |