/pyrope/jsonrpc.py
Python | 1095 lines | 1056 code | 15 blank | 24 comment | 13 complexity | f6d74dc17c4ec9323f6dd623d712688f MD5 | raw file
1# -*- coding:utf-8 -*- 2""" 3JSON-RPC (remote procedure call). 4 5It consists of 3 (independent) parts: 6 - proxy/dispatcher 7 - data structure / serializer 8 - transport 9 10It's intended for JSON-RPC, but since the above 3 parts are independent, 11it could be used for other RPCs as well. 12 13Currently, JSON-RPC 2.0(pre) and JSON-RPC 1.0 are implemented 14 15:Version: 2008-08-31-beta 16:Status: experimental 17 18:Example: 19 simple Client with JsonRPC2.0 and TCP/IP:: 20 21 >>> proxy = ServerProxy( JsonRpc20(), TransportTcpIp(addr=("127.0.0.1",31415)) ) 22 >>> proxy.echo( "hello world" ) 23 u'hello world' 24 >>> proxy.echo( "bye." ) 25 u'bye.' 26 27 simple Server with JsonRPC2.0 and TCP/IP with logging to STDOUT:: 28 29 >>> server = Server( JsonRpc20(), TransportTcpIp(addr=("127.0.0.1",31415), logfunc=log_stdout) ) 30 >>> def echo( s ): 31 ... return s 32 >>> server.register_function( echo ) 33 >>> server.serve( 2 ) # serve 2 requests # doctest: +ELLIPSIS 34 listen ('127.0.0.1', 31415) 35 ('127.0.0.1', ...) connected 36 ('127.0.0.1', ...) <-- {"jsonrpc": "2.0", "method": "echo", "params": ["hello world"], "id": 0} 37 ('127.0.0.1', ...) --> {"jsonrpc": "2.0", "result": "hello world", "id": 0} 38 ('127.0.0.1', ...) close 39 ('127.0.0.1', ...) connected 40 ('127.0.0.1', ...) <-- {"jsonrpc": "2.0", "method": "echo", "params": ["bye."], "id": 0} 41 ('127.0.0.1', ...) --> {"jsonrpc": "2.0", "result": "bye.", "id": 0} 42 ('127.0.0.1', ...) close 43 close ('127.0.0.1', 31415) 44 45 Client with JsonRPC2.0 and an abstract Unix Domain Socket:: 46 47 >>> proxy = ServerProxy( JsonRpc20(), TransportUnixSocket(addr="\\x00.rpcsocket") ) 48 >>> proxy.hi( message="hello" ) #named parameters 49 u'hi there' 50 >>> proxy.test() #fault 51 Traceback (most recent call last): 52 ... 53 jsonrpc.RPCMethodNotFound: <RPCFault -32601: u'Method not found.' (None)> 54 >>> proxy.debug.echo( "hello world" ) #hierarchical procedures 55 u'hello world' 56 57 Server with JsonRPC2.0 and abstract Unix Domain Socket with a logfile:: 58 59 >>> server = Server( JsonRpc20(), TransportUnixSocket(addr="\\x00.rpcsocket", logfunc=log_file("mylog.txt")) ) 60 >>> def echo( s ): 61 ... return s 62 >>> def hi( message ): 63 ... return "hi there" 64 >>> server.register_function( hi ) 65 >>> server.register_function( echo, name="debug.echo" ) 66 >>> server.serve( 3 ) # serve 3 requests 67 68 "mylog.txt" then contains: 69 listen '\\x00.rpcsocket' 70 '' connected 71 '' --> '{"jsonrpc": "2.0", "method": "hi", "params": {"message": "hello"}, "id": 0}' 72 '' <-- '{"jsonrpc": "2.0", "result": "hi there", "id": 0}' 73 '' close 74 '' connected 75 '' --> '{"jsonrpc": "2.0", "method": "test", "id": 0}' 76 '' <-- '{"jsonrpc": "2.0", "error": {"code":-32601, "message": "Method not found."}, "id": 0}' 77 '' close 78 '' connected 79 '' --> '{"jsonrpc": "2.0", "method": "debug.echo", "params": ["hello world"], "id": 0}' 80 '' <-- '{"jsonrpc": "2.0", "result": "hello world", "id": 0}' 81 '' close 82 close '\\x00.rpcsocket' 83 84:Note: all exceptions derived from RPCFault are propagated to the client. 85 other exceptions are logged and result in a sent-back "empty" INTERNAL_ERROR. 86:Uses: simplejson, socket, sys,time,codecs 87:SeeAlso: JSON-RPC 2.0 proposal, 1.0 specification 88:Warning: 89 .. Warning:: 90 This is **experimental** code! 91:Bug: 92 93:Author: Roland Koebler (rk(at)simple-is-better.org) 94:Copyright: 2007-2008 by Roland Koebler (rk(at)simple-is-better.org) 95:License: see __license__ 96:Changelog: 97 - 2008-08-31: 1st release 98 99TODO: 100 - server: multithreading rpc-server 101 - client: multicall (send several requests) 102 - transport: SSL sockets, maybe HTTP, HTTPS 103 - types: support for date/time (ISO 8601) 104 - errors: maybe customizable error-codes/exceptions 105 - mixed 1.0/2.0 server ? 106 - system description etc. ? 107 - maybe test other json-serializers, like cjson? 108""" 109 110__version__ = "2008-08-31-beta" 111__author__ = "Roland Koebler <rk(at)simple-is-better.org>" 112__license__ = """Copyright (c) 2007-2008 by Roland Koebler (rk(at)simple-is-better.org) 113 114Permission is hereby granted, free of charge, to any person obtaining 115a copy of this software and associated documentation files (the 116"Software"), to deal in the Software without restriction, including 117without limitation the rights to use, copy, modify, merge, publish, 118distribute, sublicense, and/or sell copies of the Software, and to 119permit persons to whom the Software is furnished to do so, subject to 120the following conditions: 121 122The above copyright notice and this permission notice shall be included 123in all copies or substantial portions of the Software. 124 125THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 126EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 127MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 128IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 129CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 130TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 131SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.""" 132 133#========================================= 134#import 135 136import sys 137 138#========================================= 139# errors 140 141#---------------------- 142# error-codes + exceptions 143 144#JSON-RPC 2.0 error-codes 145PARSE_ERROR = -32700 146INVALID_REQUEST = -32600 147METHOD_NOT_FOUND = -32601 148INVALID_METHOD_PARAMS = -32602 #invalid number/type of parameters 149INTERNAL_ERROR = -32603 #"all other errors" 150 151#additional error-codes 152PROCEDURE_EXCEPTION = -32000 153AUTHENTIFICATION_ERROR = -32001 154PERMISSION_DENIED = -32002 155INVALID_PARAM_VALUES = -32003 156 157#human-readable messages 158ERROR_MESSAGE = { 159 PARSE_ERROR : "Parse error.", 160 INVALID_REQUEST : "Invalid Request.", 161 METHOD_NOT_FOUND : "Method not found.", 162 INVALID_METHOD_PARAMS : "Invalid parameters.", 163 INTERNAL_ERROR : "Internal error.", 164 165 PROCEDURE_EXCEPTION : "Procedure exception.", 166 AUTHENTIFICATION_ERROR : "Authentification error.", 167 PERMISSION_DENIED : "Permission denied.", 168 INVALID_PARAM_VALUES: "Invalid parameter values." 169 } 170 171#---------------------- 172# exceptions 173 174class RPCError(Exception): 175 """Base class for rpc-errors.""" 176 177 178class RPCTransportError(RPCError): 179 """Transport error.""" 180class RPCTimeoutError(RPCTransportError): 181 """Transport/reply timeout.""" 182 183class RPCFault(RPCError): 184 """RPC error/fault package received. 185 186 This exception can also be used as a class, to generate a 187 RPC-error/fault message. 188 189 :Variables: 190 - error_code: the RPC error-code 191 - error_string: description of the error 192 - error_data: optional additional information 193 (must be json-serializable) 194 :TODO: improve __str__ 195 """ 196 def __init__(self, error_code, error_message, error_data=None): 197 RPCError.__init__(self) 198 self.error_code = error_code 199 self.error_message = error_message 200 self.error_data = error_data 201 def __str__(self): 202 return repr(self) 203 def __repr__(self): 204 return( "<RPCFault %s: %s (%s)>" % (self.error_code, repr(self.error_message), repr(self.error_data)) ) 205 206class RPCParseError(RPCFault): 207 """Broken rpc-package. (PARSE_ERROR)""" 208 def __init__(self, error_data=None): 209 RPCFault.__init__(self, PARSE_ERROR, ERROR_MESSAGE[PARSE_ERROR], error_data) 210 211class RPCInvalidRPC(RPCFault): 212 """Invalid rpc-package. (INVALID_REQUEST)""" 213 def __init__(self, error_data=None): 214 RPCFault.__init__(self, INVALID_REQUEST, ERROR_MESSAGE[INVALID_REQUEST], error_data) 215 216class RPCMethodNotFound(RPCFault): 217 """Method not found. (METHOD_NOT_FOUND)""" 218 def __init__(self, error_data=None): 219 RPCFault.__init__(self, METHOD_NOT_FOUND, ERROR_MESSAGE[METHOD_NOT_FOUND], error_data) 220 221class RPCInvalidMethodParams(RPCFault): 222 """Invalid method-parameters. (INVALID_METHOD_PARAMS)""" 223 def __init__(self, error_data=None): 224 RPCFault.__init__(self, INVALID_METHOD_PARAMS, ERROR_MESSAGE[INVALID_METHOD_PARAMS], error_data) 225 226class RPCInternalError(RPCFault): 227 """Internal error. (INTERNAL_ERROR)""" 228 def __init__(self, error_data=None): 229 RPCFault.__init__(self, INTERNAL_ERROR, ERROR_MESSAGE[INTERNAL_ERROR], error_data) 230 231 232class RPCProcedureException(RPCFault): 233 """Procedure exception. (PROCEDURE_EXCEPTION)""" 234 def __init__(self, error_data=None): 235 RPCFault.__init__(self, PROCEDURE_EXCEPTION, ERROR_MESSAGE[PROCEDURE_EXCEPTION], error_data) 236class RPCAuthentificationError(RPCFault): 237 """AUTHENTIFICATION_ERROR""" 238 def __init__(self, error_data=None): 239 RPCFault.__init__(self, AUTHENTIFICATION_ERROR, ERROR_MESSAGE[AUTHENTIFICATION_ERROR], error_data) 240class RPCPermissionDenied(RPCFault): 241 """PERMISSION_DENIED""" 242 def __init__(self, error_data=None): 243 RPCFault.__init__(self, PERMISSION_DENIED, ERROR_MESSAGE[PERMISSION_DENIED], error_data) 244class RPCInvalidParamValues(RPCFault): 245 """INVALID_PARAM_VALUES""" 246 def __init__(self, error_data=None): 247 RPCFault.__init__(self, INVALID_PARAM_VALUES, ERROR_MESSAGE[INVALID_PARAM_VALUES], error_data) 248 249 250#========================================= 251# data structure / serializer 252 253try: 254 import simplejson 255except ImportError, err: 256 print "FATAL: json-module 'simplejson' is missing (%s)" % (err) 257 sys.exit(1) 258 259#---------------------- 260# 261def dictkeyclean(d): 262 """Convert all keys of the dict 'd' to (ascii-)strings. 263 264 :Raises: UnicodeEncodeError 265 """ 266 new_d = {} 267 for (k, v) in d.iteritems(): 268 new_d[str(k)] = v 269 return new_d 270 271#---------------------- 272# JSON-RPC 1.0 273 274class JsonRpc10: 275 """JSON-RPC V1.0 data-structure / serializer 276 277 This implementation is quite liberal in what it accepts: It treats 278 missing "params" and "id" in Requests and missing "result"/"error" in 279 Responses as empty/null. 280 281 :SeeAlso: JSON-RPC 1.0 specification 282 :TODO: catch simplejson.dumps not-serializable-exceptions 283 """ 284 def __init__(self, dumps=simplejson.dumps, loads=simplejson.loads): 285 """init: set serializer to use 286 287 :Parameters: 288 - dumps: json-encoder-function 289 - loads: json-decoder-function 290 :Note: The dumps_* functions of this class already directly create 291 the invariant parts of the resulting json-object themselves, 292 without using the given json-encoder-function. 293 """ 294 self.dumps = dumps 295 self.loads = loads 296 297 def dumps_request( self, method, params=(), id=0 ): 298 """serialize JSON-RPC-Request 299 300 :Parameters: 301 - method: the method-name (str/unicode) 302 - params: the parameters (list/tuple) 303 - id: if id=None, this results in a Notification 304 :Returns: | {"method": "...", "params": ..., "id": ...} 305 | "method", "params" and "id" are always in this order. 306 :Raises: TypeError if method/params is of wrong type or 307 not JSON-serializable 308 """ 309 if not isinstance(method, (str, unicode)): 310 raise TypeError('"method" must be a string (or unicode string).') 311 if not isinstance(params, (tuple, list)): 312 raise TypeError("params must be a tuple/list.") 313 314 return '{"method": %s, "params": %s, "id": %s}' % \ 315 (self.dumps(method), self.dumps(params), self.dumps(id)) 316 317 def dumps_notification( self, method, params=() ): 318 """serialize a JSON-RPC-Notification 319 320 :Parameters: see dumps_request 321 :Returns: | {"method": "...", "params": ..., "id": null} 322 | "method", "params" and "id" are always in this order. 323 :Raises: see dumps_request 324 """ 325 if not isinstance(method, (str, unicode)): 326 raise TypeError('"method" must be a string (or unicode string).') 327 if not isinstance(params, (tuple, list)): 328 raise TypeError("params must be a tuple/list.") 329 330 return '{"method": %s, "params": %s, "id": null}' % \ 331 (self.dumps(method), self.dumps(params)) 332 333 def dumps_response( self, result, id=None ): 334 """serialize a JSON-RPC-Response (without error) 335 336 :Returns: | {"result": ..., "error": null, "id": ...} 337 | "result", "error" and "id" are always in this order. 338 :Raises: TypeError if not JSON-serializable 339 """ 340 return '{"result": %s, "error": null, "id": %s}' % \ 341 (self.dumps(result), self.dumps(id)) 342 343 def dumps_error( self, error, id=None ): 344 """serialize a JSON-RPC-Response-error 345 346 Since JSON-RPC 1.0 does not define an error-object, this uses the 347 JSON-RPC 2.0 error-object. 348 349 :Parameters: 350 - error: a RPCFault instance 351 :Returns: | {"result": null, "error": {"code": error_code, "message": error_message, "data": error_data}, "id": ...} 352 | "result", "error" and "id" are always in this order, data is omitted if None. 353 :Raises: ValueError if error is not a RPCFault instance, 354 TypeError if not JSON-serializable 355 """ 356 if not isinstance(error, RPCFault): 357 raise ValueError("""error must be a RPCFault-instance.""") 358 if error.error_data is None: 359 return '{"result": null, "error": {"code":%s, "message": %s}, "id": %s}' % \ 360 (self.dumps(error.error_code), self.dumps(error.error_message), self.dumps(id)) 361 else: 362 return '{"result": null, "error": {"code":%s, "message": %s, "data": %s}, "id": %s}' % \ 363 (self.dumps(error.error_code), self.dumps(error.error_message), self.dumps(error.error_data), self.dumps(id)) 364 365 def loads_request( self, string ): 366 """de-serialize a JSON-RPC Request/Notification 367 368 :Returns: | [method_name, params, id] or [method_name, params] 369 | params is a tuple/list 370 | if id is missing, this is a Notification 371 :Raises: RPCParseError, RPCInvalidRPC, RPCInvalidMethodParams 372 """ 373 try: 374 data = self.loads(string) 375 except ValueError, err: 376 raise RPCParseError("No valid JSON. (%s)" % str(err)) 377 if not isinstance(data, dict): raise RPCInvalidRPC("No valid RPC-package.") 378 if "method" not in data: raise RPCInvalidRPC("""Invalid Request, "method" is missing.""") 379 if not isinstance(data["method"], (str, unicode)): 380 raise RPCInvalidRPC("""Invalid Request, "method" must be a string.""") 381 if "id" not in data: data["id"] = None #be liberal 382 if "params" not in data: data["params"] = () #be liberal 383 if not isinstance(data["params"], (list, tuple)): 384 raise RPCInvalidRPC("""Invalid Request, "params" must be an array.""") 385 if len(data) != 3: raise RPCInvalidRPC("""Invalid Request, additional fields found.""") 386 387 # notification / request 388 if data["id"] is None: 389 return data["method"], data["params"] #notification 390 else: 391 return data["method"], data["params"], data["id"] #request 392 393 def loads_response( self, string ): 394 """de-serialize a JSON-RPC Response/error 395 396 :Returns: | [result, id] for Responses 397 :Raises: | RPCFault+derivates for error-packages/faults, RPCParseError, RPCInvalidRPC 398 | Note that for error-packages which do not match the 399 V2.0-definition, RPCFault(-1, "Error", RECEIVED_ERROR_OBJ) 400 is raised. 401 """ 402 try: 403 data = self.loads(string) 404 except ValueError, err: 405 raise RPCParseError("No valid JSON. (%s)" % str(err)) 406 if not isinstance(data, dict): raise RPCInvalidRPC("No valid RPC-package.") 407 if "id" not in data: raise RPCInvalidRPC("""Invalid Response, "id" missing.""") 408 if "result" not in data: data["result"] = None #be liberal 409 if "error" not in data: data["error"] = None #be liberal 410 if len(data) != 3: raise RPCInvalidRPC("""Invalid Response, additional or missing fields.""") 411 412 #error 413 if data["error"] is not None: 414 if data["result"] is not None: 415 raise RPCInvalidRPC("""Invalid Response, one of "result" or "error" must be null.""") 416 #v2.0 error-format 417 if( isinstance(data["error"], dict) and "code" in data["error"] and "message" in data["error"] and 418 (len(data["error"])==2 or ("data" in data["error"] and len(data["error"])==3)) ): 419 if "data" not in data["error"]: 420 error_data = None 421 else: 422 error_data = data["error"]["data"] 423 424 if data["error"]["code"] == PARSE_ERROR: 425 raise RPCParseError(error_data) 426 elif data["error"]["code"] == INVALID_REQUEST: 427 raise RPCInvalidRPC(error_data) 428 elif data["error"]["code"] == METHOD_NOT_FOUND: 429 raise RPCMethodNotFound(error_data) 430 elif data["error"]["code"] == INVALID_METHOD_PARAMS: 431 raise RPCInvalidMethodParams(error_data) 432 elif data["error"]["code"] == INTERNAL_ERROR: 433 raise RPCInternalError(error_data) 434 elif data["error"]["code"] == PROCEDURE_EXCEPTION: 435 raise RPCProcedureException(error_data) 436 elif data["error"]["code"] == AUTHENTIFICATION_ERROR: 437 raise RPCAuthentificationError(error_data) 438 elif data["error"]["code"] == PERMISSION_DENIED: 439 raise RPCPermissionDenied(error_data) 440 elif data["error"]["code"] == INVALID_PARAM_VALUES: 441 raise RPCInvalidParamValues(error_data) 442 else: 443 raise RPCFault(data["error"]["code"], data["error"]["message"], error_data) 444 #other error-format 445 else: 446 raise RPCFault(-1, "Error", data["error"]) 447 #result 448 else: 449 return data["result"], data["id"] 450 451#---------------------- 452# JSON-RPC 2.0 453 454class JsonRpc20: 455 """JSON-RPC V2.0 data-structure / serializer 456 457 :SeeAlso: JSON-RPC 2.0 specification 458 :TODO: catch simplejson.dumps not-serializable-exceptions 459 """ 460 def __init__(self, dumps=simplejson.dumps, loads=simplejson.loads): 461 """init: set serializer to use 462 463 :Parameters: 464 - dumps: json-encoder-function 465 - loads: json-decoder-function 466 :Note: The dumps_* functions of this class already directly create 467 the invariant parts of the resulting json-object themselves, 468 without using the given json-encoder-function. 469 """ 470 self.dumps = dumps 471 self.loads = loads 472 473 def dumps_request( self, method, params=(), id=0 ): 474 """serialize JSON-RPC-Request 475 476 :Parameters: 477 - method: the method-name (str/unicode) 478 - params: the parameters (list/tuple/dict) 479 - id: the id (should not be None) 480 :Returns: | {"jsonrpc": "2.0", "method": "...", "params": ..., "id": ...} 481 | "jsonrpc", "method", "params" and "id" are always in this order. 482 | "params" is omitted if empty 483 :Raises: TypeError if method/params is of wrong type or 484 not JSON-serializable 485 """ 486 if not isinstance(method, (str, unicode)): 487 raise TypeError('"method" must be a string (or unicode string).') 488 if not isinstance(params, (tuple, list, dict)): 489 raise TypeError("params must be a tuple/list/dict or None.") 490 491 if params: 492 return '{"jsonrpc": "2.0", "method": %s, "params": %s, "id": %s}' % \ 493 (self.dumps(method), self.dumps(params), self.dumps(id)) 494 else: 495 return '{"jsonrpc": "2.0", "method": %s, "id": %s}' % \ 496 (self.dumps(method), self.dumps(id)) 497 498 def dumps_notification( self, method, params=() ): 499 """serialize a JSON-RPC-Notification 500 501 :Parameters: see dumps_request 502 :Returns: | {"jsonrpc": "2.0", "method": "...", "params": ...} 503 | "jsonrpc", "method" and "params" are always in this order. 504 :Raises: see dumps_request 505 """ 506 if not isinstance(method, (str, unicode)): 507 raise TypeError('"method" must be a string (or unicode string).') 508 if not isinstance(params, (tuple, list, dict)): 509 raise TypeError("params must be a tuple/list/dict or None.") 510 511 if params: 512 return '{"jsonrpc": "2.0", "method": %s, "params": %s}' % \ 513 (self.dumps(method), self.dumps(params)) 514 else: 515 return '{"jsonrpc": "2.0", "method": %s}' % \ 516 (self.dumps(method)) 517 518 def dumps_response( self, result, id=None ): 519 """serialize a JSON-RPC-Response (without error) 520 521 :Returns: | {"jsonrpc": "2.0", "result": ..., "id": ...} 522 | "jsonrpc", "result", and "id" are always in this order. 523 :Raises: TypeError if not JSON-serializable 524 """ 525 return '{"jsonrpc": "2.0", "result": %s, "id": %s}' % \ 526 (self.dumps(result), self.dumps(id)) 527 528 def dumps_error( self, error, id=None ): 529 """serialize a JSON-RPC-Response-error 530 531 :Parameters: 532 - error: a RPCFault instance 533 :Returns: | {"jsonrpc": "2.0", "error": {"code": error_code, "message": error_message, "data": error_data}, "id": ...} 534 | "jsonrpc", "result", "error" and "id" are always in this order, data is omitted if None. 535 :Raises: ValueError if error is not a RPCFault instance, 536 TypeError if not JSON-serializable 537 """ 538 if not isinstance(error, RPCFault): 539 raise ValueError("""error must be a RPCFault-instance.""") 540 if error.error_data is None: 541 return '{"jsonrpc": "2.0", "error": {"code":%s, "message": %s}, "id": %s}' % \ 542 (self.dumps(error.error_code), self.dumps(error.error_message), self.dumps(id)) 543 else: 544 return '{"jsonrpc": "2.0", "error": {"code":%s, "message": %s, "data": %s}, "id": %s}' % \ 545 (self.dumps(error.error_code), self.dumps(error.error_message), self.dumps(error.error_data), self.dumps(id)) 546 547 def loads_request( self, string ): 548 """de-serialize a JSON-RPC Request/Notification 549 550 :Returns: | [method_name, params, id] or [method_name, params] 551 | params is a tuple/list or dict (with only str-keys) 552 | if id is missing, this is a Notification 553 :Raises: RPCParseError, RPCInvalidRPC, RPCInvalidMethodParams 554 """ 555 try: 556 data = self.loads(string) 557 except ValueError, err: 558 raise RPCParseError("No valid JSON. (%s)" % str(err)) 559 if not isinstance(data, dict): raise RPCInvalidRPC("No valid RPC-package.") 560 if "jsonrpc" not in data: raise RPCInvalidRPC("""Invalid Response, "jsonrpc" missing.""") 561 if not isinstance(data["jsonrpc"], (str, unicode)): 562 raise RPCInvalidRPC("""Invalid Response, "jsonrpc" must be a string.""") 563 if data["jsonrpc"] != "2.0": raise RPCInvalidRPC("""Invalid jsonrpc version.""") 564 if "method" not in data: raise RPCInvalidRPC("""Invalid Request, "method" is missing.""") 565 if not isinstance(data["method"], (str, unicode)): 566 raise RPCInvalidRPC("""Invalid Request, "method" must be a string.""") 567 if "params" not in data: data["params"] = () 568 #convert params-keys from unicode to str 569 elif isinstance(data["params"], dict): 570 try: 571 data["params"] = dictkeyclean(data["params"]) 572 except UnicodeEncodeError: 573 raise RPCInvalidMethodParams("Parameter-names must be in ascii.") 574 elif not isinstance(data["params"], (list, tuple)): 575 raise RPCInvalidRPC("""Invalid Request, "params" must be an array or object.""") 576 if not( len(data)==3 or ("id" in data and len(data)==4) ): 577 raise RPCInvalidRPC("""Invalid Request, additional fields found.""") 578 579 # notification / request 580 if "id" not in data: 581 return data["method"], data["params"] #notification 582 else: 583 return data["method"], data["params"], data["id"] #request 584 585 def loads_response( self, string ): 586 """de-serialize a JSON-RPC Response/error 587 588 :Returns: | [result, id] for Responses 589 :Raises: | RPCFault+derivates for error-packages/faults, RPCParseError, RPCInvalidRPC 590 """ 591 try: 592 data = self.loads(string) 593 except ValueError, err: 594 raise RPCParseError("No valid JSON. (%s)" % str(err)) 595 if not isinstance(data, dict): raise RPCInvalidRPC("No valid RPC-package.") 596 if "jsonrpc" not in data: raise RPCInvalidRPC("""Invalid Response, "jsonrpc" missing.""") 597 if not isinstance(data["jsonrpc"], (str, unicode)): 598 raise RPCInvalidRPC("""Invalid Response, "jsonrpc" must be a string.""") 599 if data["jsonrpc"] != "2.0": raise RPCInvalidRPC("""Invalid jsonrpc version.""") 600 if "id" not in data: raise RPCInvalidRPC("""Invalid Response, "id" missing.""") 601 if "result" not in data: data["result"] = None 602 if "error" not in data: data["error"] = None 603 if len(data) != 4: raise RPCInvalidRPC("""Invalid Response, additional or missing fields.""") 604 605 #error 606 if data["error"] is not None: 607 if data["result"] is not None: 608 raise RPCInvalidRPC("""Invalid Response, only "result" OR "error" allowed.""") 609 if not isinstance(data["error"], dict): raise RPCInvalidRPC("Invalid Response, invalid error-object.") 610 if "code" not in data["error"] or "message" not in data["error"]: 611 raise RPCInvalidRPC("Invalid Response, invalid error-object.") 612 if "data" not in data["error"]: data["error"]["data"] = None 613 if len(data["error"]) != 3: 614 raise RPCInvalidRPC("Invalid Response, invalid error-object.") 615 616 error_data = data["error"]["data"] 617 if data["error"]["code"] == PARSE_ERROR: 618 raise RPCParseError(error_data) 619 elif data["error"]["code"] == INVALID_REQUEST: 620 raise RPCInvalidRPC(error_data) 621 elif data["error"]["code"] == METHOD_NOT_FOUND: 622 raise RPCMethodNotFound(error_data) 623 elif data["error"]["code"] == INVALID_METHOD_PARAMS: 624 raise RPCInvalidMethodParams(error_data) 625 elif data["error"]["code"] == INTERNAL_ERROR: 626 raise RPCInternalError(error_data) 627 elif data["error"]["code"] == PROCEDURE_EXCEPTION: 628 raise RPCProcedureException(error_data) 629 elif data["error"]["code"] == AUTHENTIFICATION_ERROR: 630 raise RPCAuthentificationError(error_data) 631 elif data["error"]["code"] == PERMISSION_DENIED: 632 raise RPCPermissionDenied(error_data) 633 elif data["error"]["code"] == INVALID_PARAM_VALUES: 634 raise RPCInvalidParamValues(error_data) 635 else: 636 raise RPCFault(data["error"]["code"], data["error"]["message"], error_data) 637 #result 638 else: 639 return data["result"], data["id"] 640 641 642#========================================= 643# transports 644 645#---------------------- 646# transport-logging 647 648import codecs 649import time 650 651def log_dummy( message ): 652 """dummy-logger: do nothing""" 653 pass 654def log_stdout( message ): 655 """print message to STDOUT""" 656 print message 657 658def log_file( filename ): 659 """return a logfunc which logs to a file (in utf-8)""" 660 def logfile( message ): 661 f = codecs.open( filename, 'a', encoding='utf-8' ) 662 f.write( message+"\n" ) 663 f.close() 664 return logfile 665 666def log_filedate( filename ): 667 """return a logfunc which logs date+message to a file (in utf-8)""" 668 def logfile( message ): 669 f = codecs.open( filename, 'a', encoding='utf-8' ) 670 f.write( time.strftime("%Y-%m-%d %H:%M:%S ")+message+"\n" ) 671 f.close() 672 return logfile 673 674#---------------------- 675 676class Transport: 677 """generic Transport-interface. 678 679 This class, and especially its methods and docstrings, 680 define the Transport-Interface. 681 """ 682 def __init__(self): 683 pass 684 685 def send( self, data ): 686 """send all data. must be implemented by derived classes.""" 687 raise NotImplementedError 688 def recv( self ): 689 """receive data. must be implemented by derived classes.""" 690 raise NotImplementedError 691 692 def sendrecv( self, string ): 693 """send + receive data""" 694 self.send( string ) 695 return self.recv() 696 def serve( self, handler, n=None ): 697 """serve (forever or for n communicaions). 698 699 - receive data 700 - call result = handler(data) 701 - send back result if not None 702 703 The serving can be stopped by SIGINT. 704 705 :TODO: 706 - how to stop? 707 maybe use a .run-file, and stop server if file removed? 708 - maybe make n_current accessible? (e.g. for logging) 709 """ 710 n_current = 0 711 while 1: 712 if n is not None and n_current >= n: 713 break 714 data = self.recv() 715 result = handler(data) 716 if result is not None: 717 self.send( result ) 718 n_current += 1 719 720 721class TransportSTDINOUT(Transport): 722 """receive from STDIN, send to STDOUT. 723 724 Useful e.g. for debugging. 725 """ 726 def send(self, string): 727 """write data to STDOUT with '***SEND:' prefix """ 728 print "***SEND:" 729 print string 730 def recv(self): 731 """read data from STDIN""" 732 print "***RECV (please enter, ^D ends.):" 733 return sys.stdin.read() 734 735 736import socket, select 737TERMINAL = '<!@#!@#!@#ZCX?>' 738TERMINAL_LEN = len(TERMINAL) 739class TransportSocket(Transport): 740 741 """Transport via socket. 742 743 :SeeAlso: python-module socket 744 :TODO: 745 - documentation 746 - improve this (e.g. make sure that connections are closed, socket-files are deleted etc.) 747 - exception-handling? (socket.error) 748 """ 749 def __init__( self, addr, limit=4096, sock_type=socket.AF_INET, sock_prot=socket.SOCK_STREAM, timeout=5.0, logfunc=log_dummy ): 750 """ 751 :Parameters: 752 - addr: socket-address 753 - timeout: timeout in seconds 754 - logfunc: function for logging, logfunc(message) 755 :Raises: socket.timeout after timeout 756 """ 757 self.limit = limit 758 self.addr = addr 759 self.s_type = sock_type 760 self.s_prot = sock_prot 761 self.s = None 762 self.timeout = timeout 763 self.log = logfunc 764 def connect( self ): 765 self.close() 766 self.log( "connect to %s" % repr(self.addr) ) 767 self.s = socket.socket( self.s_type, self.s_prot ) 768 self.s.settimeout( self.timeout ) 769 self.s.connect( self.addr ) 770 def close( self ): 771 if self.s is not None: 772 self.log( "close %s" % repr(self.addr) ) 773 self.s.close() 774 self.s = None 775 def __repr__(self): 776 return "<TransportSocket, %s>" % repr(self.addr) 777 778 def send( self, string ): 779 if self.s is None: 780 self.connect() 781 self.log( "--> "+repr(string) ) 782 self.sendAll(self.s, string) 783 784 def sendAll(self, socket, string): 785 socket.sendall(string + TERMINAL) 786 787 def recvAll(self, socket): 788 allData = '' 789 terminalReceived = False 790 while not terminalReceived: 791 data = socket.recv(self.limit) 792 allData += data 793 if len(allData) > TERMINAL_LEN: 794 terminalReceived = allData[-TERMINAL_LEN:] == TERMINAL 795 return allData[:-TERMINAL_LEN] 796 797 def recv( self ): 798 if self.s is None: 799 self.connect() 800 data = self.recvAll(self.s) 801 self.log( "<-- "+repr(data) ) 802 return data 803 804 def sendrecv( self, string ): 805 """send data + receive data + close""" 806 try: 807 self.send( string ) 808 return self.recv() 809 finally: 810 self.close() 811 def serve(self, handler, n=None): 812 """open socket, wait for incoming connections and handle them. 813 814 :Parameters: 815 - n: serve n requests, None=forever 816 """ 817 self.close() 818 self.s = socket.socket( self.s_type, self.s_prot ) 819 try: 820 self.log( "listen %s" % repr(self.addr) ) 821 self.s.bind( self.addr ) 822 self.s.listen(1) 823 n_current = 0 824 while 1: 825 if n is not None and n_current >= n: 826 break 827 conn, addr = self.s.accept() 828 self.log( "%s connected" % repr(addr) ) 829 data = self.recvAll(conn) 830 self.log( "%s --> %s" % (repr(addr), repr(data)) ) 831 result = handler(data) 832 if data is not None: 833 self.log( "%s <-- %s" % (repr(addr), repr(result)) ) 834 self.sendAll(conn, result) 835 self.log( "%s close" % repr(addr) ) 836 conn.close() 837 n_current += 1 838 finally: 839 self.close() 840 841 842if hasattr(socket, 'AF_UNIX'): 843 844 class TransportUnixSocket(TransportSocket): 845 """Transport via Unix Domain Socket. 846 """ 847 def __init__(self, addr=None, limit=4096, timeout=5.0, logfunc=log_dummy): 848 """ 849 :Parameters: 850 - addr: "socket_file" 851 :Note: | The socket-file is not deleted. 852 | If the socket-file begins with \x00, abstract sockets are used, 853 and no socket-file is created. 854 :SeeAlso: TransportSocket 855 """ 856 TransportSocket.__init__( self, addr, limit, socket.AF_UNIX, socket.SOCK_STREAM, timeout, logfunc ) 857 858class TransportTcpIp(TransportSocket): 859 """Transport via TCP/IP. 860 """ 861 def __init__(self, addr=None, limit=4096, timeout=5.0, logfunc=log_dummy): 862 """ 863 :Parameters: 864 - addr: ("host",port) 865 :SeeAlso: TransportSocket 866 """ 867 TransportSocket.__init__( self, addr, limit, socket.AF_INET, socket.SOCK_STREAM, timeout, logfunc ) 868 869 870#========================================= 871# client side: server proxy 872 873class ServerProxy: 874 """RPC-client: server proxy 875 876 A logical connection to a RPC server. 877 878 It works with different data/serializers and different transports. 879 880 Notifications and id-handling/multicall are not yet implemented. 881 882 :Example: 883 see module-docstring 884 885 :TODO: verbose/logging? 886 """ 887 def __init__( self, data_serializer, transport ): 888 """ 889 :Parameters: 890 - data_serializer: a data_structure+serializer-instance 891 - transport: a Transport instance 892 """ 893 #TODO: check parameters 894 self.__data_serializer = data_serializer 895 if not isinstance(transport, Transport): 896 raise ValueError('invalid "transport" (must be a Transport-instance)"') 897 self.__transport = transport 898 899 def __str__(self): 900 return repr(self) 901 def __repr__(self): 902 return "<ServerProxy for %s, with serializer %s>" % (self.__transport, self.__data_serializer) 903 904 def __req( self, methodname, args=None, kwargs=None, id=0 ): 905 # JSON-RPC 1.0: only positional parameters 906 if len(kwargs) > 0 and isinstance(self.data_serializer, JsonRpc10): 907 raise ValueError("Only positional parameters allowed in JSON-RPC 1.0") 908 # JSON-RPC 2.0: only args OR kwargs allowed! 909 if len(args) > 0 and len(kwargs) > 0: 910 raise ValueError("Only positional or named parameters are allowed!") 911 if len(kwargs) == 0: 912 req_str = self.__data_serializer.dumps_request( methodname, args, id ) 913 else: 914 req_str = self.__data_serializer.dumps_request( methodname, kwargs, id ) 915 916 try: 917 resp_str = self.__transport.sendrecv( req_str ) 918 except Exception,err: 919 raise RPCTransportError(err) 920 resp = self.__data_serializer.loads_response( resp_str ) 921 return resp[0] 922 923 def __getattr__(self, name): 924 # magic method dispatcher 925 # note: to call a remote object with an non-standard name, use 926 # result getattr(my_server_proxy, "strange-python-name")(args) 927 return _method(self.__req, name) 928 929# request dispatcher 930class _method: 931 """some "magic" to bind an RPC method to an RPC server. 932 933 Supports "nested" methods (e.g. examples.getStateName). 934 935 :Raises: AttributeError for method-names/attributes beginning with '_'. 936 """ 937 def __init__(self, req, name): 938 if name[0] == "_": #prevent rpc-calls for proxy._*-functions 939 raise AttributeError("invalid attribute '%s'" % name) 940 self.__req = req 941 self.__name = name 942 def __getattr__(self, name): 943 if name[0] == "_": #prevent rpc-calls for proxy._*-functions 944 raise AttributeError("invalid attribute '%s'" % name) 945 return _method(self.__req, "%s.%s" % (self.__name, name)) 946 def __call__(self, *args, **kwargs): 947 return self.__req(self.__name, args, kwargs) 948 949#========================================= 950# server side: Server 951 952import types 953 954class Server: 955 """RPC-server. 956 957 It works with different data/serializers and 958 with different transports. 959 960 :Example: 961 see module-docstring 962 963 :TODO: 964 - mixed JSON-RPC 1.0/2.0 server? 965 - logging/loglevels? 966 """ 967 def __init__( self, data_serializer, transport, logfile=None ): 968 """ 969 :Parameters: 970 - data_serializer: a data_structure+serializer-instance 971 - transport: a Transport instance 972 - logfile: file to log ("unexpected") errors to 973 """ 974 #TODO: check parameters 975 self.__data_serializer = data_serializer 976 if not isinstance(transport, Transport): 977 raise ValueError('invalid "transport" (must be a Transport-instance)"') 978 self.__transport = transport 979 self.logfile = logfile 980 if self.logfile is not None: #create logfile (or raise exception) 981 f = codecs.open( self.logfile, 'a', encoding='utf-8' ) 982 f.close() 983 984 self.funcs = {} 985 986 def __repr__(self): 987 return "<Server for %s, with serializer %s>" % (self.__transport, self.__data_serializer) 988 989 def log(self, message): 990 """write a message to the logfile (in utf-8)""" 991 if self.logfile is not None: 992 f = codecs.open( self.logfile, 'a', encoding='utf-8' ) 993 f.write( time.strftime("%Y-%m-%d %H:%M:%S ")+message+"\n" ) 994 f.close() 995 996 def register_instance(self, myinst, name=None): 997 """Add all functions of a class-instance to the RPC-services. 998 999 All entries of the instance which do not begin with '_' are added. 1000 1001 :Parameters: 1002 - myinst: class-instance containing the functions 1003 - name: | hierarchical prefix. 1004 | If omitted, the functions are added directly. 1005 | If given, the functions are added as "name.function". 1006 :TODO: 1007 - improve hierarchy? 1008 """ 1009 for e in dir(myinst): 1010 if e[0][0] != "_": 1011 attr = getattr( myinst, e ) 1012 if not isinstance( attr, types.MethodType ): 1013 continue 1014 1015 if name is None: 1016 self.register_function( attr ) 1017 else: 1018 self.register_function( attr, name="%s.%s" % (name, e) ) 1019 def register_function(self, function, name=None): 1020 """Add a function to the RPC-services. 1021 1022 :Parameters: 1023 - function: function to add 1024 - name: RPC-name for the function. If omitted/None, the original 1025 name of the function is used. 1026 """ 1027 if name is None: 1028 self.funcs[function.__name__] = function 1029 else: 1030 self.funcs[name] = function 1031 1032 def handle(self, rpcstr): 1033 """Handle a RPC-Request. 1034 1035 :Parameters: 1036 - rpcstr: the received rpc-string 1037 :Returns: the data to send back or None if nothing should be sent back 1038 :Raises: RPCFault (and maybe others) 1039 """ 1040 #TODO: id 1041 notification = False 1042 try: 1043 req = self.__data_serializer.loads_request( rpcstr ) 1044 if len(req) == 2: #notification 1045 method, params = req 1046 notification = True 1047 else: #request 1048 method, params, id = req 1049 except RPCFault, err: 1050 return self.__data_serializer.dumps_error( err, id=None ) 1051 except Exception, err: 1052 import traceback 1053 print traceback.format_exc() 1054 self.log( "%d (%s): %s" % (INTERNAL_ERROR, ERROR_MESSAGE[INTERNAL_ERROR], repr(err)) ) 1055 return self.__data_serializer.dumps_error( RPCFault(INTERNAL_ERROR, ERROR_MESSAGE[INTERNAL_ERROR]), id=None ) 1056 1057 if method not in self.funcs: 1058 if notification: 1059 return None 1060 return self.__data_serializer.dumps_error( RPCFault(METHOD_NOT_FOUND, ERROR_MESSAGE[METHOD_NOT_FOUND]), id ) 1061 1062 try: 1063 if isinstance(params, dict): 1064 result = self.funcs[method]( **params ) 1065 else: 1066 result = self.funcs[method]( *params ) 1067 except RPCFault, err: 1068 if notification: 1069 return None 1070 return self.__data_serializer.dumps_error( err, id=None ) 1071 except Exception, err: 1072 if notification: 1073 return None 1074 import traceback 1075 print traceback.format_exc() 1076 self.log( "%d (%s): %s" % (INTERNAL_ERROR, ERROR_MESSAGE[INTERNAL_ERROR], repr(err)) ) 1077 self.log(traceback.format_exc()) 1078 return self.__data_serializer.dumps_error( RPCFault(INTERNAL_ERROR, ERROR_MESSAGE[INTERNAL_ERROR]), id ) 1079 print "Serializing response" 1080 if notification: 1081 return None 1082 try: 1083 return self.__data_serializer.dumps_response( result, id ) 1084 except Exception, err: 1085 self.log( "%d (%s): %s" % (INTERNAL_ERROR, ERROR_MESSAGE[INTERNAL_ERROR], repr(err)) ) 1086 return self.__data_serializer.dumps_error( RPCFault(INTERNAL_ERROR, ERROR_MESSAGE[INTERNAL_ERROR]), id ) 1087 1088 def serve(self, n=None): 1089 """serve (forever or for n communicaions). 1090 1091 :See: Transport 1092 """ 1093 self.__transport.serve( self.handle, n ) 1094 1095#=========================================