Reorganize features into plugins.

This commit is contained in:
Lance Stout 2011-06-30 15:40:22 -07:00
parent 9ed972ffeb
commit 754ac5092a
10 changed files with 391 additions and 198 deletions

View file

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

View file

@ -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)
# Setup default stream features
self.register_plugin('feature_starttls')
self.register_plugin('feature_mechanisms')
self.register_plugin('feature_bind')
self.register_plugin('feature_session')
self.register_sasl_mechanism('PLAIN',
self._handle_sasl_plain,
priority=1)
self.register_sasl_mechanism('ANONYMOUS',
self._handle_sasl_anonymous,
priority=0)
# 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.

View file

@ -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']

View file

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

View file

@ -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

View file

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

View file

@ -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()

View file

@ -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

View file

@ -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

View file

@ -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