diff options
author | Nathan Fritz <nathan@andyet.net> | 2011-02-05 04:54:52 -0800 |
---|---|---|
committer | Nathan Fritz <nathan@andyet.net> | 2011-02-05 04:54:52 -0800 |
commit | 683f717cf756b6487c9dcdfcc0aee4923da7f7c9 (patch) | |
tree | 0a23b3fae5171f0b7bf62cea9d6c6ad3a66e5ea8 | |
parent | 8dbe6f65462ec9b1a0506a00316415996f4d53d8 (diff) | |
parent | b68e7bed403924dc4ebf7294854d4892c48ce0ab (diff) | |
download | slixmpp-683f717cf756b6487c9dcdfcc0aee4923da7f7c9.tar.gz slixmpp-683f717cf756b6487c9dcdfcc0aee4923da7f7c9.tar.bz2 slixmpp-683f717cf756b6487c9dcdfcc0aee4923da7f7c9.tar.xz slixmpp-683f717cf756b6487c9dcdfcc0aee4923da7f7c9.zip |
fixed merge
-rw-r--r-- | examples/rpc_async.py | 44 | ||||
-rw-r--r-- | examples/rpc_client_side.py | 53 | ||||
-rw-r--r-- | examples/rpc_server_side.py | 52 | ||||
-rw-r--r-- | setup.py | 2 | ||||
-rw-r--r-- | sleekxmpp/plugins/old_0009.py (renamed from sleekxmpp/plugins/xep_0009.py) | 0 | ||||
-rw-r--r-- | sleekxmpp/plugins/xep_0009/__init__.py | 11 | ||||
-rw-r--r-- | sleekxmpp/plugins/xep_0009/binding.py | 168 | ||||
-rw-r--r-- | sleekxmpp/plugins/xep_0009/remote.py | 752 | ||||
-rw-r--r-- | sleekxmpp/plugins/xep_0009/rpc.py | 221 | ||||
-rw-r--r-- | sleekxmpp/plugins/xep_0009/stanza/RPC.py | 68 | ||||
-rw-r--r-- | sleekxmpp/plugins/xep_0009/stanza/__init__.py | 9 | ||||
-rw-r--r-- | tests/test_stanza_xep_0009.py | 55 |
12 files changed, 1435 insertions, 0 deletions
diff --git a/examples/rpc_async.py b/examples/rpc_async.py new file mode 100644 index 00000000..0b6d1936 --- /dev/null +++ b/examples/rpc_async.py @@ -0,0 +1,44 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2011 Dann Martens + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from sleekxmpp.plugins.xep_0009.remote import Endpoint, remote, Remote, \ + ANY_ALL, Future +import time + +class Boomerang(Endpoint): + + def FQN(self): + return 'boomerang' + + @remote + def throw(self): + print "Duck!" + + + +def main(): + + session = Remote.new_session('kangaroo@xmpp.org/rpc', '*****') + + session.new_handler(ANY_ALL, Boomerang) + + boomerang = session.new_proxy('kangaroo@xmpp.org/rpc', Boomerang) + + callback = Future() + + boomerang.async(callback).throw() + + time.sleep(10) + + session.close() + + + +if __name__ == '__main__': + main() +
\ No newline at end of file diff --git a/examples/rpc_client_side.py b/examples/rpc_client_side.py new file mode 100644 index 00000000..ca1084f0 --- /dev/null +++ b/examples/rpc_client_side.py @@ -0,0 +1,53 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2011 Dann Martens + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from sleekxmpp.plugins.xep_0009.remote import Endpoint, remote, Remote, \ + ANY_ALL +import threading +import time + +class Thermostat(Endpoint): + + def FQN(self): + return 'thermostat' + + def __init(self, initial_temperature): + self._temperature = initial_temperature + self._event = threading.Event() + + @remote + def set_temperature(self, temperature): + return NotImplemented + + @remote + def get_temperature(self): + return NotImplemented + + @remote(False) + def release(self): + return NotImplemented + + + +def main(): + + session = Remote.new_session('operator@xmpp.org/rpc', '*****') + + thermostat = session.new_proxy('thermostat@xmpp.org/rpc', Thermostat) + + print("Current temperature is %s" % thermostat.get_temperature()) + + thermostat.set_temperature(20) + + time.sleep(10) + + session.close() + +if __name__ == '__main__': + main() +
\ No newline at end of file diff --git a/examples/rpc_server_side.py b/examples/rpc_server_side.py new file mode 100644 index 00000000..0af8af43 --- /dev/null +++ b/examples/rpc_server_side.py @@ -0,0 +1,52 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2011 Dann Martens + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from sleekxmpp.plugins.xep_0009.remote import Endpoint, remote, Remote, \ + ANY_ALL +import threading + +class Thermostat(Endpoint): + + def FQN(self): + return 'thermostat' + + def __init(self, initial_temperature): + self._temperature = initial_temperature + self._event = threading.Event() + + @remote + def set_temperature(self, temperature): + print("Setting temperature to %s" % temperature) + self._temperature = temperature + + @remote + def get_temperature(self): + return self._temperature + + @remote(False) + def release(self): + self._event.set() + + def wait_for_release(self): + self._event.wait() + + + +def main(): + + session = Remote.new_session('sleek@xmpp.org/rpc', '*****') + + thermostat = session.new_handler(ANY_ALL, Thermostat, 18) + + thermostat.wait_for_release() + + session.close() + +if __name__ == '__main__': + main() +
\ No newline at end of file @@ -45,6 +45,8 @@ packages = [ 'sleekxmpp', 'sleekxmpp/xmlstream/handler',
'sleekxmpp/thirdparty',
'sleekxmpp/plugins',
+ 'sleekxmpp/plugins/xep_0009',
+ 'sleekxmpp/plugins/xep_0009/stanza',
'sleekxmpp/plugins/xep_0030',
'sleekxmpp/plugins/xep_0030/stanza',
'sleekxmpp/plugins/xep_0059',
diff --git a/sleekxmpp/plugins/xep_0009.py b/sleekxmpp/plugins/old_0009.py index 625b03fb..625b03fb 100644 --- a/sleekxmpp/plugins/xep_0009.py +++ b/sleekxmpp/plugins/old_0009.py diff --git a/sleekxmpp/plugins/xep_0009/__init__.py b/sleekxmpp/plugins/xep_0009/__init__.py new file mode 100644 index 00000000..2cd14170 --- /dev/null +++ b/sleekxmpp/plugins/xep_0009/__init__.py @@ -0,0 +1,11 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2011 Nathanael C. Fritz, Dann Martens (TOMOTON). + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from sleekxmpp.plugins.xep_0009 import stanza +from sleekxmpp.plugins.xep_0009.rpc import xep_0009 +from sleekxmpp.plugins.xep_0009.stanza import RPCQuery, MethodCall, MethodResponse diff --git a/sleekxmpp/plugins/xep_0009/binding.py b/sleekxmpp/plugins/xep_0009/binding.py new file mode 100644 index 00000000..61ef1469 --- /dev/null +++ b/sleekxmpp/plugins/xep_0009/binding.py @@ -0,0 +1,168 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2011 Nathanael C. Fritz, Dann Martens (TOMOTON). + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from xml.etree import cElementTree as ET +import base64 +import logging +import time + + + +log = logging.getLogger(__name__) + +_namespace = 'jabber:iq:rpc' + +def fault2xml(fault): + value = dict() + value['faultCode'] = fault['code'] + value['faultString'] = fault['string'] + fault = ET.Element("fault", {'xmlns': _namespace}) + fault.append(_py2xml((value))) + return fault + +def xml2fault(params): + vals = [] + for value in params.findall('{%s}value' % _namespace): + vals.append(_xml2py(value)) + fault = dict() + fault['code'] = vals[0]['faultCode'] + fault['string'] = vals[0]['faultString'] + return fault + +def py2xml(*args): + params = ET.Element("{%s}params" % _namespace) + for x in args: + param = ET.Element("param") + param.append(_py2xml(x)) + params.append(param) #<params><param>... + return params + +def _py2xml(*args): + for x in args: + val = ET.Element("value") + if x is None: + nil = ET.Element("nil") + val.append(nil) + elif type(x) is int: + i4 = ET.Element("i4") + i4.text = str(x) + val.append(i4) + elif type(x) is bool: + boolean = ET.Element("boolean") + boolean.text = str(int(x)) + val.append(boolean) + elif type(x) is str: + string = ET.Element("string") + string.text = x + val.append(string) + elif type(x) is float: + double = ET.Element("double") + double.text = str(x) + val.append(double) + elif type(x) is rpcbase64: + b64 = ET.Element("Base64") + b64.text = x.encoded() + val.append(b64) + elif type(x) is rpctime: + iso = ET.Element("dateTime.iso8601") + iso.text = str(x) + val.append(iso) + elif type(x) in (list, tuple): + array = ET.Element("array") + data = ET.Element("data") + for y in x: + data.append(_py2xml(y)) + array.append(data) + val.append(array) + elif type(x) is dict: + struct = ET.Element("struct") + for y in x.keys(): + member = ET.Element("member") + name = ET.Element("name") + name.text = y + member.append(name) + member.append(_py2xml(x[y])) + struct.append(member) + val.append(struct) + return val + +def xml2py(params): + namespace = 'jabber:iq:rpc' + vals = [] + for param in params.findall('{%s}param' % namespace): + vals.append(_xml2py(param.find('{%s}value' % namespace))) + return vals + +def _xml2py(value): + namespace = 'jabber:iq:rpc' + if value.find('{%s}nil' % namespace) is not None: + return None + if value.find('{%s}i4' % namespace) is not None: + return int(value.find('{%s}i4' % namespace).text) + if value.find('{%s}int' % namespace) is not None: + return int(value.find('{%s}int' % namespace).text) + if value.find('{%s}boolean' % namespace) is not None: + return bool(value.find('{%s}boolean' % namespace).text) + if value.find('{%s}string' % namespace) is not None: + return value.find('{%s}string' % namespace).text + if value.find('{%s}double' % namespace) is not None: + return float(value.find('{%s}double' % namespace).text) + if value.find('{%s}Base64') is not None: + return rpcbase64(value.find('Base64' % namespace).text) + if value.find('{%s}dateTime.iso8601') is not None: + return rpctime(value.find('{%s}dateTime.iso8601')) + if value.find('{%s}struct' % namespace) is not None: + struct = {} + for member in value.find('{%s}struct' % namespace).findall('{%s}member' % namespace): + struct[member.find('{%s}name' % namespace).text] = _xml2py(member.find('{%s}value' % namespace)) + return struct + if value.find('{%s}array' % namespace) is not None: + array = [] + for val in value.find('{%s}array' % namespace).find('{%s}data' % namespace).findall('{%s}value' % namespace): + array.append(_xml2py(val)) + return array + raise ValueError() + + + +class rpcbase64(object): + + def __init__(self, data): + #base 64 encoded string + self.data = data + + def decode(self): + return base64.decodestring(self.data) + + def __str__(self): + return self.decode() + + def encoded(self): + return self.data + + + +class rpctime(object): + + def __init__(self,data=None): + #assume string data is in iso format YYYYMMDDTHH:MM:SS + if type(data) is str: + self.timestamp = time.strptime(data,"%Y%m%dT%H:%M:%S") + elif type(data) is time.struct_time: + self.timestamp = data + elif data is None: + self.timestamp = time.gmtime() + else: + raise ValueError() + + def iso8601(self): + #return a iso8601 string + return time.strftime("%Y%m%dT%H:%M:%S",self.timestamp) + + def __str__(self): + return self.iso8601() diff --git a/sleekxmpp/plugins/xep_0009/remote.py b/sleekxmpp/plugins/xep_0009/remote.py new file mode 100644 index 00000000..bd931c0c --- /dev/null +++ b/sleekxmpp/plugins/xep_0009/remote.py @@ -0,0 +1,752 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2011 Nathanael C. Fritz, Dann Martens (TOMOTON). + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from binding import py2xml, xml2py, xml2fault, fault2xml +from threading import RLock +import abc +import inspect +import logging +import sleekxmpp +import sys +import threading +import traceback + + + +log = logging.getLogger(__name__) + + + +def _intercept(method, name, public): + def _resolver(instance, *args, **kwargs): + log.debug("Locally calling %s.%s with arguments %s." % (instance.FQN(), method.__name__, args)) + try: + value = method(instance, *args, **kwargs) + if value == NotImplemented: + raise InvocationException("Local handler does not implement %s.%s!" % (instance.FQN(), method.__name__)) + return value + except InvocationException: + raise + except Exception as e: + raise InvocationException("A problem occured calling %s.%s!" % (instance.FQN(), method.__name__), e) + _resolver._rpc = public + _resolver._rpc_name = method.__name__ if name is None else name + return _resolver + +def remote(function_argument, public = True): + ''' + Decorator for methods which are remotely callable. This decorator + works in conjunction with classes which extend ABC Endpoint. + Example: + + @remote + def remote_method(arg1, arg2) + + Arguments: + function_argument -- a stand-in for either the actual method + OR a new name (string) for the method. In that case the + method is considered mapped: + Example: + + @remote("new_name") + def remote_method(arg1, arg2) + + public -- A flag which indicates if this method should be part + of the known dictionary of remote methods. Defaults to True. + Example: + + @remote(False) + def remote_method(arg1, arg2) + + Note: renaming and revising (public vs. private) can be combined. + Example: + + @remote("new_name", False) + def remote_method(arg1, arg2) + ''' + if hasattr(function_argument, '__call__'): + return _intercept(function_argument, None, public) + else: + if not isinstance(function_argument, basestring): + if not isinstance(function_argument, bool): + raise Exception('Expected an RPC method name or visibility modifier!') + else: + def _wrap_revised(function): + function = _intercept(function, None, function_argument) + return function + return _wrap_revised + def _wrap_remapped(function): + function = _intercept(function, function_argument, public) + return function + return _wrap_remapped + + + +class ACL: + ''' + An Access Control List (ACL) is a list of rules, which are evaluated + in order until a match is found. The policy of the matching rule + is then applied. + + Rules are 3-tuples, consisting of a policy enumerated type, a JID + expression and a RCP resource expression. + + Examples: + [ (ACL.ALLOW, '*', '*') ] allow everyone everything, no restrictions + [ (ACL.DENY, '*', '*') ] deny everyone everything, no restrictions + [ (ACL.ALLOW, 'test@xmpp.org/unit', 'test.*'), + (ACL.DENY, '*', '*') ] deny everyone everything, except named + JID, which is allowed access to endpoint 'test' only. + + The use of wildcards is allowed in expressions, as follows: + '*' everyone, or everything (= all endpoints and methods) + 'test@xmpp.org/*' every JID regardless of JID resource + '*@xmpp.org/rpc' every JID from domain xmpp.org with JID res 'rpc' + 'frank@*' every 'frank', regardless of domain or JID res + 'system.*' all methods of endpoint 'system' + '*.reboot' all methods reboot regardless of endpoint + ''' + ALLOW = True + DENY = False + + @classmethod + def check(cls, rules, jid, resource): + if rules is None: + return cls.DENY # No rules means no access! + for rule in rules: + policy = cls._check(rule, jid, resource) + if policy is not None: + return policy + return cls.DENY # By default if not rule matches, deny access. + + @classmethod + def _check(cls, rule, jid, resource): + if cls._match(jid, rule[1]) and cls._match(resource, rule[2]): + return rule[0] + else: + return None + + @classmethod + def _next_token(cls, expression, index): + new_index = expression.find('*', index) + if new_index == 0: + return '' + else: + if new_index == -1: + return expression[index : ] + else: + return expression[index : new_index] + + @classmethod + def _match(cls, value, expression): + #! print "_match [VALUE] %s [EXPR] %s" % (value, expression) + index = 0 + position = 0 + while index < len(expression): + token = cls._next_token(expression, index) + #! print "[TOKEN] '%s'" % token + size = len(token) + if size > 0: + token_index = value.find(token, position) + if token_index == -1: + return False + else: + #! print "[INDEX-OF] %s" % token_index + position = token_index + len(token) + pass + if size == 0: + index += 1 + else: + index += size + #! print "index %s position %s" % (index, position) + return True + +ANY_ALL = [ (ACL.ALLOW, '*', '*') ] + + + +class RemoteException(Exception): + ''' + Base exception for RPC. This exception is raised when a problem + occurs in the network layer. + ''' + + def __init__(self, message="", cause=None): + ''' + Initializes a new RemoteException. + + Arguments: + message -- The message accompanying this exception. + cause -- The underlying cause of this exception. + ''' + self._message = message + self._cause = cause + pass + + def __str__(self): + return repr(self._message) + + def get_message(self): + return self._message + + def get_cause(self): + return self._cause + + + +class InvocationException(RemoteException): + ''' + Exception raised when a problem occurs during the remote invocation + of a method. + ''' + pass + + + +class AuthorizationException(RemoteException): + ''' + Exception raised when the caller is not authorized to invoke the + remote method. + ''' + pass + + + +class TimeoutException(Exception): + ''' + Exception raised when the synchronous execution of a method takes + longer than the given threshold because an underlying asynchronous + reply did not arrive in time. + ''' + pass + + + +class Callback(object): + ''' + A base class for callback handlers. + ''' + __metaclass__ = abc.ABCMeta + + + @abc.abstractproperty + def set_value(self, value): + return NotImplemented + + @abc.abstractproperty + def cancel_with_error(self, exception): + return NotImplemented + + + +class Future(Callback): + ''' + Represents the result of an asynchronous computation. + ''' + + def __init__(self): + ''' + Initializes a new Future. + ''' + self._value = None + self._exception = None + self._event = threading.Event() + pass + + def set_value(self, value): + ''' + Sets the value of this Future. Once the value is set, a caller + blocked on get_value will be able to continue. + ''' + self._value = value + self._event.set() + + def get_value(self, timeout=None): + ''' + Gets the value of this Future. This call will block until + the result is available, or until an optional timeout expires. + When this Future is cancelled with an error, + + Arguments: + timeout -- The maximum waiting time to obtain the value. + ''' + self._event.wait(timeout) + if self._exception: + raise self._exception + if not self._event.is_set(): + raise TimeoutException + return self._value + + def is_done(self): + ''' + Returns true if a value has been returned. + ''' + return self._event.is_set() + + def cancel_with_error(self, exception): + ''' + Cancels the Future because of an error. Once cancelled, a + caller blocked on get_value will be able to continue. + ''' + self._exception = exception + self._event.set() + + + +class Endpoint(object): + ''' + The Endpoint class is an abstract base class for all objects + participating in an RPC-enabled XMPP network. + + A user subclassing this class is required to implement the method: + FQN(self) + where FQN stands for Fully Qualified Name, an unambiguous name + which specifies which object an RPC call refers to. It is the + first part in a RPC method name '<fqn>.<method>'. + ''' + __metaclass__ = abc.ABCMeta + + + def __init__(self, session, target_jid): + ''' + Initialize a new Endpoint. This constructor should never be + invoked by a user, instead it will be called by the factories + which instantiate the RPC-enabled objects, of which only + the classes are provided by the user. + + Arguments: + session -- An RPC session instance. + target_jid -- the identity of the remote XMPP entity. + ''' + self.session = session + self.target_jid = target_jid + + @abc.abstractproperty + def FQN(self): + return NotImplemented + + def get_methods(self): + ''' + Returns a dictionary of all RPC method names provided by this + class. This method returns the actual method names as found + in the class definition which have been decorated with: + + @remote + def some_rpc_method(arg1, arg2) + + + Unless: + (1) the name has been remapped, in which case the new + name will be returned. + + @remote("new_name") + def some_rpc_method(arg1, arg2) + + (2) the method is set to hidden + + @remote(False) + def some_hidden_method(arg1, arg2) + ''' + result = dict() + for function in dir(self): + test_attr = getattr(self, function, None) + try: + if test_attr._rpc: + result[test_attr._rpc_name] = test_attr + except Exception: + pass + return result + + + +class Proxy(Endpoint): + ''' + Implementation of the Proxy pattern which is intended to wrap + around Endpoints in order to intercept calls, marshall them and + forward them to the remote object. + ''' + + def __init__(self, endpoint, callback = None): + ''' + Initializes a new Proxy. + + Arguments: + endpoint -- The endpoint which is proxified. + ''' + self._endpoint = endpoint + self._callback = callback + + def __getattribute__(self, name, *args): + if name in ('__dict__', '_endpoint', 'async', '_callback'): + return object.__getattribute__(self, name) + else: + attribute = self._endpoint.__getattribute__(name) + if hasattr(attribute, '__call__'): + try: + if attribute._rpc: + def _remote_call(*args, **kwargs): + log.debug("Remotely calling '%s.%s' with arguments %s." % (self._endpoint.FQN(), attribute._rpc_name, args)) + return self._endpoint.session._call_remote(self._endpoint.target_jid, "%s.%s" % (self._endpoint.FQN(), attribute._rpc_name), self._callback, *args, **kwargs) + return _remote_call + except: + pass # If the attribute doesn't exist, don't care! + return attribute + + def async(self, callback): + return Proxy(self._endpoint, callback) + + def get_endpoint(self): + ''' + Returns the proxified endpoint. + ''' + return self._endpoint + + def FQN(self): + return self._endpoint.FQN() + + + +class JabberRPCEntry(object): + + + def __init__(self, endpoint_FQN, call): + self._endpoint_FQN = endpoint_FQN + self._call = call + + def call_method(self, args): + return_value = self._call(*args) + if return_value is None: + return return_value + else: + return self._return(return_value) + + def get_endpoint_FQN(self): + return self._endpoint_FQN + + def _return(self, *args): + return args + + + +class RemoteSession(object): + ''' + A context object for a Jabber-RPC session. + ''' + + + def __init__(self, client, session_close_callback): + ''' + Initializes a new RPC session. + + Arguments: + client -- The SleekXMPP client associated with this session. + session_close_callback -- A callback called when the + session is closed. + ''' + self._client = client + self._session_close_callback = session_close_callback + self._event = threading.Event() + self._entries = {} + self._callbacks = {} + self._acls = {} + self._lock = RLock() + + def _wait(self): + self._event.wait() + + def _notify(self, event): + log.debug("RPC Session as %s started." % self._client.boundjid.full) + self._client.sendPresence() + self._event.set() + pass + + def _register_call(self, endpoint, method, name=None): + ''' + Registers a method from an endpoint as remotely callable. + ''' + if name is None: + name = method.__name__ + key = "%s.%s" % (endpoint, name) + log.debug("Registering call handler for %s (%s)." % (key, method)) + with self._lock: + if self._entries.has_key(key): + raise KeyError("A handler for %s has already been regisered!" % endpoint) + self._entries[key] = JabberRPCEntry(endpoint, method) + return key + + def _register_acl(self, endpoint, acl): + log.debug("Registering ACL %s for endpoint %s." % (repr(acl), endpoint)) + with self._lock: + self._acls[endpoint] = acl + + def _register_callback(self, pid, callback): + with self._lock: + self._callbacks[pid] = callback + + def forget_callback(self, callback): + with self._lock: + pid = self._find_key(self._callbacks, callback) + if pid is not None: + del self._callback[pid] + else: + raise ValueError("Unknown callback!") + pass + + def _find_key(self, dict, value): + """return the key of dictionary dic given the value""" + search = [k for k, v in dict.iteritems() if v == value] + if len(search) == 0: + return None + else: + return search[0] + + def _unregister_call(self, key): + #removes the registered call + with self._lock: + if self._entries[key]: + del self._entries[key] + else: + raise ValueError() + + def new_proxy(self, target_jid, endpoint_cls): + ''' + Instantiates a new proxy object, which proxies to a remote + endpoint. This method uses a class reference without + constructor arguments to instantiate the proxy. + + Arguments: + target_jid -- the XMPP entity ID hosting the endpoint. + endpoint_cls -- The remote (duck) type. + ''' + try: + argspec = inspect.getargspec(endpoint_cls.__init__) + args = [None] * (len(argspec[0]) - 1) + result = endpoint_cls(*args) + Endpoint.__init__(result, self, target_jid) + return Proxy(result) + except: + traceback.print_exc(file=sys.stdout) + + def new_handler(self, acl, handler_cls, *args, **kwargs): + ''' + Instantiates a new handler object, which is called remotely + by others. The user can control the effect of the call by + implementing the remote method in the local endpoint class. The + returned reference can be called locally and will behave as a + regular instance. + + Arguments: + acl -- Access control list (see ACL class) + handler_clss -- The local (duck) type. + *args -- Constructor arguments for the local type. + **kwargs -- Constructor keyworded arguments for the local + type. + ''' + argspec = inspect.getargspec(handler_cls.__init__) + base_argspec = inspect.getargspec(Endpoint.__init__) + if(argspec == base_argspec): + result = handler_cls(self, self._client.boundjid.full) + else: + result = handler_cls(*args, **kwargs) + Endpoint.__init__(result, self, self._client.boundjid.full) + method_dict = result.get_methods() + for method_name, method in method_dict.iteritems(): + #!!! self._client.plugin['xep_0009'].register_call(result.FQN(), method, method_name) + self._register_call(result.FQN(), method, method_name) + self._register_acl(result.FQN(), acl) + return result + +# def is_available(self, targetCls, pto): +# return self._client.is_available(pto) + + def _call_remote(self, pto, pmethod, callback, *arguments): + iq = self._client.plugin['xep_0009'].make_iq_method_call(pto, pmethod, py2xml(*arguments)) + pid = iq['id'] + if callback is None: + future = Future() + self._register_callback(pid, future) + iq.send() + return future.get_value(30) + else: + print "[RemoteSession] _call_remote %s" % callback + self._register_callback(pid, callback) + iq.send() + + def close(self): + ''' + Closes this session. + ''' + self._client.disconnect(False) + self._session_close_callback() + + def _on_jabber_rpc_method_call(self, iq): + iq.enable('rpc_query') + params = iq['rpc_query']['method_call']['params'] + args = xml2py(params) + pmethod = iq['rpc_query']['method_call']['method_name'] + try: + with self._lock: + entry = self._entries[pmethod] + rules = self._acls[entry.get_endpoint_FQN()] + if ACL.check(rules, iq['from'], pmethod): + return_value = entry.call_method(args) + else: + raise AuthorizationException("Unauthorized access to %s from %s!" % (pmethod, iq['from'])) + if return_value is None: + return_value = () + response = self._client.plugin['xep_0009'].make_iq_method_response(iq['id'], iq['from'], py2xml(*return_value)) + response.send() + except InvocationException as ie: + fault = dict() + fault['code'] = 500 + fault['string'] = ie.get_message() + self._client.plugin['xep_0009']._send_fault(iq, fault2xml(fault)) + except AuthorizationException as ae: + log.error(ae.get_message()) + error = self._client.plugin['xep_0009']._forbidden(iq) + error.send() + except Exception as e: + if isinstance(e, KeyError): + log.error("No handler available for %s!" % pmethod) + error = self._client.plugin['xep_0009']._item_not_found(iq) + else: + traceback.print_exc(file=sys.stderr) + log.error("An unexpected problem occurred invoking method %s!" % pmethod) + error = self._client.plugin['xep_0009']._undefined_condition(iq) + #! print "[REMOTE.PY] _handle_remote_procedure_call AN ERROR SHOULD BE SENT NOW %s " % e + error.send() + + def _on_jabber_rpc_method_response(self, iq): + iq.enable('rpc_query') + args = xml2py(iq['rpc_query']['method_response']['params']) + pid = iq['id'] + with self._lock: + callback = self._callbacks[pid] + del self._callbacks[pid] + if(len(args) > 0): + callback.set_value(args[0]) + else: + callback.set_value(None) + pass + + def _on_jabber_rpc_method_response2(self, iq): + iq.enable('rpc_query') + if iq['rpc_query']['method_response']['fault'] is not None: + self._on_jabber_rpc_method_fault(iq) + else: + args = xml2py(iq['rpc_query']['method_response']['params']) + pid = iq['id'] + with self._lock: + callback = self._callbacks[pid] + del self._callbacks[pid] + if(len(args) > 0): + callback.set_value(args[0]) + else: + callback.set_value(None) + pass + + def _on_jabber_rpc_method_fault(self, iq): + iq.enable('rpc_query') + fault = xml2fault(iq['rpc_query']['method_response']['fault']) + pid = iq['id'] + with self._lock: + callback = self._callbacks[pid] + del self._callbacks[pid] + e = { + 500: InvocationException + }[fault['code']](fault['string']) + callback.cancel_with_error(e) + + def _on_jabber_rpc_error(self, iq): + pid = iq['id'] + pmethod = self._client.plugin['xep_0009']._extract_method(iq['rpc_query']) + code = iq['error']['code'] + type = iq['error']['type'] + condition = iq['error']['condition'] + #! print("['REMOTE.PY']._BINDING_handle_remote_procedure_error -> ERROR! ERROR! ERROR! Condition is '%s'" % condition) + with self._lock: + callback = self._callbacks[pid] + del self._callbacks[pid] + e = { + 'item-not-found': RemoteException("No remote handler available for %s at %s!" % (pmethod, iq['from'])), + 'forbidden': AuthorizationException("Forbidden to invoke remote handler for %s at %s!" % (pmethod, iq['from'])), + 'undefined-condition': RemoteException("An unexpected problem occured trying to invoke %s at %s!" % (pmethod, iq['from'])), + }[condition] + if e is None: + RemoteException("An unexpected exception occurred at %s!" % iq['from']) + callback.cancel_with_error(e) + + + +class Remote(object): + ''' + Bootstrap class for Jabber-RPC sessions. New sessions are openend + with an existing XMPP client, or one is instantiated on demand. + ''' + _instance = None + _sessions = dict() + _lock = threading.RLock() + + @classmethod + def new_session_with_client(cls, client, callback=None): + ''' + Opens a new session with a given client. + + Arguments: + client -- An XMPP client. + callback -- An optional callback which can be used to track + the starting state of the session. + ''' + with Remote._lock: + if(client.boundjid.bare in cls._sessions): + raise RemoteException("There already is a session associated with these credentials!") + else: + cls._sessions[client.boundjid.bare] = client; + def _session_close_callback(): + with Remote._lock: + del cls._sessions[client.boundjid.bare] + result = RemoteSession(client, _session_close_callback) + client.plugin['xep_0009'].xmpp.add_event_handler('jabber_rpc_method_call', result._on_jabber_rpc_method_call) + client.plugin['xep_0009'].xmpp.add_event_handler('jabber_rpc_method_response', result._on_jabber_rpc_method_response) + client.plugin['xep_0009'].xmpp.add_event_handler('jabber_rpc_method_fault', result._on_jabber_rpc_method_fault) + client.plugin['xep_0009'].xmpp.add_event_handler('jabber_rpc_error', result._on_jabber_rpc_error) + if callback is None: + start_event_handler = result._notify + else: + start_event_handler = callback + client.add_event_handler("session_start", start_event_handler) + if client.connect(): + client.process(threaded=True) + else: + raise RemoteException("Could not connect to XMPP server!") + pass + if callback is None: + result._wait() + return result + + @classmethod + def new_session(cls, jid, password, callback=None): + ''' + Opens a new session and instantiates a new XMPP client. + + Arguments: + jid -- The XMPP JID for logging in. + password -- The password for logging in. + callback -- An optional callback which can be used to track + the starting state of the session. + ''' + client = sleekxmpp.ClientXMPP(jid, password) + #? Register plug-ins. + client.registerPlugin('xep_0004') # Data Forms + client.registerPlugin('xep_0009') # Jabber-RPC + client.registerPlugin('xep_0030') # Service Discovery + client.registerPlugin('xep_0060') # PubSub + client.registerPlugin('xep_0199') # XMPP Ping + return cls.new_session_with_client(client, callback) + +
\ No newline at end of file diff --git a/sleekxmpp/plugins/xep_0009/rpc.py b/sleekxmpp/plugins/xep_0009/rpc.py new file mode 100644 index 00000000..84afeb5f --- /dev/null +++ b/sleekxmpp/plugins/xep_0009/rpc.py @@ -0,0 +1,221 @@ +"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2011 Nathanael C. Fritz, Dann Martens (TOMOTON).
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+from .. import base
+from stanza.RPC import RPCQuery, MethodCall, MethodResponse
+from sleekxmpp.stanza.iq import Iq
+from sleekxmpp.xmlstream.handler.callback import Callback
+from sleekxmpp.xmlstream.matcher.xpath import MatchXPath
+from sleekxmpp.xmlstream.stanzabase import register_stanza_plugin
+from xml.etree import cElementTree as ET
+import logging
+
+
+
+log = logging.getLogger(__name__)
+
+
+
+class xep_0009(base.base_plugin):
+
+ def plugin_init(self):
+ self.xep = '0009'
+ self.description = 'Jabber-RPC'
+ #self.stanza = sleekxmpp.plugins.xep_0009.stanza
+
+ register_stanza_plugin(Iq, RPCQuery)
+ register_stanza_plugin(RPCQuery, MethodCall)
+ register_stanza_plugin(RPCQuery, MethodResponse)
+
+ self.xmpp.registerHandler(
+ Callback('RPC Call', MatchXPath('{%s}iq/{%s}query/{%s}methodCall' % (self.xmpp.default_ns, RPCQuery.namespace, RPCQuery.namespace)),
+ self._handle_method_call)
+ )
+ self.xmpp.registerHandler(
+ Callback('RPC Call', MatchXPath('{%s}iq/{%s}query/{%s}methodResponse' % (self.xmpp.default_ns, RPCQuery.namespace, RPCQuery.namespace)),
+ self._handle_method_response)
+ )
+ self.xmpp.registerHandler(
+ Callback('RPC Call', MatchXPath('{%s}iq/{%s}error' % (self.xmpp.default_ns, self.xmpp.default_ns)),
+ self._handle_error)
+ )
+ self.xmpp.add_event_handler('jabber_rpc_method_call', self._on_jabber_rpc_method_call)
+ self.xmpp.add_event_handler('jabber_rpc_method_response', self._on_jabber_rpc_method_response)
+ self.xmpp.add_event_handler('jabber_rpc_method_fault', self._on_jabber_rpc_method_fault)
+ self.xmpp.add_event_handler('jabber_rpc_error', self._on_jabber_rpc_error)
+ self.xmpp.add_event_handler('error', self._handle_error)
+ #self.activeCalls = []
+
+ def post_init(self):
+ base.base_plugin.post_init(self)
+ self.xmpp.plugin['xep_0030'].add_feature('jabber:iq:rpc')
+ self.xmpp.plugin['xep_0030'].add_identity('automation','rpc')
+
+ def make_iq_method_call(self, pto, pmethod, params):
+ iq = self.xmpp.makeIqSet()
+ iq.attrib['to'] = pto
+ iq.attrib['from'] = self.xmpp.boundjid.full
+ iq.enable('rpc_query')
+ iq['rpc_query']['method_call']['method_name'] = pmethod
+ iq['rpc_query']['method_call']['params'] = params
+ return iq;
+
+ def make_iq_method_response(self, pid, pto, params):
+ iq = self.xmpp.makeIqResult(pid)
+ iq.attrib['to'] = pto
+ iq.attrib['from'] = self.xmpp.boundjid.full
+ iq.enable('rpc_query')
+ iq['rpc_query']['method_response']['params'] = params
+ return iq
+
+ def make_iq_method_response_fault(self, pid, pto, params):
+ iq = self.xmpp.makeIqResult(pid)
+ iq.attrib['to'] = pto
+ iq.attrib['from'] = self.xmpp.boundjid.full
+ iq.enable('rpc_query')
+ iq['rpc_query']['method_response']['params'] = None
+ iq['rpc_query']['method_response']['fault'] = params
+ return iq
+
+# def make_iq_method_error(self, pto, pid, pmethod, params, code, type, condition):
+# iq = self.xmpp.makeIqError(pid)
+# iq.attrib['to'] = pto
+# iq.attrib['from'] = self.xmpp.boundjid.full
+# iq['error']['code'] = code
+# iq['error']['type'] = type
+# iq['error']['condition'] = condition
+# iq['rpc_query']['method_call']['method_name'] = pmethod
+# iq['rpc_query']['method_call']['params'] = params
+# return iq
+
+ def _item_not_found(self, iq):
+ payload = iq.get_payload()
+ iq.reply().error().set_payload(payload);
+ iq['error']['code'] = '404'
+ iq['error']['type'] = 'cancel'
+ iq['error']['condition'] = 'item-not-found'
+ return iq
+
+ def _undefined_condition(self, iq):
+ payload = iq.get_payload()
+ iq.reply().error().set_payload(payload)
+ iq['error']['code'] = '500'
+ iq['error']['type'] = 'cancel'
+ iq['error']['condition'] = 'undefined-condition'
+ return iq
+
+ def _forbidden(self, iq):
+ payload = iq.get_payload()
+ iq.reply().error().set_payload(payload)
+ iq['error']['code'] = '403'
+ iq['error']['type'] = 'auth'
+ iq['error']['condition'] = 'forbidden'
+ return iq
+
+ def _recipient_unvailable(self, iq):
+ payload = iq.get_payload()
+ iq.reply().error().set_payload(payload)
+ iq['error']['code'] = '404'
+ iq['error']['type'] = 'wait'
+ iq['error']['condition'] = 'recipient-unavailable'
+ return iq
+
+ def _handle_method_call(self, iq):
+ type = iq['type']
+ if type == 'set':
+ log.debug("Incoming Jabber-RPC call from %s" % iq['from'])
+ self.xmpp.event('jabber_rpc_method_call', iq)
+ else:
+ if type == 'error' and ['rpc_query'] is None:
+ self.handle_error(iq)
+ else:
+ log.debug("Incoming Jabber-RPC error from %s" % iq['from'])
+ self.xmpp.event('jabber_rpc_error', iq)
+
+ def _handle_method_response(self, iq):
+ if iq['rpc_query']['method_response']['fault'] is not None:
+ log.debug("Incoming Jabber-RPC fault from %s" % iq['from'])
+ #self._on_jabber_rpc_method_fault(iq)
+ self.xmpp.event('jabber_rpc_method_fault', iq)
+ else:
+ log.debug("Incoming Jabber-RPC response from %s" % iq['from'])
+ self.xmpp.event('jabber_rpc_method_response', iq)
+
+ def _handle_error(self, iq):
+ print("['XEP-0009']._handle_error -> ERROR! Iq is '%s'" % iq)
+ print("#######################")
+ print("### NOT IMPLEMENTED ###")
+ print("#######################")
+
+ def _on_jabber_rpc_method_call(self, iq, forwarded=False):
+ """
+ A default handler for Jabber-RPC method call. If another
+ handler is registered, this one will defer and not run.
+
+ If this handler is called by your own custom handler with
+ forwarded set to True, then it will run as normal.
+ """
+ if not forwarded and self.xmpp.event_handled('jabber_rpc_method_call') > 1:
+ return
+ # Reply with error by default
+ error = self.client.plugin['xep_0009']._item_not_found(iq)
+ error.send()
+
+ def _on_jabber_rpc_method_response(self, iq, forwarded=False):
+ """
+ A default handler for Jabber-RPC method response. If another
+ handler is registered, this one will defer and not run.
+
+ If this handler is called by your own custom handler with
+ forwarded set to True, then it will run as normal.
+ """
+ if not forwarded and self.xmpp.event_handled('jabber_rpc_method_response') > 1:
+ return
+ error = self.client.plugin['xep_0009']._recpient_unavailable(iq)
+ error.send()
+
+ def _on_jabber_rpc_method_fault(self, iq, forwarded=False):
+ """
+ A default handler for Jabber-RPC fault response. If another
+ handler is registered, this one will defer and not run.
+
+ If this handler is called by your own custom handler with
+ forwarded set to True, then it will run as normal.
+ """
+ if not forwarded and self.xmpp.event_handled('jabber_rpc_method_fault') > 1:
+ return
+ error = self.client.plugin['xep_0009']._recpient_unavailable(iq)
+ error.send()
+
+ def _on_jabber_rpc_error(self, iq, forwarded=False):
+ """
+ A default handler for Jabber-RPC error response. If another
+ handler is registered, this one will defer and not run.
+
+ If this handler is called by your own custom handler with
+ forwarded set to True, then it will run as normal.
+ """
+ if not forwarded and self.xmpp.event_handled('jabber_rpc_error') > 1:
+ return
+ error = self.client.plugin['xep_0009']._recpient_unavailable(iq, iq.get_payload())
+ error.send()
+
+ def _send_fault(self, iq, fault_xml): #
+ fault = self.make_iq_method_response_fault(iq['id'], iq['from'], fault_xml)
+ fault.send()
+
+ def _send_error(self, iq):
+ print("['XEP-0009']._send_error -> ERROR! Iq is '%s'" % iq)
+ print("#######################")
+ print("### NOT IMPLEMENTED ###")
+ print("#######################")
+
+ def _extract_method(self, stanza):
+ xml = ET.fromstring("%s" % stanza)
+ return xml.find("./methodCall/methodName").text
+
\ No newline at end of file diff --git a/sleekxmpp/plugins/xep_0009/stanza/RPC.py b/sleekxmpp/plugins/xep_0009/stanza/RPC.py new file mode 100644 index 00000000..24f2efd8 --- /dev/null +++ b/sleekxmpp/plugins/xep_0009/stanza/RPC.py @@ -0,0 +1,68 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2011 Nathanael C. Fritz, Dann Martens (TOMOTON). + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from sleekxmpp.xmlstream.stanzabase import ElementBase +from xml.etree import cElementTree as ET + + +class RPCQuery(ElementBase): + name = 'query' + namespace = 'jabber:iq:rpc' + plugin_attrib = 'rpc_query' + interfaces = set(()) + subinterfaces = set(()) + plugin_attrib_map = {} + plugin_tag_map = {} + + + +class MethodCall(ElementBase): + name = 'methodCall' + namespace = 'jabber:iq:rpc' + plugin_attrib = 'method_call' + interfaces = set(('method_name', 'params')) + subinterfaces = set(()) + plugin_attrib_map = {} + plugin_tag_map = {} + + + def get_method_name(self): + return self._get_sub_text('methodName') + + def set_method_name(self, value): + return self._set_sub_text('methodName', value) + + def get_params(self): + return self.xml.find('{%s}params' % self.namespace) + + def set_params(self, params): + self.append(params) + + + +class MethodResponse(ElementBase): + name = 'methodResponse' + namespace = 'jabber:iq:rpc' + plugin_attrib = 'method_response' + interfaces = set(('params', 'fault')) + subinterfaces = set(()) + plugin_attrib_map = {} + plugin_tag_map = {} + + + def get_params(self): + return self.xml.find('{%s}params' % self.namespace) + + def set_params(self, params): + self.append(params) + + def get_fault(self): + return self.xml.find('{%s}fault' % self.namespace) + + def set_fault(self, fault): + self.append(fault) diff --git a/sleekxmpp/plugins/xep_0009/stanza/__init__.py b/sleekxmpp/plugins/xep_0009/stanza/__init__.py new file mode 100644 index 00000000..0b902238 --- /dev/null +++ b/sleekxmpp/plugins/xep_0009/stanza/__init__.py @@ -0,0 +1,9 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2011 Nathanael C. Fritz, Dann Martens (TOMOTON). + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from RPC import RPCQuery, MethodCall, MethodResponse diff --git a/tests/test_stanza_xep_0009.py b/tests/test_stanza_xep_0009.py new file mode 100644 index 00000000..f6ab7ed0 --- /dev/null +++ b/tests/test_stanza_xep_0009.py @@ -0,0 +1,55 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2011 Nathanael C. Fritz, Dann Martens (TOMOTON). + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from sleekxmpp.plugins.old_0009 import py2xml +from sleekxmpp.plugins.xep_0009.stanza.RPC import RPCQuery, MethodCall, \ + MethodResponse +from sleekxmpp.stanza.iq import Iq +from sleekxmpp.test.sleektest import SleekTest +from sleekxmpp.xmlstream.stanzabase import register_stanza_plugin +import unittest + + + +class TestJabberRPC(SleekTest): + + def setUp(self): + register_stanza_plugin(Iq, RPCQuery) + register_stanza_plugin(RPCQuery, MethodCall) + register_stanza_plugin(RPCQuery, MethodResponse) + + def testMethodCall(self): + iq = self.Iq() + iq['rpc_query']['method_call']['method_name'] = 'system.exit' + iq['rpc_query']['method_call']['params'] = py2xml(*()) + self.check(iq, """ + <iq> + <query xmlns="jabber:iq:rpc"> + <methodCall> + <methodName>system.exit</methodName> + <params /> + </methodCall> + </query> + </iq> + """, use_values=False) + + def testMethodResponse(self): + iq = self.Iq() + iq['rpc_query']['method_response']['params'] = py2xml(*()) + self.check(iq, """ + <iq> + <query xmlns="jabber:iq:rpc"> + <methodResponse> + <params /> + </methodResponse> + </query> + </iq> + """, use_values=False) + +suite = unittest.TestLoader().loadTestsFromTestCase(TestJabberRPC) +
\ No newline at end of file |