You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1076 lines
40 KiB
1076 lines
40 KiB
""" |
|
JSON-RPC (remote procedure call). |
|
|
|
It consists of 3 (independent) parts: |
|
- proxy/dispatcher |
|
- data structure / serializer |
|
- transport |
|
|
|
It's intended for JSON-RPC, but since the above 3 parts are independent, |
|
it could be used for other RPCs as well. |
|
|
|
Currently, JSON-RPC 2.0 and JSON-RPC 1.0 are implemented |
|
|
|
:version: 2017-12-03-RELEASE |
|
:status: experimental |
|
|
|
:example: |
|
simple Client with JsonRPC 2.0 and TCP/IP:: |
|
|
|
>>> proxy = ServerProxy( JsonRpc20(), TransportTcpIp(addr=("127.0.0.1",31415)) ) |
|
>>> proxy.echo( "hello world" ) |
|
u'hello world' |
|
>>> proxy.echo( "bye." ) |
|
u'bye.' |
|
|
|
simple Server with JsonRPC2.0 and TCP/IP with logging to STDOUT:: |
|
|
|
>>> server = Server( JsonRpc20(), TransportTcpIp(addr=("127.0.0.1",31415), logfunc=log_stdout) ) |
|
>>> def echo( s ): |
|
... return s |
|
>>> server.register_function( echo ) |
|
>>> server.serve( 2 ) # serve 2 requests # doctest: +ELLIPSIS |
|
listen ('127.0.0.1', 31415) |
|
('127.0.0.1', ...) connected |
|
('127.0.0.1', ...) <-- {"jsonrpc": "2.0", "method": "echo", "params": ["hello world"], "id": 0} |
|
('127.0.0.1', ...) --> {"jsonrpc": "2.0", "result": "hello world", "id": 0} |
|
('127.0.0.1', ...) close |
|
('127.0.0.1', ...) connected |
|
('127.0.0.1', ...) <-- {"jsonrpc": "2.0", "method": "echo", "params": ["bye."], "id": 0} |
|
('127.0.0.1', ...) --> {"jsonrpc": "2.0", "result": "bye.", "id": 0} |
|
('127.0.0.1', ...) close |
|
close ('127.0.0.1', 31415) |
|
|
|
Client with JsonRPC2.0 and an abstract Unix Domain Socket:: |
|
|
|
>>> proxy = ServerProxy( JsonRpc20(), TransportUnixSocket(addr="\\x00.rpcsocket") ) |
|
>>> proxy.hi( message="hello" ) #named parameters |
|
u'hi there' |
|
>>> proxy.test() #fault |
|
Traceback (most recent call last): |
|
... |
|
jsonrpc.RPCMethodNotFound: <RPCFault -32601: u'Method not found.' (None)> |
|
>>> proxy.debug.echo( "hello world" ) #hierarchical procedures |
|
u'hello world' |
|
|
|
Server with JsonRPC2.0 and abstract Unix Domain Socket with a logfile:: |
|
|
|
>>> server = Server( JsonRpc20(), TransportUnixSocket(addr="\\x00.rpcsocket", logfunc=log_file("mylog.txt")) ) |
|
>>> def echo( s ): |
|
... return s |
|
>>> def hi( message ): |
|
... return "hi there" |
|
>>> server.register_function( hi ) |
|
>>> server.register_function( echo, name="debug.echo" ) |
|
>>> server.serve( 3 ) # serve 3 requests |
|
|
|
"mylog.txt" then contains: |
|
listen '\\x00.rpcsocket' |
|
'' connected |
|
'' --> '{"jsonrpc": "2.0", "method": "hi", "params": {"message": "hello"}, "id": 0}' |
|
'' <-- '{"jsonrpc": "2.0", "result": "hi there", "id": 0}' |
|
'' close |
|
'' connected |
|
'' --> '{"jsonrpc": "2.0", "method": "test", "id": 0}' |
|
'' <-- '{"jsonrpc": "2.0", "error": {"code":-32601, "message": "Method not found."}, "id": 0}' |
|
'' close |
|
'' connected |
|
'' --> '{"jsonrpc": "2.0", "method": "debug.echo", "params": ["hello world"], "id": 0}' |
|
'' <-- '{"jsonrpc": "2.0", "result": "hello world", "id": 0}' |
|
'' close |
|
close '\\x00.rpcsocket' |
|
|
|
:note: all exceptions derived from RPCFault are propagated to the client. |
|
other exceptions are logged and result in a sent-back "empty" INTERNAL_ERROR. |
|
:uses: logging, sys, json, codecs, time, socket, select |
|
:seealso: JSON-RPC 2.0 proposal, 1.0 specification |
|
:warning: |
|
.. Warning:: |
|
This is **experimental** code! |
|
:author: Dave Pedu (dave(at)davepedu.com) |
|
:changelog: |
|
- 2008-08-31: 1st release |
|
- 2017-12-03-RELEASE Modern python 3.0 rewrite |
|
|
|
:todo: |
|
- server: multithreading rpc-server |
|
- client: multicall (send several requests) |
|
- transport: SSL sockets, maybe HTTP, HTTPS |
|
- types: support for date/time (ISO 8601) |
|
- errors: maybe customizable error-codes/exceptions |
|
- mixed 1.0/2.0 server ? |
|
- system description etc. ? |
|
- maybe test other json-serializers, like cjson? |
|
""" |
|
|
|
__version__ = "2017-12-03-RELEASE" |
|
__author__ = "Dave Pedu (dave(at)davepedu.com)" |
|
|
|
|
|
# ========================================= |
|
# imports |
|
|
|
import logging |
|
import sys |
|
import json #TODO faster implementation |
|
import codecs |
|
import time |
|
import socket |
|
import select |
|
|
|
|
|
# ========================================= |
|
# errors |
|
|
|
# JSON-RPC 2.0 error-codes |
|
PARSE_ERROR = -32700 |
|
INVALID_REQUEST = -32600 |
|
METHOD_NOT_FOUND = -32601 |
|
INVALID_METHOD_PARAMS = -32602 # invalid number/type of parameters |
|
INTERNAL_ERROR = -32603 # "all other errors" |
|
|
|
# additional error-codes |
|
PROCEDURE_EXCEPTION = -32000 |
|
AUTHENTIFICATION_ERROR = -32001 |
|
PERMISSION_DENIED = -32002 |
|
INVALID_PARAM_VALUES = -32003 |
|
|
|
# human-readable messages |
|
ERROR_MESSAGE = { |
|
PARSE_ERROR: "Parse error.", |
|
INVALID_REQUEST: "Invalid Request.", |
|
METHOD_NOT_FOUND: "Method not found.", |
|
INVALID_METHOD_PARAMS: "Invalid parameters.", |
|
INTERNAL_ERROR: "Internal error.", |
|
PROCEDURE_EXCEPTION: "Procedure exception.", |
|
AUTHENTIFICATION_ERROR: "Authentification error.", |
|
PERMISSION_DENIED: "Permission denied.", |
|
INVALID_PARAM_VALUES: "Invalid parameter values." |
|
} |
|
|
|
|
|
# ---------------------- |
|
# exceptions |
|
|
|
|
|
class RPCError(Exception): |
|
"""Base class for rpc-errors.""" |
|
|
|
|
|
class RPCTransportError(RPCError): |
|
"""Transport error.""" |
|
|
|
|
|
class RPCTimeoutError(RPCTransportError): |
|
"""Transport/reply timeout.""" |
|
|
|
|
|
class RPCFault(RPCError): |
|
"""RPC error/fault package received. |
|
|
|
This exception can also be used as a class, to generate a |
|
RPC-error/fault message. |
|
|
|
:Variables: |
|
- error_code: the RPC error-code |
|
- error_string: description of the error |
|
- error_data: optional additional information |
|
(must be json-serializable) |
|
:TODO: improve __str__ |
|
""" |
|
def __init__(self, error_code, error_message, error_data=None): |
|
RPCError.__init__(self) |
|
self.error_code = error_code |
|
self.error_message = error_message |
|
self.error_data = error_data |
|
|
|
def __str__(self): |
|
return repr(self) |
|
|
|
def __repr__(self): |
|
return "<RPCFault %s: %s (%s)>" % (self.error_code, repr(self.error_message), repr(self.error_data)) |
|
|
|
|
|
class RPCParseError(RPCFault): |
|
"""Broken rpc-package. (PARSE_ERROR)""" |
|
def __init__(self, error_data=None): |
|
RPCFault.__init__(self, PARSE_ERROR, ERROR_MESSAGE[PARSE_ERROR], error_data) |
|
|
|
|
|
class RPCInvalidRPC(RPCFault): |
|
"""Invalid rpc-package. (INVALID_REQUEST)""" |
|
def __init__(self, error_data=None): |
|
RPCFault.__init__(self, INVALID_REQUEST, ERROR_MESSAGE[INVALID_REQUEST], error_data) |
|
|
|
|
|
class RPCMethodNotFound(RPCFault): |
|
"""Method not found. (METHOD_NOT_FOUND)""" |
|
def __init__(self, error_data=None): |
|
RPCFault.__init__(self, METHOD_NOT_FOUND, ERROR_MESSAGE[METHOD_NOT_FOUND], error_data) |
|
|
|
|
|
class RPCInvalidMethodParams(RPCFault): |
|
"""Invalid method-parameters. (INVALID_METHOD_PARAMS)""" |
|
def __init__(self, error_data=None): |
|
RPCFault.__init__(self, INVALID_METHOD_PARAMS, ERROR_MESSAGE[INVALID_METHOD_PARAMS], error_data) |
|
|
|
|
|
class RPCInternalError(RPCFault): |
|
"""Internal error. (INTERNAL_ERROR)""" |
|
def __init__(self, error_data=None): |
|
RPCFault.__init__(self, INTERNAL_ERROR, ERROR_MESSAGE[INTERNAL_ERROR], error_data) |
|
|
|
|
|
class RPCProcedureException(RPCFault): |
|
"""Procedure exception. (PROCEDURE_EXCEPTION)""" |
|
def __init__(self, error_data=None): |
|
RPCFault.__init__(self, PROCEDURE_EXCEPTION, ERROR_MESSAGE[PROCEDURE_EXCEPTION], error_data) |
|
|
|
|
|
class RPCAuthentificationError(RPCFault): |
|
"""AUTHENTIFICATION_ERROR""" |
|
def __init__(self, error_data=None): |
|
RPCFault.__init__(self, AUTHENTIFICATION_ERROR, ERROR_MESSAGE[AUTHENTIFICATION_ERROR], error_data) |
|
|
|
|
|
class RPCPermissionDenied(RPCFault): |
|
"""PERMISSION_DENIED""" |
|
def __init__(self, error_data=None): |
|
RPCFault.__init__(self, PERMISSION_DENIED, ERROR_MESSAGE[PERMISSION_DENIED], error_data) |
|
|
|
|
|
class RPCInvalidParamValues(RPCFault): |
|
"""INVALID_PARAM_VALUES""" |
|
def __init__(self, error_data=None): |
|
RPCFault.__init__(self, INVALID_PARAM_VALUES, ERROR_MESSAGE[INVALID_PARAM_VALUES], error_data) |
|
|
|
|
|
ERROR_CODE_EXCEPTIONS = {PARSE_ERROR: RPCParseError, |
|
INVALID_REQUEST: RPCInvalidRPC, |
|
METHOD_NOT_FOUND: RPCMethodNotFound, |
|
INVALID_METHOD_PARAMS: RPCInvalidMethodParams, |
|
INTERNAL_ERROR: RPCInternalError, |
|
PROCEDURE_EXCEPTION: RPCProcedureException, |
|
AUTHENTIFICATION_ERROR: RPCAuthentificationError, |
|
PERMISSION_DENIED: RPCPermissionDenied, |
|
INVALID_PARAM_VALUES: RPCInvalidParamValues} |
|
|
|
|
|
class JsonRpc10: |
|
""" |
|
JSON-RPC V1.0 data-structure / serializer |
|
|
|
This implementation is quite liberal in what it accepts: It treats |
|
missing "params" and "id" in Requests and missing "result"/"error" in |
|
Responses as empty/null. |
|
|
|
:seealso: JSON-RPC 1.0 specification |
|
:todo: catch json.dumps not-serializable-exceptions |
|
""" |
|
def __init__(self, dumps=json.dumps, loads=json.loads): |
|
""" |
|
init: set serializer to use |
|
|
|
:param dumps: json-encoder-function |
|
:param loads: json-decoder-function |
|
:note: The dumps_* functions of this class already directly create the invariant parts of the resulting |
|
json-object themselves, without using the given json-encoder-function. |
|
""" |
|
self.dumps = dumps |
|
self.loads = loads |
|
|
|
def dumps_request(self, method, params=(), id=0): |
|
""" |
|
serialize JSON-RPC-Request |
|
|
|
:param method: the method-name |
|
:type method: str |
|
:param params: the parameters |
|
:type params: list, tuple |
|
:param id: if id isNone, this results in a Notification |
|
:return: str like`{"method": "...", "params": ..., "id": ...}`. "method", "params" and "id" are always in this |
|
order. |
|
:raises: TypeError if method/params is of wrong type or not JSON-serializable |
|
""" |
|
if not isinstance(method, str): |
|
raise TypeError('"method" must be a string (or unicode string).') |
|
if not isinstance(params, (tuple, list)): |
|
raise TypeError("params must be a tuple/list, got {}".format(type(params))) |
|
|
|
return '{{"method": {}, "params": {}, "id": {}}}'.format(self.dumps(method), self.dumps(params), self.dumps(id)) |
|
|
|
def dumps_notification(self, method, params=()): |
|
""" |
|
serialize a JSON-RPC-Notification |
|
|
|
:param method: the method-name |
|
:type method: str |
|
:param params: the parameters |
|
:type params: list, tuple |
|
:return: str like `{"method": "...", "params": ..., "id": null}`. "method", "params" and "id" are always in this |
|
order. |
|
:raises: TypeError if method/params is of wrong type or not JSON-serializable |
|
""" |
|
if not isinstance(method, str): |
|
raise TypeError('"method" must be a string (or unicode string).') |
|
if not isinstance(params, (tuple, list)): |
|
raise TypeError("params must be a tuple/list.") |
|
|
|
return '{{"method": {}, "params": {}, "id": null}}'.format(self.dumps(method), self.dumps(params)) |
|
|
|
def dumps_response(self, result, id=None): |
|
""" |
|
serialize a JSON-RPC-Response (without error) |
|
|
|
:return: str like `{"result": ..., "error": null, "id": ...}`. "result", "error" and "id" are always in this |
|
order. |
|
:raises: TypeError if not JSON-serializable |
|
""" |
|
return '{{"result": {}, "error": null, "id": {}}}'.format(self.dumps(result), self.dumps(id)) |
|
|
|
def dumps_error(self, error, id=None): |
|
""" |
|
serialize a JSON-RPC-Response-error |
|
|
|
Since JSON-RPC 1.0 does not define an error-object, this uses the |
|
JSON-RPC 2.0 error-object. |
|
|
|
:param error: an RPCFault instance |
|
:type error: RPCFault |
|
:returns: str like `{"result": null, "error": {"code": code, "message": message, "data": data}, "id": ...}`. |
|
"result", "error" and "id" are always in this order, data is omitted if None. |
|
:raises: ValueError if error is not a RPCFault instance, |
|
TypeError if not JSON-serializable |
|
""" |
|
if not isinstance(error, RPCFault): |
|
raise ValueError("error must be a RPCFault-instance.") |
|
if error.error_data is None: |
|
return '{{"result": null, "error": {{"code": {}, "message": {}}}, "id": {}}}' \ |
|
.format(self.dumps(error.error_code), self.dumps(error.error_message), self.dumps(id)) |
|
else: |
|
return '{{"result": null, "error": {{"code": {}, "message": {}, "data": {}}}, "id": {}}}' \ |
|
.format(self.dumps(error.error_code), self.dumps(error.error_message), |
|
self.dumps(error.error_data), self.dumps(id)) |
|
|
|
def loads_request(self, string): |
|
""" |
|
de-serialize a JSON-RPC Request/Notification |
|
|
|
:returns: list like `[method_name, params, id]` or `[method_name, params]`. params is a tuple/list. if id is |
|
missing, this is a Notification |
|
:raises: RPCParseError, RPCInvalidRPC, RPCInvalidMethodParams |
|
""" |
|
try: |
|
data = self.loads(string) |
|
except ValueError as err: |
|
raise RPCParseError("No valid JSON. (%s)" % str(err)) |
|
if not isinstance(data, dict): |
|
raise RPCInvalidRPC("No valid RPC-package.") |
|
if "method" not in data: |
|
raise RPCInvalidRPC('Invalid Request, "method" is missing.') |
|
if not isinstance(data["method"], str, ): |
|
raise RPCInvalidRPC('Invalid Request, "method" must be a string.') |
|
if "id" not in data: |
|
data["id"] = None |
|
if "params" not in data: |
|
data["params"] = () |
|
if not isinstance(data["params"], (list, tuple)): |
|
raise RPCInvalidRPC('Invalid Request, "params" must be an array.') |
|
if len(data) != 3: |
|
raise RPCInvalidRPC('Invalid Request, additional fields found.') |
|
|
|
# notification / request |
|
if data["id"] is None: |
|
return data["method"], data["params"] # notification |
|
else: |
|
return data["method"], data["params"], data["id"] # request |
|
|
|
def loads_response(self, string): |
|
""" |
|
de-serialize a JSON-RPC Response/error |
|
|
|
:return: list like `[result, id]` for Responses |
|
:raises: | RPCFault+derivates for error-packages/faults, RPCParseError, RPCInvalidRPC |
|
:note: error-packages which do not match the V2.0-definition, RPCFault(-1, "Error", RECEIVED_ERROR_OBJ) is |
|
instead raised. |
|
""" |
|
try: |
|
data = self.loads(string) |
|
except ValueError as err: |
|
raise RPCParseError("No valid JSON. (%s)" % str(err)) |
|
if not isinstance(data, dict): |
|
raise RPCInvalidRPC("No valid RPC-package.") |
|
if "id" not in data: |
|
raise RPCInvalidRPC('Invalid Response, "id" missing.') |
|
if "result" not in data: |
|
data["result"] = None |
|
if "error" not in data: |
|
data["error"] = None |
|
if len(data) != 3: |
|
raise RPCInvalidRPC('Invalid Response, additional or missing fields.') |
|
|
|
if data["error"] is not None: |
|
if data["result"] is not None: |
|
raise RPCInvalidRPC('Invalid Response, one of "result" or "error" must be null.') |
|
# v2.0 error-format |
|
if (isinstance(data["error"], dict) and "code" in data["error"] and "message" in data["error"] and |
|
(len(data["error"]) == 2 or ("data" in data["error"] and len(data["error"]) == 3))): |
|
if "data" not in data["error"]: |
|
error_data = None |
|
else: |
|
error_data = data["error"]["data"] |
|
error_code = data["error"]["code"] |
|
if error_code in ERROR_CODE_EXCEPTIONS: |
|
raise ERROR_CODE_EXCEPTIONS[error_code](error_data) |
|
else: |
|
raise RPCFault(data["error"]["code"], data["error"]["message"], error_data) |
|
else: # other error-format |
|
raise RPCFault(-1, "Error", data["error"]) |
|
else: #successful result |
|
return data["result"], data["id"] |
|
|
|
|
|
class JsonRpc20(object): |
|
""" |
|
JSON-RPC V2.0 data-structure / serializer |
|
|
|
:see: JSON-RPC 2.0 specification |
|
:todo: catch simplejson.dumps not-serializable-exceptions |
|
:todo: rewrite serializer as modern java encoder subclass? support for more types this way? |
|
""" |
|
def __init__(self, dumps=json.dumps, loads=json.loads): |
|
""" |
|
init: set serializer to use |
|
|
|
:param dumps: json-encoder-function |
|
:param loads: json-decoder-function |
|
:note: The dumps_* functions of this class already directly create the invariant parts of the resulting |
|
json-object themselves, without using the given json-encoder-function. |
|
""" |
|
self.dumps = dumps |
|
self.loads = loads |
|
|
|
def dumps_request(self, method, params=(), id=0): |
|
""" |
|
serialize a JSON-RPC-Request to string |
|
|
|
:param method: name of the method to call |
|
:type methods: str |
|
:param params: data structure of args |
|
:type params: dict,list,tuple |
|
: type id: request id (should not be None) |
|
:returns: string like: `{"jsonrpc": "2.0", "method": "...", "params": ..., "id": ...}`. "jsonrpc", "method", |
|
"params" and "id" are always in this order. "params" is omitted if empty |
|
:raises: TypeError if method/params is of wrong type or not JSON-serializable |
|
""" |
|
if not isinstance(method, (str)): |
|
raise TypeError('"method" must be a string (or unicode string).') |
|
if not isinstance(params, (tuple, list, dict)): |
|
raise TypeError("params must be a tuple/list/dict or None.") |
|
|
|
if params: |
|
return '{{"jsonrpc": "2.0", "method": {}, "params": {}, "id": {}}}' \ |
|
.format(self.dumps(method), self.dumps(params), self.dumps(id)) |
|
else: |
|
return '{{"jsonrpc": "2.0", "method": {}, "id": {}}}'.format(self.dumps(method), self.dumps(id)) |
|
|
|
def dumps_notification(self, method, params=()): |
|
""" |
|
serialize a JSON-RPC-Notification |
|
|
|
:param method: name of the method to call |
|
:type methods: str |
|
:param params: data structure of args |
|
:type params: dict,list,tuple |
|
:return: String like `{"jsonrpc": "2.0", "method": "...", "params": ...}`. "jsonrpc", "method" and "params" |
|
are always in this order. |
|
:raises: see dumps_request |
|
""" |
|
if not isinstance(method, str): |
|
raise TypeError('"method" must be a string (or unicode string).') |
|
if not isinstance(params, (tuple, list, dict)): |
|
raise TypeError("params must be a tuple/list/dict or None.") |
|
|
|
if params: |
|
return '{{"jsonrpc": "2.0", "method": {}, "params": {}}}'.format(self.dumps(method), self.dumps(params)) |
|
else: |
|
return '{{"jsonrpc": "2.0", "method": {}}}'.format(self.dumps(method)) |
|
|
|
def dumps_response(self, result, id=None): |
|
""" |
|
serialize a JSON-RPC-Response (without error) |
|
|
|
:returns: str like `{"jsonrpc": "2.0", "result": ..., "id": ...}`."jsonrpc", "result", and "id" are always in |
|
this order. |
|
:raises: TypeError if not JSON-serializable |
|
""" |
|
return '{{"jsonrpc": "2.0", "result": {}, "id": {}}}'.format(self.dumps(result), self.dumps(id)) |
|
|
|
def dumps_error(self, error, id=None): |
|
""" |
|
serialize a JSON-RPC-Response-error |
|
|
|
:param error: error to serialize |
|
:type error: RPCFault |
|
:return: str like `{"jsonrpc": "2.0", "error": {"code": code, "message": message, "data": data}, "id": ...}`. |
|
"jsonrpc", "result", "error" and "id" are always in this order, data is omitted if None. |
|
:raises: ValueError if error is not a RPCFault instance, |
|
TypeError if not JSON-serializable |
|
""" |
|
if not isinstance(error, RPCFault): |
|
raise ValueError("error must be a RPCFault-instance.") |
|
if error.error_data is None: |
|
return '{{"jsonrpc": "2.0", "error": {{"code": {}, "message": {}}}, "id": {}}}' \ |
|
.format(self.dumps(error.error_code), self.dumps(error.error_message), self.dumps(id)) |
|
else: |
|
return '{{"jsonrpc": "2.0", "error": {{"code": {}, "message": {}, "data": {}}}, "id": {}}}' \ |
|
.format(self.dumps(error.error_code), self.dumps(error.error_message), |
|
self.dumps(error.error_data), self.dumps(id)) |
|
|
|
def loads_request(self, string): |
|
""" |
|
de-serialize a JSON-RPC Request or Notification |
|
|
|
:return: `[method_name, params, id] or [method_name, params]`. `params` is a tuple/list or dict (with only |
|
str-keys). if id is missing, this is a Notification |
|
:raises: RPCParseError, RPCInvalidRPC, RPCInvalidMethodParams |
|
""" |
|
try: |
|
data = self.loads(string) |
|
except ValueError as err: |
|
raise RPCParseError("No valid JSON. ({})".format(err)) |
|
if not isinstance(data, dict): |
|
raise RPCInvalidRPC("No valid RPC-package.") |
|
if "jsonrpc" not in data: |
|
raise RPCInvalidRPC('Invalid Response, "jsonrpc" missing.') |
|
if not isinstance(data["jsonrpc"], str): |
|
raise RPCInvalidRPC('Invalid Response, "jsonrpc" must be a string.') |
|
if data["jsonrpc"] != "2.0": |
|
raise RPCInvalidRPC("Invalid jsonrpc version.") |
|
if "method" not in data: |
|
raise RPCInvalidRPC('Invalid Request, "method" is missing.') |
|
if not isinstance(data["method"], str): |
|
raise RPCInvalidRPC('Invalid Request, "method" must be a string.') |
|
if "params" not in data: |
|
data["params"] = () |
|
elif not isinstance(data["params"], (list, tuple)): |
|
raise RPCInvalidRPC('Invalid Request, "params" must be an array or object.') |
|
if not(len(data) == 3 or ("id" in data and len(data) == 4)): |
|
raise RPCInvalidRPC('Invalid Request, additional fields found.') |
|
# notification / request |
|
if "id" in data: |
|
return data["method"], data["params"], data["id"] # request |
|
else: |
|
return data["method"], data["params"] # notification |
|
|
|
def loads_response(self, string): |
|
""" |
|
de-serialize a JSON-RPC Response/error |
|
|
|
:return: [result, id] for Responses |
|
:raises: RPCFault+derivates for error-packages/faults, RPCParseError, RPCInvalidRPC |
|
""" |
|
try: |
|
data = self.loads(string) |
|
except ValueError as err: |
|
raise RPCParseError("No valid JSON. ({})".format(err)) |
|
if not isinstance(data, dict): |
|
raise RPCInvalidRPC("No valid RPC-package.") |
|
if "jsonrpc" not in data: |
|
raise RPCInvalidRPC('Invalid Response, "jsonrpc" missing.') |
|
if not isinstance(data["jsonrpc"], (str)): |
|
raise RPCInvalidRPC('Invalid Response, "jsonrpc" must be a string.') |
|
if data["jsonrpc"] != "2.0": |
|
raise RPCInvalidRPC('Invalid jsonrpc version.') |
|
if "id" not in data: |
|
raise RPCInvalidRPC('Invalid Response, "id" missing.') |
|
if "result" not in data: |
|
data["result"] = None |
|
if "error" not in data: |
|
data["error"] = None |
|
if len(data) != 4: |
|
raise RPCInvalidRPC("Invalid Response, additional or missing fields.") |
|
if data["error"] is not None: # handle remote error case |
|
if data["result"] is not None: |
|
raise RPCInvalidRPC('Invalid Response, only "result" OR "error" allowed.') |
|
if not isinstance(data["error"], dict): |
|
raise RPCInvalidRPC("Invalid Response, invalid error-object.") |
|
if "code" not in data["error"] or "message" not in data["error"]: |
|
raise RPCInvalidRPC("Invalid Response, invalid error-object.") |
|
if "data" not in data["error"]: |
|
data["error"]["data"] = None |
|
if len(data["error"]) != 3: |
|
raise RPCInvalidRPC("Invalid Response, invalid error-object.") |
|
error_data = data["error"]["data"] |
|
error_code = data["error"]["code"] |
|
if error_code in ERROR_CODE_EXCEPTIONS: |
|
raise ERROR_CODE_EXCEPTIONS[error_code](error_data) |
|
else: |
|
raise RPCFault(data["error"]["code"], data["error"]["message"], error_data) |
|
else: # successful call, return result |
|
return data["result"], data["id"] |
|
|
|
|
|
def log_dummy(message): |
|
pass |
|
|
|
|
|
def log_stdout(message): |
|
""" |
|
print message to STDOUT |
|
""" |
|
print(message) |
|
|
|
|
|
def log_file(filename): |
|
"""return a logfunc which logs to a file (in utf-8)""" |
|
def logfile(message): |
|
f = codecs.open(filename, 'a') |
|
f.write(message + "\n") |
|
f.close() |
|
return logfile |
|
|
|
|
|
def log_filedate(filename): |
|
"""return a logfunc which logs date+message to a file (in utf-8)""" |
|
def logfile(message): |
|
f = codecs.open(filename, 'a') |
|
f.write("{} {}\n".format(time.strftime("%Y-%m-%d %H:%M:%S"), message)) |
|
f.close() |
|
return logfile |
|
|
|
|
|
class Transport(object): |
|
""" |
|
generic Transport-interface. |
|
|
|
This class, and especially its methods and docstrings, define the Transport-Interface. |
|
""" |
|
def __init__(self): |
|
pass |
|
|
|
def send(self, data): |
|
""" |
|
send all data. must be implemented by derived classes. |
|
:param data: data to send |
|
:type data: str |
|
""" |
|
raise NotImplementedError |
|
|
|
def recv(self): |
|
""" |
|
receive data. must be implemented by derived classes. |
|
:return str: |
|
""" |
|
raise NotImplementedError |
|
|
|
def sendrecv(self, string): |
|
""" |
|
send + receive data |
|
:param string: message to send |
|
:type string: str |
|
""" |
|
self.send(string) |
|
return self.recv() |
|
|
|
def serve(self, handler, n=None): |
|
""" |
|
serve (forever or for n communicaions). |
|
|
|
- receive data |
|
- call result = handler(data) |
|
- send back result if not None |
|
|
|
The serving can be stopped by SIGINT. |
|
|
|
:TODO: |
|
- how to stop? |
|
maybe use a .run-file, and stop server if file removed? |
|
- maybe make n_current accessible? (e.g. for logging) |
|
""" |
|
n_current = 0 |
|
while 1: |
|
if n is not None and n_current >= n: |
|
break |
|
data = self.recv() |
|
result = handler(data) |
|
if result is not None: |
|
self.send(result) |
|
n_current += 1 |
|
|
|
|
|
class TransportSTDINOUT(Transport): |
|
""" |
|
receive from STDIN, send to STDOUT. Useful e.g. for debugging. |
|
""" |
|
def send(self, string): |
|
""" |
|
write data to STDOUT with '\*\*\*SEND:' prefix |
|
""" |
|
print("***SEND:") |
|
print(string) |
|
|
|
def recv(self): |
|
"""read data from STDIN""" |
|
print("***RECV (please enter, ^D ends.):") |
|
return sys.stdin.read() |
|
|
|
|
|
class TransportSocket(Transport): |
|
""" |
|
Transport via socket. |
|
|
|
:TODO: |
|
- documentation |
|
- improve this (e.g. make sure that connections are closed, socket-files are deleted etc.) |
|
- exception-handling? (socket.error) |
|
""" |
|
def __init__(self, addr, limit=4096, |
|
sock_type=socket.AF_INET, sock_prot=socket.SOCK_STREAM, timeout=1.0, |
|
logfunc=log_dummy): |
|
""" |
|
:param addr: socket-address |
|
:param timeout: connect timeout in seconds |
|
:param logfunc: function for logging, logfunc(message) |
|
:Raises: socket.timeout after timeout |
|
""" |
|
self.limit = limit |
|
self.addr = addr |
|
self.s_type = sock_type |
|
self.s_prot = sock_prot |
|
self.s = None |
|
self.timeout = timeout |
|
self.log = logfunc |
|
|
|
def _send(self, conn, result): |
|
""" |
|
Send a result to the given connection |
|
:param conn: |
|
:param result: text result to send |
|
:type result str: |
|
""" |
|
conn.send(result.encode("UTF-8")) |
|
|
|
def connect(self): |
|
self.close() |
|
self.log("connect to %s" % repr(self.addr)) |
|
self.s = socket.socket(self.s_type, self.s_prot) |
|
self.s.settimeout(self.timeout) |
|
self.s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) |
|
self.s.connect(self.addr) |
|
|
|
def close(self): |
|
if self.s: |
|
self.log("close %s" % repr(self.addr)) |
|
self.s.close() |
|
self.s = None |
|
|
|
def __repr__(self): |
|
return "<TransportSocket, %s>" % repr(self.addr) |
|
|
|
def send(self, string): |
|
if not self.s: |
|
self.connect() |
|
self.log("--> {}".format(repr(string))) |
|
self.s.sendall(string.encode("UTF-8")) |
|
|
|
def recv(self): |
|
if not self.s: |
|
self.connect() |
|
data = self.s.recv(self.limit) |
|
#TODO: this select is probably not necessary, because server closes this socket |
|
while select.select((self.s,), (), (), 0.1)[0]: |
|
d = self.s.recv(self.limit) |
|
if len(d) == 0: |
|
break |
|
data += d |
|
self.log("<-- {}".format(repr(data))) |
|
return data.decode("UTF-8") |
|
|
|
def sendrecv(self, string): |
|
"""send data + receive data + close""" |
|
try: |
|
self.send(string) |
|
return self.recv() |
|
finally: |
|
self.close() |
|
|
|
def serve(self, handler, n=None): |
|
"""open socket, wait for incoming connections and handle them. |
|
|
|
:Parameters: |
|
- n: serve n requests, None=forever |
|
""" |
|
self.close() |
|
self.s = socket.socket(self.s_type, self.s_prot) |
|
self.s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) |
|
try: |
|
self.log("listen {}".format(repr(self.addr))) |
|
self.s.bind(self.addr) |
|
self.s.listen(1) |
|
n_current = 0 |
|
while 1: |
|
if n is not None and n_current >= n: |
|
break |
|
try: |
|
conn, addr = self.s.accept() |
|
except OSError: # Socket likely shut down |
|
break |
|
self.log("%s connected" % repr(addr)) |
|
data = conn.recv(self.limit) |
|
self.log("%s --> %s" % (repr(addr), repr(data))) |
|
result = handler(data) |
|
if data: |
|
self.log("%s <-- %s" % (repr(addr), repr(result))) |
|
self._send(conn, result) |
|
self.log("%s close" % repr(addr)) |
|
conn.close() |
|
n_current += 1 |
|
finally: |
|
self.close() |
|
|
|
|
|
if hasattr(socket, 'AF_UNIX'): |
|
class TransportUnixSocket(TransportSocket): |
|
""" |
|
Transport via Unix Domain Socket. |
|
""" |
|
def __init__(self, addr=None, limit=4096, timeout=1.0, logfunc=log_dummy): |
|
""" |
|
:param addr: path to socket file |
|
:type addr: str |
|
:note: The socket-file is not deleted. If the socket-file begins with \x00, abstract sockets are used, |
|
and no socket-file is created. |
|
:see: TransportSocket |
|
""" |
|
TransportSocket.__init__(self, addr, limit, socket.AF_UNIX, socket.SOCK_STREAM, timeout, logfunc) |
|
|
|
|
|
class TransportTcpIp(TransportSocket): |
|
""" |
|
Transport via TCP/IP. |
|
""" |
|
def __init__(self, addr=None, limit=4096, timeout=1.0, logfunc=log_dummy): |
|
""" |
|
:param addr: ("host", port) |
|
:type param: tuple |
|
:see: TransportSocket |
|
""" |
|
TransportSocket.__init__(self, addr, limit, socket.AF_INET, socket.SOCK_STREAM, timeout, logfunc) |
|
|
|
|
|
class ServerProxy(object): |
|
"""RPC-client: server proxy |
|
|
|
A client-side logical connection to a RPC server. |
|
|
|
It works with different data/serializers and different transports. |
|
|
|
Notifications and id-handling/multicall are not yet implemented. |
|
|
|
:example: see module-docstring |
|
:todo: verbose/logging? |
|
""" |
|
def __init__(self, data_serializer, transport): |
|
""" |
|
:param data_serializer: a data_structure+serializer-instance |
|
:param transport: a Transport instance |
|
""" |
|
#TODO: check parameters |
|
self.__data_serializer = data_serializer |
|
if not isinstance(transport, Transport): |
|
raise ValueError('invalid "transport" (must be a Transport-instance)"') |
|
self.__transport = transport |
|
|
|
def __str__(self): |
|
return repr(self) |
|
|
|
def __repr__(self): |
|
return "<ServerProxy for %s, with serializer %s>" % (self.__transport, self.__data_serializer) |
|
|
|
def __req(self, methodname, args=None, kwargs=None, id=0): |
|
# JSON-RPC 1.0: only positional parameters |
|
if kwargs and isinstance(self.data_serializer, JsonRpc10): |
|
raise ValueError("Only positional parameters allowed in JSON-RPC 1.0") |
|
# JSON-RPC 2.0: only args OR kwargs allowed! |
|
if args and kwargs: |
|
raise ValueError("Only positional or named parameters are allowed!") |
|
req_str = self.__data_serializer.dumps_request(methodname, |
|
args if isinstance(self.data_serializer, JsonRpc10) else args, |
|
id) |
|
|
|
try: |
|
resp_str = self.__transport.sendrecv(req_str) |
|
except Exception as err: |
|
raise RPCTransportError(err) |
|
resp = self.__data_serializer.loads_response(resp_str) |
|
return resp[0] |
|
|
|
def __getattr__(self, name): |
|
# magic method dispatcher |
|
# note: to call a remote object with an non-standard name, use |
|
# result getattr(my_server_proxy, "strange-python-name")(args) |
|
return _method(self.__req, name) |
|
|
|
|
|
class _method(object): |
|
""" |
|
Some "magic" to bind an RPC method to an RPC server. A request dispatcher. |
|
|
|
Supports "nested" methods (e.g. examples.getStateName). |
|
|
|
:raises: AttributeError for method-names/attributes beginning with '_'. |
|
""" |
|
def __init__(self, req, name): |
|
if name.startswith("_"): # prevent rpc-calls for proxy._*-functions |
|
raise AttributeError("invalid attribute '%s'" % name) |
|
self.__req = req |
|
self.__name = name |
|
|
|
def __getattr__(self, name): |
|
if name.startswith("_"): # prevent rpc-calls for proxy._*-functions |
|
raise AttributeError("invalid attribute '%s'" % name) |
|
return _method(self.__req, "%s.%s" % (self.__name, name)) |
|
|
|
def __call__(self, *args, **kwargs): |
|
return self.__req(self.__name, args, kwargs) |
|
|
|
|
|
class Server(object): |
|
""" |
|
RPC server. |
|
|
|
It works with different data/serializers and |
|
with different transports. |
|
|
|
:Example: |
|
see module-docstring |
|
|
|
:TODO: |
|
- mixed JSON-RPC 1.0/2.0 server? |
|
- logging/loglevels? |
|
""" |
|
def __init__(self, data_serializer, transport, logfile=None): |
|
""" |
|
:param data_serializer: a data_structure+serializer-instance |
|
:param transport: a Transport instance |
|
:param logfile: file to log ("unexpected") errors to |
|
""" |
|
#TODO: check all parameters |
|
self.__data_serializer = data_serializer |
|
if not isinstance(transport, Transport): |
|
raise ValueError('invalid "transport" (must be a Transport-instance)"') |
|
self.__transport = transport |
|
self.logfile = logfile |
|
if self.logfile: |
|
with open(self.logfile, 'a'): # create logfile (or raise exception) |
|
pass |
|
self.funcs = {} |
|
|
|
def __repr__(self): |
|
return "<Server for %s, with serializer %s>" % (self.__transport, self.__data_serializer) |
|
|
|
def log(self, message): |
|
""" |
|
write a message to the logfile |
|
:param message: log message to write |
|
:type message: str |
|
""" |
|
if self.logfile: |
|
#TODO don't reopen the log every time |
|
with open(self.logfile, 'a') as f: |
|
f.write("{} {}\n".format(time.strftime("%Y-%m-%d %H:%M:%S"), message)) |
|
|
|
def register_instance(self, myinst, name=None): |
|
""" |
|
Add all functions of a class-instance to the RPC-services. |
|
|
|
All entries of the instance which do not begin with '_' are added. |
|
|
|
:param myinst: class-instance containing the functions |
|
:param name: hierarchical prefix. If omitted, the functions are added directly. If given, the functions are |
|
added as "name.function". |
|
:todo: |
|
- only add functions and omit attributes? |
|
- improve hierarchy? |
|
""" |
|
for attr in dir(myinst): |
|
if attr.startswith("_"): |
|
continue |
|
if name: |
|
self.register_function(getattr(myinst, attr), name="{}.{}".format(name, attr)) |
|
else: |
|
self.register_function(getattr(myinst, attr)) |
|
|
|
def register_function(self, function, name=None): |
|
""" |
|
Add a function to the RPC-services. |
|
|
|
:param function: callable to add |
|
:param name: RPC-name for the function. If omitted/None, the original name of the function is used. |
|
:type name: str |
|
""" |
|
self.funcs[name or function.__name__] = function |
|
|
|
def handle(self, rpcstr): |
|
""" |
|
Handle a RPC Request. |
|
|
|
:param rpcstr: the received rpc message |
|
:type rpcstr: str |
|
:Returns: the data to send back or None if nothing should be sent back |
|
:Raises: RPCFault (and maybe others) |
|
""" |
|
#TODO: id |
|
notification = False |
|
try: |
|
req = self.__data_serializer.loads_request(rpcstr) |
|
if len(req) == 2: # notification |
|
method, params = req |
|
notification = True |
|
else: # request |
|
method, params, id = req |
|
except RPCFault as err: |
|
return self.__data_serializer.dumps_error(err, id=None) |
|
except Exception as err: |
|
self.log("%d (%s): %s" % (INTERNAL_ERROR, ERROR_MESSAGE[INTERNAL_ERROR], str(err))) |
|
return self.__data_serializer.dumps_error(RPCFault(INTERNAL_ERROR, ERROR_MESSAGE[INTERNAL_ERROR]), id=None) |
|
|
|
if method not in self.funcs: |
|
if notification: |
|
return None |
|
return self.__data_serializer.dumps_error(RPCFault(METHOD_NOT_FOUND, ERROR_MESSAGE[METHOD_NOT_FOUND]), id) |
|
|
|
try: |
|
if isinstance(params, dict): |
|
result = self.funcs[method](**params) |
|
else: |
|
result = self.funcs[method](*params) |
|
except RPCFault as err: |
|
if notification: |
|
return None |
|
return self.__data_serializer.dumps_error(err, id=None) |
|
except Exception as err: |
|
logging.getLogger('RPCLib').error("Error executing RPC: %s" % str(err)) |
|
if notification: |
|
return None |
|
self.log("%d (%s): %s" % (INTERNAL_ERROR, ERROR_MESSAGE[INTERNAL_ERROR], str(err))) |
|
return self.__data_serializer.dumps_error( |
|
RPCFault(INTERNAL_ERROR, ERROR_MESSAGE[INTERNAL_ERROR], str(err)), id) |
|
|
|
if notification: |
|
return None |
|
|
|
try: |
|
return self.__data_serializer.dumps_response(result, id) |
|
except Exception as err: |
|
self.log("%d (%s): %s" % (INTERNAL_ERROR, ERROR_MESSAGE[INTERNAL_ERROR], str(err))) |
|
return self.__data_serializer.dumps_error(RPCFault(INTERNAL_ERROR, ERROR_MESSAGE[INTERNAL_ERROR]), id) |
|
|
|
def serve(self, n=None): |
|
""" |
|
Run the server (forever or for n communicaions). |
|
|
|
:see: Transport |
|
""" |
|
self.__transport.serve(self.handle, n)
|
|
|