Awesome IRC bot
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

"""
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)