/tests/web/jsonrpclib.py
Python | 454 lines | 319 code | 56 blank | 79 comment | 9 complexity | caa317051c53564764f7608a3de1d17a MD5 | raw file
1# a port of xmlrpclib to json.... 2# 3# 4# The JSON-RPC client interface is based on the XML-RPC client 5# 6# Copyright (c) 1999-2002 by Secret Labs AB 7# Copyright (c) 1999-2002 by Fredrik Lundh 8# Copyright (c) 2006 by Matt Harrison 9# 10# By obtaining, using, and/or copying this software and/or its 11# associated documentation, you agree that you have read, understood, 12# and will comply with the following terms and conditions: 13# 14# Permission to use, copy, modify, and distribute this software and 15# its associated documentation for any purpose and without fee is 16# hereby granted, provided that the above copyright notice appears in 17# all copies, and that both that copyright notice and this permission 18# notice appear in supporting documentation, and that the name of 19# Secret Labs AB or the author not be used in advertising or publicity 20# pertaining to distribution of the software without specific, written 21# prior permission. 22# 23# SECRET LABS AB AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD 24# TO THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANT- 25# ABILITY AND FITNESS. IN NO EVENT SHALL SECRET LABS AB OR THE AUTHOR 26# BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY 27# DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, 28# WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS 29# ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE 30# OF THIS SOFTWARE. 31# -------------------------------------------------------------------- 32 33import sys 34import json 35import base64 36 37PY3 = sys.version_info[0] == 3 38 39try: 40 from http.client import HTTPConnection 41 from http.client import HTTPSConnection 42except ImportError: 43 from httplib import HTTP as HTTPConnection # NOQA 44 from httplib import HTTPS as HTTPSConnection # NOQA 45 46try: 47 from urllib.parse import unquote 48 from urllib.parse import splithost, splittype, splituser 49except ImportError: 50 from urllib import unquote # NOQA 51 from urllib import splithost, splittype, splituser # NOQA 52 53__version__ = "0.0.1" 54 55ID = 1 56 57 58def _gen_id(): 59 global ID 60 ID = ID + 1 61 return ID 62 63 64# -------------------------------------------------------------------- 65# Exceptions 66 67## 68# Base class for all kinds of client-side errors. 69 70class Error(Exception): 71 """Base class for client errors.""" 72 def __str__(self): 73 return repr(self) 74 75## 76# Indicates an HTTP-level protocol error. This is raised by the HTTP 77# transport layer, if the server returns an error code other than 200 78# (OK). 79# 80# @param url The target URL. 81# @param errcode The HTTP error code. 82# @param errmsg The HTTP error message. 83# @param headers The HTTP header dictionary. 84 85 86class ProtocolError(Error): 87 """Indicates an HTTP protocol error.""" 88 89 def __init__(self, url, errcode, errmsg, headers, response): 90 Error.__init__(self) 91 self.url = url 92 self.errcode = errcode 93 self.errmsg = errmsg 94 self.headers = headers 95 self.response = response 96 97 def __repr__(self): 98 return ( 99 "<ProtocolError for %s: %s %s>" % 100 (self.url, self.errcode, self.errmsg) 101 ) 102 103 104def getparser(encoding): 105 un = Unmarshaller(encoding) 106 par = Parser(un) 107 return par, un 108 109 110def dumps(params, methodname=None, methodresponse=None, encoding=None, 111 allow_none=0): 112 if methodname: 113 request = {} 114 request["method"] = methodname 115 request["params"] = params 116 request["id"] = _gen_id() 117 return json.dumps(request) 118 119 120class Unmarshaller(object): 121 122 def __init__(self, encoding): 123 self.data = None 124 self.encoding = encoding 125 126 def feed(self, data): 127 if self.data is None: 128 self.data = data 129 else: 130 self.data = self.data + data 131 132 def close(self): 133 #try to convert string to json 134 return json.loads(self.data.decode(self.encoding)) 135 136 137class Parser(object): 138 139 def __init__(self, unmarshaller): 140 self._target = unmarshaller 141 self.data = None 142 143 def feed(self, data): 144 if self.data is None: 145 self.data = data 146 else: 147 self.data = self.data + data 148 149 def close(self): 150 self._target.feed(self.data) 151 152 153class _Method(object): 154 # some magic to bind an JSON-RPC method to an RPC server. 155 # supports "nested" methods (e.g. examples.getStateName) 156 157 def __init__(self, send, name): 158 self.__send = send 159 self.__name = name 160 161 def __getattr__(self, name): 162 return _Method(self.__send, "%s.%s" % (self.__name, name)) 163 164 def __call__(self, *args): 165 return self.__send(self.__name, args) 166 167## 168# Standard transport class for JSON-RPC over HTTP. 169# <p> 170# You can create custom transports by subclassing this method, and 171# overriding selected methods. 172 173 174class Transport: 175 """Handles an HTTP transaction to an JSON-RPC server.""" 176 177 # client identifier (may be overridden) 178 user_agent = "jsonlib.py/%s (by matt harrison)" % __version__ 179 180 ## 181 # Send a complete request, and parse the response. 182 # 183 # @param host Target host. 184 # @param handler Target PRC handler. 185 # @param request_body JSON-RPC request body. 186 # @param verbose Debugging flag. 187 # @return Parsed response. 188 189 def request(self, host, handler, request_body, encoding, verbose=0): 190 # issue JSON-RPC request 191 192 h = self.make_connection(host) 193 if verbose: 194 h.set_debuglevel(1) 195 196 self.send_request(h, handler, request_body) 197 if not PY3: 198 self.send_host(h, host) 199 self.send_user_agent(h) 200 self.send_content(h, request_body) 201 202 try: 203 errcode, errmsg, headers = h.getreply() 204 r = h.getfile() 205 except AttributeError: 206 r = h.getresponse() 207 errcode = r.status 208 errmsg = r.reason 209 headers = r.getheaders() 210 211 if errcode != 200: 212 response = r.read() 213 raise ProtocolError( 214 host + handler, 215 errcode, errmsg, 216 headers, 217 response 218 ) 219 220 self.verbose = verbose 221 222 try: 223 sock = h._conn.sock 224 except AttributeError: 225 sock = None 226 227 return self._parse_response(r, sock, encoding) 228 229 ## 230 # Create parser. 231 # 232 # @return A 2-tuple containing a parser and a unmarshaller. 233 234 def getparser(self, encoding): 235 # get parser and unmarshaller 236 return getparser(encoding) 237 238 ## 239 # Get authorization info from host parameter 240 # Host may be a string, or a (host, x509-dict) tuple; if a string, 241 # it is checked for a "user:pw@host" format, and a "Basic 242 # Authentication" header is added if appropriate. 243 # 244 # @param host Host descriptor (URL or (URL, x509 info) tuple). 245 # @return A 3-tuple containing (actual host, extra headers, 246 # x509 info). The header and x509 fields may be None. 247 248 def get_host_info(self, host): 249 250 x509 = {} 251 if isinstance(host, tuple): 252 host, x509 = host 253 254 auth, host = splituser(host) 255 256 if auth: 257 auth = base64.encodestring(unquote(auth)) 258 auth = "".join(auth.split()) # get rid of whitespace 259 extra_headers = [ 260 ("Authorization", "Basic " + auth) 261 ] 262 else: 263 extra_headers = None 264 265 return host, extra_headers, x509 266 267 ## 268 # Connect to server. 269 # 270 # @param host Target host. 271 # @return A connection handle. 272 273 def make_connection(self, host): 274 # create a HTTP connection object from a host descriptor 275 host, extra_headers, x509 = self.get_host_info(host) 276 return HTTPConnection(host) 277 278 ## 279 # Send request header. 280 # 281 # @param connection Connection handle. 282 # @param handler Target RPC handler. 283 # @param request_body JSON-RPC body. 284 285 def send_request(self, connection, handler, request_body): 286 connection.putrequest("POST", handler) 287 288 ## 289 # Send host name. 290 # 291 # @param connection Connection handle. 292 # @param host Host name. 293 294 def send_host(self, connection, host): 295 host, extra_headers, x509 = self.get_host_info(host) 296 connection.putheader("Host", host) 297 if extra_headers: 298 if isinstance(extra_headers, dict): 299 extra_headers = list(extra_headers.items()) 300 for key, value in extra_headers: 301 connection.putheader(key, value) 302 303 ## 304 # Send user-agent identifier. 305 # 306 # @param connection Connection handle. 307 308 def send_user_agent(self, connection): 309 connection.putheader("User-Agent", self.user_agent) 310 311 ## 312 # Send request body. 313 # 314 # @param connection Connection handle. 315 # @param request_body JSON-RPC request body. 316 317 def send_content(self, connection, request_body): 318 connection.putheader("Content-Type", "text/xml") 319 connection.putheader("Content-Length", str(len(request_body))) 320 connection.endheaders() 321 if request_body: 322 connection.send(request_body) 323 324 ## 325 # Parse response. 326 # 327 # @param file Stream. 328 # @return Response tuple and target method. 329 330 def parse_response(self, file): 331 # compatibility interface 332 return self._parse_response(file, None) 333 334 ## 335 # Parse response (alternate interface). This is similar to the 336 # parse_response method, but also provides direct access to the 337 # underlying socket object (where available). 338 # 339 # @param file Stream. 340 # @param sock Socket handle (or None, if the socket object 341 # could not be accessed). 342 # @return Response tuple and target method. 343 344 def _parse_response(self, file, sock, encoding): 345 # read response from input file/socket, and parse it 346 347 p, u = self.getparser(encoding) 348 349 while 1: 350 if sock: 351 response = sock.recv(1024) 352 else: 353 response = file.read(1024) 354 if not response: 355 break 356 if self.verbose: 357 print("body:", repr(response)) 358 p.feed(response) 359 360 file.close() 361 p.close() 362 363 return u.close() 364 365## 366# Standard transport class for JSON-RPC over HTTPS. 367 368 369class SafeTransport(Transport): 370 """Handles an HTTPS transaction to an JSON-RPC server.""" 371 372 # FIXME: mostly untested 373 374 def make_connection(self, host): 375 # create a HTTPS connection object from a host descriptor 376 # host may be a string, or a (host, x509-dict) tuple 377 host, extra_headers, x509 = self.get_host_info(host) 378 try: 379 HTTPS = HTTPSConnection 380 except AttributeError: 381 raise NotImplementedError( 382 "your version of httplib doesn't support HTTPS" 383 ) 384 else: 385 return HTTPS(host, None, **(x509 or {})) 386 387 388class ServerProxy(object): 389 390 def __init__(self, uri, transport=None, encoding=None, 391 verbose=None, allow_none=0): 392 utype, uri = splittype(uri) 393 if utype not in ("http", "https"): 394 raise IOError("Unsupported JSONRPC protocol") 395 self.__host, self.__handler = splithost(uri) 396 if not self.__handler: 397 self.__handler = "/RPC2" 398 399 if transport is None: 400 if utype == "https": 401 transport = SafeTransport() 402 else: 403 transport = Transport() 404 self.__transport = transport 405 406 self.__encoding = encoding 407 self.__verbose = verbose 408 self.__allow_none = allow_none 409 410 def __request(self, methodname, params): 411 """call a method on the remote server 412 """ 413 414 request = dumps(params, methodname, encoding=self.__encoding, 415 allow_none=self.__allow_none) 416 417 response = self.__transport.request( 418 self.__host, 419 self.__handler, 420 request.encode(self.__encoding), 421 self.__encoding, 422 verbose=self.__verbose 423 ) 424 425 if len(response) == 1: 426 response = response[0] 427 428 return response 429 430 def __repr__(self): 431 return ("<JSONProxy for %s%s>" % 432 (self.__host, self.__handler) 433 ) 434 435 __str__ = __repr__ 436 437 def __getattr__(self, name): 438 #dispatch 439 return _Method(self.__request, name) 440 441 # note: to call a remote object with an non-standard name, use 442 # result getattr(server, "strange-python-name")(args) 443 444 445if __name__ == "__main__": 446 s = ServerProxy("http://localhost:8080/foo/", verbose=1) 447 c = s.echo("foo bar") 448 print(c) 449 d = s.bad("other") 450 print(d) 451 e = s.echo("foo bar", "baz") 452 print(e) 453 f = s.echo(5) 454 print(f)