From 754ac5092a3a37819a71f6565a1e54b3f2547940 Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Thu, 30 Jun 2011 15:40:22 -0700 Subject: Reorganize features into plugins. --- sleekxmpp/basexmpp.py | 23 ++-- sleekxmpp/clientxmpp.py | 207 +++---------------------------- sleekxmpp/features/__init__.py | 10 ++ sleekxmpp/features/feature_bind.py | 55 ++++++++ sleekxmpp/features/feature_mechanisms.py | 116 +++++++++++++++++ sleekxmpp/features/feature_session.py | 46 +++++++ sleekxmpp/features/feature_starttls.py | 61 +++++++++ sleekxmpp/features/sasl_anonymous.py | 31 +++++ sleekxmpp/features/sasl_plain.py | 41 ++++++ sleekxmpp/plugins/base.py | 3 +- 10 files changed, 393 insertions(+), 200 deletions(-) create mode 100644 sleekxmpp/features/__init__.py create mode 100644 sleekxmpp/features/feature_bind.py create mode 100644 sleekxmpp/features/feature_mechanisms.py create mode 100644 sleekxmpp/features/feature_session.py create mode 100644 sleekxmpp/features/feature_starttls.py create mode 100644 sleekxmpp/features/sasl_anonymous.py create mode 100644 sleekxmpp/features/sasl_plain.py diff --git a/sleekxmpp/basexmpp.py b/sleekxmpp/basexmpp.py index 8e5c762a..43ad420f 100644 --- a/sleekxmpp/basexmpp.py +++ b/sleekxmpp/basexmpp.py @@ -165,9 +165,14 @@ class BaseXMPP(XMLStream): try: # Import the given module that contains the plugin. if not module: - module = sleekxmpp.plugins - module = __import__("%s.%s" % (module.__name__, plugin), - globals(), locals(), [plugin]) + try: + module = sleekxmpp.plugins + module = __import__("%s.%s" % (module.__name__, plugin), + globals(), locals(), [plugin]) + except ImportError: + module = sleekxmpp.features + module = __import__("%s.%s" % (module.__name__, plugin), + globals(), locals(), [plugin]) if isinstance(module, str): # We probably want to load a module from outside # the sleekxmpp package, so leave out the globals(). @@ -176,12 +181,14 @@ class BaseXMPP(XMLStream): # Load the plugin class from the module. self.plugin[plugin] = getattr(module, plugin)(self, pconfig) - # Let XEP implementing plugins have some extra logging info. - xep = '' - if hasattr(self.plugin[plugin], 'xep'): - xep = "(XEP-%s) " % self.plugin[plugin].xep + # Let XEP/RFC implementing plugins have some extra logging info. + spec = '(CUSTOM) ' + if self.plugin[plugin].xep: + spec = "(XEP-%s) " % self.plugin[plugin].xep + elif self.plugin[plugin].rfc: + spec = "(RFC-%s) " % self.plugin[plugin].rfc - desc = (xep, self.plugin[plugin].description) + desc = (spec, self.plugin[plugin].description) log.debug("Loaded Plugin %s%s" % desc) except: log.exception("Unable to load plugin: %s", plugin) diff --git a/sleekxmpp/clientxmpp.py b/sleekxmpp/clientxmpp.py index 5d7ca125..9c2696da 100644 --- a/sleekxmpp/clientxmpp.py +++ b/sleekxmpp/clientxmpp.py @@ -15,8 +15,10 @@ import hashlib import random import threading +import sleekxmpp from sleekxmpp import plugins from sleekxmpp import stanza +from sleekxmpp import features from sleekxmpp.basexmpp import BaseXMPP from sleekxmpp.stanza import * from sleekxmpp.stanza.stream import tls, sasl @@ -97,10 +99,6 @@ class ClientXMPP(BaseXMPP): self.add_event_handler('connected', self._handle_connected) self.register_stanza(StreamFeatures) - self.register_stanza(tls.Proceed) - self.register_stanza(sasl.Success) - self.register_stanza(sasl.Failure) - self.register_stanza(sasl.Auth) self.register_handler( Callback('Stream Features', @@ -112,43 +110,18 @@ class ClientXMPP(BaseXMPP): self.default_ns, 'jabber:iq:roster')), self._handle_roster)) - self.register_handler( - Callback('SASL Success', - MatchXPath(sasl.Success.tag_name()), - self._handle_sasl_success, - instream=True, - once=True)) - self.register_handler( - Callback('SASL Failure', - MatchXPath(sasl.Failure.tag_name()), - self._handle_sasl_fail, - instream=True, - once=True)) - self.register_handler( - Callback('STARTTLS Proceed', - MatchXPath(tls.Proceed.tag_name()), - self._handle_starttls_proceed, - instream=True)) - - self.register_feature('starttls', self._handle_starttls, - restart=True, - order=0) - self.register_feature('mechanisms', self._handle_sasl_auth, - restart=True, - order=100) - self.register_feature('bind', self._handle_bind_resource, - restart=False, - order=10000) - self.register_feature('session', self._handle_start_session, - restart=False, - order=10001) - - self.register_sasl_mechanism('PLAIN', - self._handle_sasl_plain, - priority=1) - self.register_sasl_mechanism('ANONYMOUS', - self._handle_sasl_anonymous, - priority=0) + + # Setup default stream features + self.register_plugin('feature_starttls') + self.register_plugin('feature_mechanisms') + self.register_plugin('feature_bind') + self.register_plugin('feature_session') + + # Setup default SASL mechanisms + self.register_plugin('sasl_plain', + {'priority': 1}) + self.register_plugin('sasl_anonymous', + {'priority': 0}) def connect(self, address=tuple(), reattempt=True, use_tls=True): """ @@ -242,9 +215,7 @@ class ClientXMPP(BaseXMPP): preferred ordering for the mechanism. High values will be attempted first. """ - self._sasl_mechanism_handlers[name] = handler - self._sasl_mechanism_priorities.append((priority, name)) - self._sasl_mechanism_priorities.sort(reverse=True) + self['feature_mechanisms'].register_mechanism(name, handler, priority) def remove_sasl_mechanism(self, name): """ @@ -253,11 +224,7 @@ class ClientXMPP(BaseXMPP): Arguments: name -- The name of the mechanism to remove (all caps) """ - if name in self._sasl_mechanism_handlers: - del self._sasl_mechanism_handlers[name] - - p = self._sasl_mechanism_priorities - self._sasl_mechanism_priorities = [i for i in p if i[1] != name] + self['feature_mechanisms'].remove_mechanism(name) def update_roster(self, jid, name=None, subscription=None, groups=[], block=True, timeout=None, callback=None): @@ -359,148 +326,6 @@ class ClientXMPP(BaseXMPP): # restarting the XML stream. return True - def _handle_starttls(self, features): - """ - Handle notification that the server supports TLS. - - Arguments: - features -- The stream:features element. - """ - if not self.use_tls: - return False - elif self.ssl_support: - self.send(features['starttls'], now=True) - return True - else: - log.warning("The module tlslite is required to log in" +\ - " to some servers, and has not been found.") - return False - - def _handle_starttls_proceed(self, proceed): - """Restart the XML stream when TLS is accepted.""" - log.debug("Starting TLS") - if self.start_tls(): - self.features.append('starttls') - raise RestartStream() - - def _handle_sasl_auth(self, features): - """ - Handle authenticating using SASL. - - Arguments: - features -- The stream features stanza. - """ - for priority, mech in self._sasl_mechanism_priorities: - if mech in features['mechanisms']: - handler = self._sasl_mechanism_handlers[mech] - if handler(self): - break - else: - log.error("No appropriate login method.") - self.event("no_auth", direct=True) - self.disconnect() - - return True - - def _handle_sasl_success(self, stanza): - """SASL authentication succeeded. Restart the stream.""" - self.authenticated = True - self.features.append('mechanisms') - raise RestartStream() - - def _handle_sasl_fail(self, stanza): - """SASL authentication failed. Disconnect and shutdown.""" - log.info("Authentication failed.") - self.event("failed_auth", direct=True) - self.disconnect() - log.debug("Starting SASL Auth") - return True - - def _handle_sasl_plain(self, xmpp): - """ - Attempt to authenticate using SASL PLAIN. - - Arguments: - xmpp -- The SleekXMPP connection instance. - """ - if not xmpp.boundjid.user: - return False - - if sys.version_info < (3, 0): - user = bytes(self.boundjid.user) - password = bytes(self.password) - else: - user = bytes(self.boundjid.user, 'utf-8') - password = bytes(self.password, 'utf-8') - - auth = base64.b64encode(b'\x00' + user + \ - b'\x00' + password).decode('utf-8') - - resp = sasl.Auth(xmpp) - resp['mechanism'] = 'PLAIN' - resp['value'] = auth - resp.send(now=True) - return True - - def _handle_sasl_anonymous(self, xmpp): - """ - Attempt to authenticate using SASL ANONYMOUS. - - Arguments: - xmpp -- The SleekXMPP connection instance. - """ - if xmpp.boundjid.user: - return False - - resp = sasl.Auth(xmpp) - resp['mechanism'] = 'ANONYMOUS' - resp.send() - - return True - - def _handle_bind_resource(self, features): - """ - Handle requesting a specific resource. - - Arguments: - features -- The stream features stanza. - """ - log.debug("Requesting resource: %s" % self.boundjid.resource) - iq = self.Iq() - iq['type'] = 'set' - iq.enable('bind') - if self.boundjid.resource: - iq['bind']['resource'] = self.boundjid.resource - response = iq.send(now=True) - - self.set_jid(response['bind']['jid']) - self.bound = True - - log.info("Node set to: %s" % self.boundjid.full) - - if 'session' not in features['features']: - log.debug("Established Session") - self.sessionstarted = True - self.session_started_event.set() - self.event("session_start") - - def _handle_start_session(self, features): - """ - Handle the start of the session. - - Arguments: - feature -- The stream features element. - """ - iq = self.Iq() - iq['type'] = 'set' - iq.enable('session') - response = iq.send(now=True) - - log.debug("Established Session") - self.sessionstarted = True - self.session_started_event.set() - self.event("session_start") - def _handle_roster(self, iq, request=False): """ Update the roster after receiving a roster stanza. diff --git a/sleekxmpp/features/__init__.py b/sleekxmpp/features/__init__.py new file mode 100644 index 00000000..940a37f1 --- /dev/null +++ b/sleekxmpp/features/__init__.py @@ -0,0 +1,10 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2010 Nathanael C. Fritz + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +__all__ = ['feature_starttls', 'feature_mechanisms', + 'sasl_plain', 'sasl_anonymous'] diff --git a/sleekxmpp/features/feature_bind.py b/sleekxmpp/features/feature_bind.py new file mode 100644 index 00000000..caa3844b --- /dev/null +++ b/sleekxmpp/features/feature_bind.py @@ -0,0 +1,55 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2010 Nathanael C. Fritz + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +import logging + +from sleekxmpp.xmlstream.matcher import * +from sleekxmpp.xmlstream.handler import * +from sleekxmpp.plugins.base import base_plugin + + +log = logging.getLogger(__name__) + + +class feature_bind(base_plugin): + + def plugin_init(self): + self.name = 'Bind Resource' + self.rfc = '6120' + self.description = 'Resource Binding Stream Feature' + + self.xmpp.register_feature('bind', + self._handle_bind_resource, + restart=False, + order=10000) + + def _handle_bind_resource(self, features): + """ + Handle requesting a specific resource. + + Arguments: + features -- The stream features stanza. + """ + log.debug("Requesting resource: %s" % self.xmpp.boundjid.resource) + iq = self.xmpp.Iq() + iq['type'] = 'set' + iq.enable('bind') + if self.xmpp.boundjid.resource: + iq['bind']['resource'] = self.xmpp.boundjid.resource + response = iq.send(now=True) + + self.xmpp.set_jid(response['bind']['jid']) + self.xmpp.bound = True + + log.info("Node set to: %s" % self.xmpp.boundjid.full) + + if 'session' not in features['features']: + log.debug("Established Session") + self.xmpp.sessionstarted = True + self.xmpp.session_started_event.set() + self.xmpp.event("session_start") diff --git a/sleekxmpp/features/feature_mechanisms.py b/sleekxmpp/features/feature_mechanisms.py new file mode 100644 index 00000000..994c9bed --- /dev/null +++ b/sleekxmpp/features/feature_mechanisms.py @@ -0,0 +1,116 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2010 Nathanael C. Fritz + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +import logging + +from sleekxmpp.stanza import stream +from sleekxmpp.xmlstream import RestartStream +from sleekxmpp.xmlstream.matcher import * +from sleekxmpp.xmlstream.handler import * +from sleekxmpp.plugins.base import base_plugin + + +log = logging.getLogger(__name__) + + +class feature_mechanisms(base_plugin): + + def plugin_init(self): + self.name = 'SASL Mechanisms' + self.rfc = '6120' + self.description = "SASL Stream Feature" + + self.xmpp.register_stanza(stream.sasl.Success) + self.xmpp.register_stanza(stream.sasl.Failure) + self.xmpp.register_stanza(stream.sasl.Auth) + + self._mechanism_handlers = {} + self._mechanism_priorities = [] + + self.xmpp.register_handler( + Callback('SASL Success', + MatchXPath(stream.sasl.Success.tag_name()), + self._handle_success, + instream=True, + once=True)) + self.xmpp.register_handler( + Callback('SASL Failure', + MatchXPath(stream.sasl.Failure.tag_name()), + self._handle_fail, + instream=True, + once=True)) + + self.xmpp.register_feature('mechanisms', + self._handle_sasl_auth, + restart=True, + order=self.config.get('order', 100)) + + def register_mechanism(self, name, handler, priority=0): + """ + Register a handler for a SASL authentication mechanism. + + Arguments: + name -- The name of the mechanism (all caps) + handler -- The function that will perform the + authentication. The function must + return True if it is able to carry + out the authentication, False if + a required condition is not met. + priority -- An integer value indicating the + preferred ordering for the mechanism. + High values will be attempted first. + """ + self._mechanism_handlers[name] = handler + self._mechanism_priorities.append((priority, name)) + self._mechanism_priorities.sort(reverse=True) + + def remove_mechanism(self, name): + """ + Remove support for a given SASL authentication mechanism. + + Arguments: + name -- The name of the mechanism to remove (all caps) + """ + if name in self._mechanism_handlers: + del self._mechanism_handlers[name] + + p = self._mechanism_priorities + self._mechanism_priorities = [i for i in p if i[1] != name] + + def _handle_sasl_auth(self, features): + """ + Handle authenticating using SASL. + + Arguments: + features -- The stream features stanza. + """ + for priority, mech in self._mechanism_priorities: + if mech in features['mechanisms']: + log.debug('Attempt to use SASL %s' % mech) + if self._mechanism_handlers[mech](): + break + else: + log.error("No appropriate login method.") + self.xmpp.event("no_auth", direct=True) + self.xmpp.disconnect() + + return True + + def _handle_success(self, stanza): + """SASL authentication succeeded. Restart the stream.""" + self.xmpp.authenticated = True + self.xmpp.features.append('mechanisms') + raise RestartStream() + + def _handle_fail(self, stanza): + """SASL authentication failed. Disconnect and shutdown.""" + log.info("Authentication failed.") + self.xmpp.event("failed_auth", direct=True) + self.xmpp.disconnect() + log.debug("Starting SASL Auth") + return True diff --git a/sleekxmpp/features/feature_session.py b/sleekxmpp/features/feature_session.py new file mode 100644 index 00000000..5bae358c --- /dev/null +++ b/sleekxmpp/features/feature_session.py @@ -0,0 +1,46 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2010 Nathanael C. Fritz + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +import logging + +from sleekxmpp.xmlstream.matcher import * +from sleekxmpp.xmlstream.handler import * +from sleekxmpp.plugins.base import base_plugin + + +log = logging.getLogger(__name__) + + +class feature_session(base_plugin): + + def plugin_init(self): + self.name = 'Start Session' + self.rfc = '3920' + self.description = 'Start Session Stream Feature' + + self.xmpp.register_feature('session', + self._handle_start_session, + restart=False, + order=10001) + + def _handle_start_session(self, features): + """ + Handle the start of the session. + + Arguments: + feature -- The stream features element. + """ + iq = self.xmpp.Iq() + iq['type'] = 'set' + iq.enable('session') + response = iq.send(now=True) + + log.debug("Established Session") + self.xmpp.sessionstarted = True + self.xmpp.session_started_event.set() + self.xmpp.event("session_start") diff --git a/sleekxmpp/features/feature_starttls.py b/sleekxmpp/features/feature_starttls.py new file mode 100644 index 00000000..5367fa49 --- /dev/null +++ b/sleekxmpp/features/feature_starttls.py @@ -0,0 +1,61 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2010 Nathanael C. Fritz + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +import logging + +from sleekxmpp.stanza.stream import tls +from sleekxmpp.xmlstream import RestartStream +from sleekxmpp.xmlstream.matcher import * +from sleekxmpp.xmlstream.handler import * +from sleekxmpp.plugins.base import base_plugin + + +log = logging.getLogger(__name__) + + +class feature_starttls(base_plugin): + + def plugin_init(self): + self.name = "STARTTLS" + self.rfc = '6120' + self.description = "STARTTLS Stream Feature" + + self.xmpp.register_stanza(tls.Proceed) + self.xmpp.register_handler( + Callback('STARTTLS Proceed', + MatchXPath(tls.Proceed.tag_name()), + self._handle_starttls_proceed, + instream=True)) + self.xmpp.register_feature('starttls', + self._handle_starttls, + restart=True, + order=self.config.get('order', 0)) + + def _handle_starttls(self, features): + """ + Handle notification that the server supports TLS. + + Arguments: + features -- The stream:features element. + """ + if not self.xmpp.use_tls: + return False + elif self.xmpp.ssl_support: + self.xmpp.send(features['starttls'], now=True) + return True + else: + log.warning("The module tlslite is required to log in" +\ + " to some servers, and has not been found.") + return False + + def _handle_starttls_proceed(self, proceed): + """Restart the XML stream when TLS is accepted.""" + log.debug("Starting TLS") + if self.xmpp.start_tls(): + self.xmpp.features.append('starttls') + raise RestartStream() diff --git a/sleekxmpp/features/sasl_anonymous.py b/sleekxmpp/features/sasl_anonymous.py new file mode 100644 index 00000000..469d9d19 --- /dev/null +++ b/sleekxmpp/features/sasl_anonymous.py @@ -0,0 +1,31 @@ +import base64 +import sys +import logging + +from sleekxmpp.stanza.stream import sasl +from sleekxmpp.plugins.base import base_plugin + + +log = logging.getLogger(__name__) + + +class sasl_anonymous(base_plugin): + + def plugin_init(self): + self.name = 'SASL ANONYMOUS' + self.rfc = '6120' + self.description = 'SASL ANONYMOUS Mechanism' + + self.xmpp.register_sasl_mechanism('ANONYMOUS', + self._handle_anonymous, + priority=self.config.get('priority', 0)) + + def _handle_anonymous(self): + if self.xmpp.boundjid.user: + return False + + resp = sasl.Auth(self.xmpp) + resp['mechanism'] = 'ANONYMOUS' + resp.send(now=True) + + return True diff --git a/sleekxmpp/features/sasl_plain.py b/sleekxmpp/features/sasl_plain.py new file mode 100644 index 00000000..36c7d9df --- /dev/null +++ b/sleekxmpp/features/sasl_plain.py @@ -0,0 +1,41 @@ +import base64 +import sys +import logging + +from sleekxmpp.stanza.stream import sasl +from sleekxmpp.plugins.base import base_plugin + + +log = logging.getLogger(__name__) + + +class sasl_plain(base_plugin): + + def plugin_init(self): + self.name = 'SASL PLAIN' + self.rfc = '6120' + self.description = 'SASL PLAIN Mechanism' + + self.xmpp.register_sasl_mechanism('PLAIN', + self._handle_plain, + priority=self.config.get('priority', 1)) + + def _handle_plain(self): + if not self.xmpp.boundjid.user: + return False + + if sys.version_info < (3, 0): + user = bytes(self.xmpp.boundjid.user) + password = bytes(self.xmpp.password) + else: + user = bytes(self.xmpp.boundjid.user, 'utf-8') + password = bytes(self.xmpp.password, 'utf-8') + + auth = base64.b64encode(b'\x00' + user + \ + b'\x00' + password).decode('utf-8') + + resp = sasl.Auth(self.xmpp) + resp['mechanism'] = 'PLAIN' + resp['value'] = auth + resp.send(now=True) + return True diff --git a/sleekxmpp/plugins/base.py b/sleekxmpp/plugins/base.py index 2dd68c8d..561421d8 100644 --- a/sleekxmpp/plugins/base.py +++ b/sleekxmpp/plugins/base.py @@ -66,7 +66,8 @@ class base_plugin(object): """ if config is None: config = {} - self.xep = 'base' + self.xep = None + self.rfc = None self.description = 'Base Plugin' self.xmpp = xmpp self.config = config -- cgit v1.2.3