PageRenderTime 70ms CodeModel.GetById 3ms app.highlight 57ms RepoModel.GetById 1ms app.codeStats 0ms

/pyrope/jsonrpc.py

https://bitbucket.org/tomusdrw/mgr.p2p.proxy
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#=========================================