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 72 """Base class for client errors.""" 73 74 def __str__(self): 75 return repr(self) 76 77## 78# Indicates an HTTP-level protocol error. This is raised by the HTTP 79# transport layer, if the server returns an error code other than 200 80# (OK). 81# 82# @param url The target URL. 83# @param errcode The HTTP error code. 84# @param errmsg The HTTP error message. 85# @param headers The HTTP header dictionary. 86 87 88class ProtocolError(Error): 89 90 """Indicates an HTTP protocol error.""" 91 92 def __init__(self, url, errcode, errmsg, headers, response): 93 Error.__init__(self) 94 self.url = url 95 self.errcode = errcode 96 self.errmsg = errmsg 97 self.headers = headers 98 self.response = response 99 100 def __repr__(self): 101 return ( 102 "<ProtocolError for %s: %s %s>" % 103 (self.url, self.errcode, self.errmsg) 104 ) 105 106 107def getparser(encoding): 108 un = Unmarshaller(encoding) 109 par = Parser(un) 110 return par, un 111 112 113def dumps(params, methodname=None, methodresponse=None, encoding=None, 114 allow_none=0): 115 if methodname: 116 request = {} 117 request["method"] = methodname 118 request["params"] = params 119 request["id"] = _gen_id() 120 return json.dumps(request) 121 122 123class Unmarshaller(object): 124 125 def __init__(self, encoding): 126 self.data = None 127 self.encoding = encoding 128 129 def feed(self, data): 130 if self.data is None: 131 self.data = data 132 else: 133 self.data = self.data + data 134 135 def close(self): 136 # try to convert string to json 137 return json.loads(self.data.decode(self.encoding)) 138 139 140class Parser(object): 141 142 def __init__(self, unmarshaller): 143 self._target = unmarshaller 144 self.data = None 145 146 def feed(self, data): 147 if self.data is None: 148 self.data = data 149 else: 150 self.data = self.data + data 151 152 def close(self): 153 self._target.feed(self.data) 154 155 156class _Method(object): 157 # some magic to bind an JSON-RPC method to an RPC server. 158 # supports "nested" methods (e.g. examples.getStateName) 159 160 def __init__(self, send, name): 161 self.__send = send 162 self.__name = name 163 164 def __getattr__(self, name): 165 return _Method(self.__send, "%s.%s" % (self.__name, name)) 166 167 def __call__(self, *args): 168 return self.__send(self.__name, args) 169 170## 171# Standard transport class for JSON-RPC over HTTP. 172# <p> 173# You can create custom transports by subclassing this method, and 174# overriding selected methods. 175 176 177class Transport: 178 179 """Handles an HTTP transaction to an JSON-RPC server.""" 180 181 # client identifier (may be overridden) 182 user_agent = "jsonlib.py/%s (by matt harrison)" % __version__ 183 184 ## 185 # Send a complete request, and parse the response. 186 # 187 # @param host Target host. 188 # @param handler Target PRC handler. 189 # @param request_body JSON-RPC request body. 190 # @param verbose Debugging flag. 191 # @return Parsed response. 192 193 def request(self, host, handler, request_body, encoding, verbose=0): 194 # issue JSON-RPC request 195 196 h = self.make_connection(host) 197 if verbose: 198 h.set_debuglevel(1) 199 200 self.send_request(h, handler, request_body) 201 if not PY3: 202 self.send_host(h, host) 203 self.send_user_agent(h) 204 self.send_content(h, request_body) 205 206 try: 207 errcode, errmsg, headers = h.getreply() 208 r = h.getfile() 209 except AttributeError: 210 r = h.getresponse() 211 errcode = r.status 212 errmsg = r.reason 213 headers = r.getheaders() 214 215 if errcode != 200: 216 response = r.read() 217 raise ProtocolError( 218 host + handler, 219 errcode, errmsg, 220 headers, 221 response 222 ) 223 224 self.verbose = verbose 225 226 try: 227 sock = h._conn.sock 228 except AttributeError: 229 sock = None 230 231 return self._parse_response(r, sock, encoding) 232 233 ## 234 # Create parser. 235 # 236 # @return A 2-tuple containing a parser and a unmarshaller. 237 238 def getparser(self, encoding): 239 # get parser and unmarshaller 240 return getparser(encoding) 241 242 ## 243 # Get authorization info from host parameter 244 # Host may be a string, or a (host, x509-dict) tuple; if a string, 245 # it is checked for a "user:pw@host" format, and a "Basic 246 # Authentication" header is added if appropriate. 247 # 248 # @param host Host descriptor (URL or (URL, x509 info) tuple). 249 # @return A 3-tuple containing (actual host, extra headers, 250 # x509 info). The header and x509 fields may be None. 251 252 def get_host_info(self, host): 253 254 x509 = {} 255 if isinstance(host, tuple): 256 host, x509 = host 257 258 auth, host = splituser(host) 259 260 if auth: 261 auth = base64.encodestring(unquote(auth)) 262 auth = "".join(auth.split()) # get rid of whitespace 263 extra_headers = [ 264 ("Authorization", "Basic " + auth) 265 ] 266 else: 267 extra_headers = None 268 269 return host, extra_headers, x509 270 271 ## 272 # Connect to server. 273 # 274 # @param host Target host. 275 # @return A connection handle. 276 277 def make_connection(self, host): 278 # create a HTTP connection object from a host descriptor 279 host, extra_headers, x509 = self.get_host_info(host) 280 return HTTPConnection(host) 281 282 ## 283 # Send request header. 284 # 285 # @param connection Connection handle. 286 # @param handler Target RPC handler. 287 # @param request_body JSON-RPC body. 288 289 def send_request(self, connection, handler, request_body): 290 connection.putrequest("POST", handler) 291 292 ## 293 # Send host name. 294 # 295 # @param connection Connection handle. 296 # @param host Host name. 297 298 def send_host(self, connection, host): 299 host, extra_headers, x509 = self.get_host_info(host) 300 connection.putheader("Host", host) 301 if extra_headers: 302 if isinstance(extra_headers, dict): 303 extra_headers = list(extra_headers.items()) 304 for key, value in extra_headers: 305 connection.putheader(key, value) 306 307 ## 308 # Send user-agent identifier. 309 # 310 # @param connection Connection handle. 311 312 def send_user_agent(self, connection): 313 connection.putheader("User-Agent", self.user_agent) 314 315 ## 316 # Send request body. 317 # 318 # @param connection Connection handle. 319 # @param request_body JSON-RPC request body. 320 321 def send_content(self, connection, request_body): 322 connection.putheader("Content-Type", "text/xml") 323 connection.putheader("Content-Length", str(len(request_body))) 324 connection.endheaders() 325 if request_body: 326 connection.send(request_body) 327 328 ## 329 # Parse response. 330 # 331 # @param file Stream. 332 # @return Response tuple and target method. 333 334 def parse_response(self, file): 335 # compatibility interface 336 return self._parse_response(file, None) 337 338 ## 339 # Parse response (alternate interface). This is similar to the 340 # parse_response method, but also provides direct access to the 341 # underlying socket object (where available). 342 # 343 # @param file Stream. 344 # @param sock Socket handle (or None, if the socket object 345 # could not be accessed). 346 # @return Response tuple and target method. 347 348 def _parse_response(self, file, sock, encoding): 349 # read response from input file/socket, and parse it 350 351 p, u = self.getparser(encoding) 352 353 while 1: 354 if sock: 355 response = sock.recv(1024) 356 else: 357 response = file.read(1024) 358 if not response: 359 break 360 if self.verbose: 361 print("body:", repr(response)) 362 p.feed(response) 363 364 file.close() 365 p.close() 366 367 return u.close() 368 369## 370# Standard transport class for JSON-RPC over HTTPS. 371 372 373class SafeTransport(Transport): 374 375 """Handles an HTTPS transaction to an JSON-RPC server.""" 376 377 # FIXME: mostly untested 378 379 def make_connection(self, host): 380 # create a HTTPS connection object from a host descriptor 381 # host may be a string, or a (host, x509-dict) tuple 382 host, extra_headers, x509 = self.get_host_info(host) 383 try: 384 HTTPS = HTTPSConnection 385 except AttributeError: 386 raise NotImplementedError( 387 "your version of httplib doesn't support HTTPS" 388 ) 389 else: 390 return HTTPS(host, None, **(x509 or {})) 391 392 393class ServerProxy(object): 394 395 def __init__(self, uri, transport=None, encoding=None, 396 verbose=None, allow_none=0): 397 utype, uri = splittype(uri) 398 if utype not in ("http", "https"): 399 raise IOError("Unsupported JSONRPC protocol") 400 self.__host, self.__handler = splithost(uri) 401 if not self.__handler: 402 self.__handler = "/RPC2" 403 404 if transport is None: 405 if utype == "https": 406 transport = SafeTransport() 407 else: 408 transport = Transport() 409 self.__transport = transport 410 411 self.__encoding = encoding 412 self.__verbose = verbose 413 self.__allow_none = allow_none 414 415 def __request(self, methodname, params): 416 """call a method on the remote server 417 """ 418 419 request = dumps(params, methodname, encoding=self.__encoding, 420 allow_none=self.__allow_none) 421 422 response = self.__transport.request( 423 self.__host, 424 self.__handler, 425 request.encode(self.__encoding), 426 self.__encoding, 427 verbose=self.__verbose 428 ) 429 430 if len(response) == 1: 431 response = response[0] 432 433 return response 434 435 def __repr__(self): 436 return ("<JSONProxy for %s%s>" % 437 (self.__host, self.__handler) 438 ) 439 440 __str__ = __repr__ 441 442 def __getattr__(self, name): 443 # dispatch 444 return _Method(self.__request, name) 445 446 # note: to call a remote object with an non-standard name, use 447 # result getattr(server, "strange-python-name")(args) 448 449 450if __name__ == "__main__": 451 s = ServerProxy("http://localhost:8080/foo/", verbose=1) 452 c = s.echo("foo bar") 453 print(c) 454 d = s.bad("other") 455 print(d) 456 e = s.echo("foo bar", "baz") 457 print(e) 458 f = s.echo(5) 459 print(f) 460