diff --git a/README b/README
index da9fe8c..8a85365 100644
--- a/README
+++ b/README
@@ -42,6 +42,9 @@ Main Author: Nathan Fritz fritz@netflint.net
Contributors: Kevin Smith & Lance Stout
Patches: Remko Tronçon
+Dave Cridland, for his Suelta SASL library.
+
+
Feel free to add fritzy@netflint.net to your roster for direct support and comments.
Join sleekxmpp-discussion@googlegroups.com / http://groups.google.com/group/sleekxmpp-discussion for email discussion.
Join sleek@conference.jabber.org for groupchat discussion.
diff --git a/setup.py b/setup.py
index a03b869..e83676f 100644
--- a/setup.py
+++ b/setup.py
@@ -45,7 +45,6 @@ packages = [ 'sleekxmpp',
'sleekxmpp/xmlstream',
'sleekxmpp/xmlstream/matcher',
'sleekxmpp/xmlstream/handler',
- 'sleekxmpp/thirdparty',
'sleekxmpp/plugins',
'sleekxmpp/plugins/xep_0009',
'sleekxmpp/plugins/xep_0009/stanza',
@@ -59,6 +58,15 @@ packages = [ 'sleekxmpp',
'sleekxmpp/plugins/xep_0092',
'sleekxmpp/plugins/xep_0128',
'sleekxmpp/plugins/xep_0199',
+ 'sleekxmpp/features',
+ 'sleekxmpp/features/feature_mechanisms',
+ 'sleekxmpp/features/feature_mechanisms/stanza',
+ 'sleekxmpp/features/feature_starttls',
+ 'sleekxmpp/features/feature_bind',
+ 'sleekxmpp/features/feature_session',
+ 'sleekxmpp/thirdparty',
+ 'sleekxmpp/thirdparty/suelta',
+ 'sleekxmpp/thirdparty/suelta/mechanisms',
]
if sys.version_info < (3, 0):
diff --git a/sleekxmpp/basexmpp.py b/sleekxmpp/basexmpp.py
index a7d4931..b188e76 100644
--- a/sleekxmpp/basexmpp.py
+++ b/sleekxmpp/basexmpp.py
@@ -92,6 +92,7 @@ class BaseXMPP(XMLStream):
# Deprecated method names are re-mapped for backwards compatibility.
self.default_ns = default_ns
self.stream_ns = 'http://etherx.jabber.org/streams'
+ self.namespace_map[self.stream_ns] = 'stream'
self.boundjid = JID("")
@@ -105,6 +106,8 @@ class BaseXMPP(XMLStream):
self.sentpresence = False
+ self.stanza = sleekxmpp.stanza
+
self.register_handler(
Callback('IM',
MatchXPath('{%s}message/{%s}body' % (self.default_ns,
@@ -162,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__(str("%s.%s" % (module.__name__, plugin)),
+ globals(), locals(), [str(plugin)])
+ except ImportError:
+ module = sleekxmpp.features
+ module = __import__(str("%s.%s" % (module.__name__, plugin)),
+ globals(), locals(), [str(plugin)])
if isinstance(module, str):
# We probably want to load a module from outside
# the sleekxmpp package, so leave out the globals().
@@ -173,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 2019b23..95ae108 100644
--- a/sleekxmpp/clientxmpp.py
+++ b/sleekxmpp/clientxmpp.py
@@ -15,12 +15,14 @@ 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 Message, Presence, Iq
+from sleekxmpp.stanza import *
from sleekxmpp.xmlstream import XMLStream, RestartStream
-from sleekxmpp.xmlstream import StanzaBase, ET
+from sleekxmpp.xmlstream import StanzaBase, ET, register_stanza_plugin
from sleekxmpp.xmlstream.matcher import *
from sleekxmpp.xmlstream.handler import *
@@ -81,15 +83,19 @@ class ClientXMPP(BaseXMPP):
"xmlns='%s'" % self.default_ns)
self.stream_footer = ""
- self.features = []
- self.registered_features = []
+ self.features = set()
+ self._stream_feature_handlers = {}
+ self._stream_feature_order = []
#TODO: Use stream state here
self.authenticated = False
self.sessionstarted = False
self.bound = False
self.bindfail = False
- self.add_event_handler('connected', self.handle_connected)
+
+ self.add_event_handler('connected', self._handle_connected)
+
+ self.register_stanza(StreamFeatures)
self.register_handler(
Callback('Stream Features',
@@ -102,32 +108,11 @@ class ClientXMPP(BaseXMPP):
'jabber:iq:roster')),
self._handle_roster))
- self.register_feature(
- "",
- self._handle_starttls, True)
- self.register_feature(
- "",
- self._handle_sasl_auth, True)
- self.register_feature(
- "",
- self._handle_bind_resource)
- self.register_feature(
- "",
- self._handle_start_session)
-
- def handle_connected(self, event=None):
- #TODO: Use stream state here
- self.authenticated = False
- self.sessionstarted = False
- self.bound = False
- self.bindfail = False
- self.schedule("session timeout checker", 15,
- self._session_timeout_check)
-
- def _session_timeout_check(self):
- if not self.session_started_event.isSet():
- log.debug("Session start has taken more than 15 seconds")
- self.disconnect(reconnect=self.auto_reconnect)
+ # Setup default stream features
+ self.register_plugin('feature_starttls')
+ self.register_plugin('feature_mechanisms')
+ self.register_plugin('feature_bind')
+ self.register_plugin('feature_session')
def connect(self, address=tuple(), reattempt=True, use_tls=True):
"""
@@ -194,19 +179,22 @@ class ClientXMPP(BaseXMPP):
return XMLStream.connect(self, address[0], address[1],
use_tls=use_tls, reattempt=reattempt)
- def register_feature(self, mask, pointer, breaker=False):
+ def register_feature(self, name, handler, restart=False, order=5000):
"""
Register a stream feature.
Arguments:
- mask -- An XML string matching the feature's element.
- pointer -- The function to execute if the feature is received.
- breaker -- Indicates if feature processing should halt with
+ name -- The name of the stream feature.
+ handler -- The function to execute if the feature is received.
+ restart -- Indicates if feature processing should halt with
this feature. Defaults to False.
+ order -- The relative ordering in which the feature should
+ be negotiated. Lower values will be attempted
+ earlier when available.
"""
- self.registered_features.append((MatchXMLMask(mask),
- pointer,
- breaker))
+ self._stream_feature_handlers[name] = (handler, restart)
+ self._stream_feature_order.append((order, name))
+ self._stream_feature_order.sort()
def update_roster(self, jid, name=None, subscription=None, groups=[],
block=True, timeout=None, callback=None):
@@ -278,6 +266,21 @@ class ClientXMPP(BaseXMPP):
else:
return self._handle_roster(response, request=True)
+ def _handle_connected(self, event=None):
+ #TODO: Use stream state here
+ self.authenticated = False
+ self.sessionstarted = False
+ self.bound = False
+ self.bindfail = False
+ self.features = set()
+
+ def session_timeout():
+ if not self.session_started_event.isSet():
+ log.debug("Session start has taken more than 15 seconds")
+ self.disconnect(reconnect=self.auto_reconnect)
+
+ self.schedule("session timeout checker", 15, session_timeout)
+
def _handle_stream_features(self, features):
"""
Process the received stream features.
@@ -285,172 +288,13 @@ class ClientXMPP(BaseXMPP):
Arguments:
features -- The features stanza.
"""
- # Record all of the features.
- self.features = []
- for sub in features.xml:
- self.features.append(sub.tag)
-
- # Process the features.
- for sub in features.xml:
- for feature in self.registered_features:
- mask, handler, halt = feature
- if mask.match(sub):
- if handler(sub) and halt:
- # Don't continue if the feature was
- # marked as a breaker.
- return True
-
- def _handle_starttls(self, xml):
- """
- Handle notification that the server supports TLS.
-
- Arguments:
- xml -- The STARTLS proceed element.
- """
- if not self.use_tls:
- return False
- elif not self.authenticated and self.ssl_support:
- tls_ns = 'urn:ietf:params:xml:ns:xmpp-tls'
- self.add_handler("" % tls_ns,
- self._handle_tls_start,
- name='TLS Proceed',
- instream=True)
- self.send_xml(xml, 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_tls_start(self, xml):
- """
- Handle encrypting the stream using TLS.
-
- Restarts the stream.
- """
- log.debug("Starting TLS")
- if self.start_tls():
- raise RestartStream()
-
- def _handle_sasl_auth(self, xml):
- """
- Handle authenticating using SASL.
-
- Arguments:
- xml -- The SASL mechanisms stanza.
- """
- if self.use_tls and \
- '{urn:ietf:params:xml:ns:xmpp-tls}starttls' in self.features:
- return False
-
- log.debug("Starting SASL Auth")
- sasl_ns = 'urn:ietf:params:xml:ns:xmpp-sasl'
- self.add_handler("" % sasl_ns,
- self._handle_auth_success,
- name='SASL Sucess',
- instream=True)
- self.add_handler("" % sasl_ns,
- self._handle_auth_fail,
- name='SASL Failure',
- instream=True)
-
- sasl_mechs = xml.findall('{%s}mechanism' % sasl_ns)
- if sasl_mechs:
- for sasl_mech in sasl_mechs:
- self.features.append("sasl:%s" % sasl_mech.text)
- if 'sasl:PLAIN' in self.features and self.boundjid.user:
- 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')
-
- self.send("%s" % (
- sasl_ns,
- auth),
- now=True)
- elif 'sasl:ANONYMOUS' in self.features and not self.boundjid.user:
- self.send("" % (
- sasl_ns,
- 'ANONYMOUS'),
- now=True)
- else:
- log.error("No appropriate login method.")
- self.disconnect()
- return True
-
- def _handle_auth_success(self, xml):
- """
- SASL authentication succeeded. Restart the stream.
-
- Arguments:
- xml -- The SASL authentication success element.
- """
- self.authenticated = True
- self.features = []
- raise RestartStream()
-
- def _handle_auth_fail(self, xml):
- """
- SASL authentication failed. Disconnect and shutdown.
-
- Arguments:
- xml -- The SASL authentication failure element.
- """
- log.info("Authentication failed.")
- self.event("failed_auth", direct=True)
- self.disconnect()
-
- def _handle_bind_resource(self, xml):
- """
- Handle requesting a specific resource.
-
- Arguments:
- xml -- The bind feature element.
- """
- log.debug("Requesting resource: %s" % self.boundjid.resource)
- xml.clear()
- iq = self.Iq(stype='set')
- if self.boundjid.resource:
- res = ET.Element('resource')
- res.text = self.boundjid.resource
- xml.append(res)
- iq.append(xml)
- response = iq.send(now=True)
-
- bind_ns = 'urn:ietf:params:xml:ns:xmpp-bind'
- self.set_jid(response.xml.find('{%s}bind/{%s}jid' % (bind_ns,
- bind_ns)).text)
- self.bound = True
- log.info("Node set to: %s" % self.boundjid.full)
- session_ns = 'urn:ietf:params:xml:ns:xmpp-session'
- if "{%s}session" % session_ns not in self.features or self.bindfail:
- log.debug("Established Session")
- self.sessionstarted = True
- self.session_started_event.set()
- self.event("session_start")
-
- def _handle_start_session(self, xml):
- """
- Handle the start of the session.
-
- Arguments:
- xml -- The session feature element.
- """
- if self.authenticated and self.bound:
- iq = self.makeIqSet(xml)
- response = iq.send(now=True)
- log.debug("Established Session")
- self.sessionstarted = True
- self.session_started_event.set()
- self.event("session_start")
- else:
- # Bind probably hasn't happened yet.
- self.bindfail = True
+ for order, name in self._stream_feature_order:
+ if name in features['features']:
+ handler, restart = self._stream_feature_handlers[name]
+ if handler(features) and restart:
+ # Don't continue if the feature requires
+ # restarting the XML stream.
+ return True
def _handle_roster(self, iq, request=False):
"""
diff --git a/sleekxmpp/features/__init__.py b/sleekxmpp/features/__init__.py
new file mode 100644
index 0000000..5c86cfe
--- /dev/null
+++ b/sleekxmpp/features/__init__.py
@@ -0,0 +1,11 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2011 Nathanael C. Fritz
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+__all__ = ['feature_starttls', 'feature_mechanisms',
+ 'feature_bind', 'feature_session',
+ 'sasl_plain', 'sasl_anonymous']
diff --git a/sleekxmpp/features/feature_bind/__init__.py b/sleekxmpp/features/feature_bind/__init__.py
new file mode 100644
index 0000000..aa854f8
--- /dev/null
+++ b/sleekxmpp/features/feature_bind/__init__.py
@@ -0,0 +1,10 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2011 Nathanael C. Fritz
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+from sleekxmpp.features.feature_bind.bind import feature_bind
+from sleekxmpp.features.feature_bind.stanza import Bind
diff --git a/sleekxmpp/features/feature_bind/bind.py b/sleekxmpp/features/feature_bind/bind.py
new file mode 100644
index 0000000..de03192
--- /dev/null
+++ b/sleekxmpp/features/feature_bind/bind.py
@@ -0,0 +1,64 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2011 Nathanael C. Fritz
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+import logging
+
+from sleekxmpp.stanza import Iq, StreamFeatures
+from sleekxmpp.features.feature_bind import stanza
+from sleekxmpp.xmlstream import register_stanza_plugin
+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.stanza = stanza
+
+ self.xmpp.register_feature('bind',
+ self._handle_bind_resource,
+ restart=False,
+ order=10000)
+
+ register_stanza_plugin(Iq, stanza.Bind)
+ register_stanza_plugin(StreamFeatures, stanza.Bind)
+
+ 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
+
+ self.xmpp.features.add('bind')
+
+ 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_bind/stanza.py b/sleekxmpp/features/feature_bind/stanza.py
new file mode 100644
index 0000000..2c1484e
--- /dev/null
+++ b/sleekxmpp/features/feature_bind/stanza.py
@@ -0,0 +1,22 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2011 Nathanael C. Fritz
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+from sleekxmpp.stanza import Iq, StreamFeatures
+from sleekxmpp.xmlstream import ElementBase, ET, register_stanza_plugin
+
+
+class Bind(ElementBase):
+
+ """
+ """
+
+ name = 'bind'
+ namespace = 'urn:ietf:params:xml:ns:xmpp-bind'
+ interfaces = set(('resource', 'jid'))
+ sub_interfaces = interfaces
+ plugin_attrib = 'bind'
diff --git a/sleekxmpp/features/feature_mechanisms/__init__.py b/sleekxmpp/features/feature_mechanisms/__init__.py
new file mode 100644
index 0000000..5379ef4
--- /dev/null
+++ b/sleekxmpp/features/feature_mechanisms/__init__.py
@@ -0,0 +1,13 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2011 Nathanael C. Fritz
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+from sleekxmpp.features.feature_mechanisms.mechanisms import feature_mechanisms
+from sleekxmpp.features.feature_mechanisms.stanza import Mechanisms
+from sleekxmpp.features.feature_mechanisms.stanza import Auth
+from sleekxmpp.features.feature_mechanisms.stanza import Success
+from sleekxmpp.features.feature_mechanisms.stanza import Failure
diff --git a/sleekxmpp/features/feature_mechanisms/mechanisms.py b/sleekxmpp/features/feature_mechanisms/mechanisms.py
new file mode 100644
index 0000000..d60818b
--- /dev/null
+++ b/sleekxmpp/features/feature_mechanisms/mechanisms.py
@@ -0,0 +1,127 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2011 Nathanael C. Fritz
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+import logging
+
+from sleekxmpp.thirdparty import suelta
+
+from sleekxmpp.stanza import StreamFeatures
+from sleekxmpp.xmlstream import RestartStream, register_stanza_plugin
+from sleekxmpp.xmlstream.matcher import *
+from sleekxmpp.xmlstream.handler import *
+from sleekxmpp.plugins.base import base_plugin
+from sleekxmpp.features.feature_mechanisms import stanza
+
+
+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.stanza = stanza
+
+
+ def tls_active():
+ return 'starttls' in self.xmpp.features
+
+ def basic_callback(mech, values):
+ if 'username' in values:
+ values['username'] = self.xmpp.boundjid.user
+ if 'password' in values:
+ values['password'] = self.xmpp.password
+ mech.fulfill(values)
+
+ sasl_callback = self.config.get('sasl_callback', None)
+ if sasl_callback is None:
+ sasl_callback = basic_callback
+
+ self.mech = None
+ self.sasl = suelta.SASL(self.xmpp.boundjid.domain, 'xmpp',
+ username=self.xmpp.boundjid.user,
+ sec_query=suelta.sec_query_allow,
+ request_values=sasl_callback,
+ tls_active=tls_active)
+
+ register_stanza_plugin(StreamFeatures, stanza.Mechanisms)
+
+ self.xmpp.register_stanza(stanza.Success)
+ self.xmpp.register_stanza(stanza.Failure)
+ self.xmpp.register_stanza(stanza.Auth)
+ self.xmpp.register_stanza(stanza.Challenge)
+ self.xmpp.register_stanza(stanza.Response)
+
+ self.xmpp.register_handler(
+ Callback('SASL Success',
+ MatchXPath(stanza.Success.tag_name()),
+ self._handle_success,
+ instream=True,
+ once=True))
+ self.xmpp.register_handler(
+ Callback('SASL Failure',
+ MatchXPath(stanza.Failure.tag_name()),
+ self._handle_fail,
+ instream=True,
+ once=True))
+ self.xmpp.register_handler(
+ Callback('SASL Challenge',
+ MatchXPath(stanza.Challenge.tag_name()),
+ self._handle_challenge))
+
+ self.xmpp.register_feature('mechanisms',
+ self._handle_sasl_auth,
+ restart=True,
+ order=self.config.get('order', 100))
+
+ def _handle_sasl_auth(self, features):
+ """
+ Handle authenticating using SASL.
+
+ Arguments:
+ features -- The stream features stanza.
+ """
+ if 'mechanisms' in self.xmpp.features:
+ # SASL authentication has already succeeded, but the
+ # server has incorrectly offered it again.
+ return False
+
+ mech_list = features['mechanisms']
+ self.mech = self.sasl.choose_mechanism(mech_list)
+
+ if self.mech is not None:
+ resp = stanza.Auth(self.xmpp)
+ resp['mechanism'] = self.mech.name
+ resp['value'] = self.mech.process()
+ resp.send(now=True)
+ else:
+ log.error("No appropriate login method.")
+ self.xmpp.event("no_auth", direct=True)
+ self.xmpp.disconnect()
+ return True
+
+ def _handle_challenge(self, stanza):
+ """SASL challenge received. Process and send response."""
+ resp = self.stanza.Response(self.xmpp)
+ resp['value'] = self.mech.process(stanza['value'])
+ resp.send(now=True)
+
+ def _handle_success(self, stanza):
+ """SASL authentication succeeded. Restart the stream."""
+ self.xmpp.authenticated = True
+ self.xmpp.features.add('mechanisms')
+ raise RestartStream()
+
+ def _handle_fail(self, stanza):
+ """SASL authentication failed. Disconnect and shutdown."""
+ log.info("Authentication failed: %s" % stanza['condition'])
+ self.xmpp.event("failed_auth", stanza, direct=True)
+ self.xmpp.disconnect()
+ return True
diff --git a/sleekxmpp/features/feature_mechanisms/stanza/__init__.py b/sleekxmpp/features/feature_mechanisms/stanza/__init__.py
new file mode 100644
index 0000000..8b80f35
--- /dev/null
+++ b/sleekxmpp/features/feature_mechanisms/stanza/__init__.py
@@ -0,0 +1,15 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2011 Nathanael C. Fritz
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+
+from sleekxmpp.features.feature_mechanisms.stanza.mechanisms import Mechanisms
+from sleekxmpp.features.feature_mechanisms.stanza.auth import Auth
+from sleekxmpp.features.feature_mechanisms.stanza.success import Success
+from sleekxmpp.features.feature_mechanisms.stanza.failure import Failure
+from sleekxmpp.features.feature_mechanisms.stanza.challenge import Challenge
+from sleekxmpp.features.feature_mechanisms.stanza.response import Response
diff --git a/sleekxmpp/features/feature_mechanisms/stanza/auth.py b/sleekxmpp/features/feature_mechanisms/stanza/auth.py
new file mode 100644
index 0000000..e069b57
--- /dev/null
+++ b/sleekxmpp/features/feature_mechanisms/stanza/auth.py
@@ -0,0 +1,39 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2011 Nathanael C. Fritz
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+import base64
+
+from sleekxmpp.thirdparty.suelta.util import bytes
+
+from sleekxmpp.stanza import StreamFeatures
+from sleekxmpp.xmlstream import ElementBase, StanzaBase, ET
+from sleekxmpp.xmlstream import register_stanza_plugin
+
+
+class Auth(StanzaBase):
+
+ """
+ """
+
+ name = 'auth'
+ namespace = 'urn:ietf:params:xml:ns:xmpp-sasl'
+ interfaces = set(('mechanism', 'value'))
+ plugin_attrib = name
+
+ def setup(self, xml):
+ StanzaBase.setup(self, xml)
+ self.xml.tag = self.tag_name()
+
+ def get_value(self):
+ return base64.b64decode(bytes(self.xml.text))
+
+ def set_value(self, values):
+ self.xml.text = bytes(base64.b64encode(values)).decode('utf-8')
+
+ def del_value(self):
+ self.xml.text = ''
diff --git a/sleekxmpp/features/feature_mechanisms/stanza/challenge.py b/sleekxmpp/features/feature_mechanisms/stanza/challenge.py
new file mode 100644
index 0000000..82af869
--- /dev/null
+++ b/sleekxmpp/features/feature_mechanisms/stanza/challenge.py
@@ -0,0 +1,39 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2011 Nathanael C. Fritz
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+import base64
+
+from sleekxmpp.thirdparty.suelta.util import bytes
+
+from sleekxmpp.stanza import StreamFeatures
+from sleekxmpp.xmlstream import ElementBase, StanzaBase, ET
+from sleekxmpp.xmlstream import register_stanza_plugin
+
+
+class Challenge(StanzaBase):
+
+ """
+ """
+
+ name = 'challenge'
+ namespace = 'urn:ietf:params:xml:ns:xmpp-sasl'
+ interfaces = set(('value',))
+ plugin_attrib = name
+
+ def setup(self, xml):
+ StanzaBase.setup(self, xml)
+ self.xml.tag = self.tag_name()
+
+ def get_value(self):
+ return base64.b64decode(bytes(self.xml.text))
+
+ def set_value(self, values):
+ self.xml.text = bytes(base64.b64encode(values)).decode('utf-8')
+
+ def del_value(self):
+ self.xml.text = ''
diff --git a/sleekxmpp/features/feature_mechanisms/stanza/failure.py b/sleekxmpp/features/feature_mechanisms/stanza/failure.py
new file mode 100644
index 0000000..027cc5a
--- /dev/null
+++ b/sleekxmpp/features/feature_mechanisms/stanza/failure.py
@@ -0,0 +1,78 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2011 Nathanael C. Fritz
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+from sleekxmpp.stanza import StreamFeatures
+from sleekxmpp.xmlstream import ElementBase, StanzaBase, ET
+from sleekxmpp.xmlstream import register_stanza_plugin
+
+
+class Failure(StanzaBase):
+
+ """
+ """
+
+ name = 'failure'
+ namespace = 'urn:ietf:params:xml:ns:xmpp-sasl'
+ interfaces = set(('condition', 'text'))
+ plugin_attrib = name
+ sub_interfaces = set(('text',))
+ conditions = set(('aborted', 'account-disabled', 'credentials-expired',
+ 'encryption-required', 'incorrect-encoding', 'invalid-authzid',
+ 'invalid-mechanism', 'malformed-request', 'mechansism-too-weak',
+ 'not-authorized', 'temporary-auth-failure'))
+
+ def setup(self, xml=None):
+ """
+ Populate the stanza object using an optional XML object.
+
+ Overrides ElementBase.setup.
+
+ Sets a default error type and condition, and changes the
+ parent stanza's type to 'error'.
+
+ Arguments:
+ xml -- Use an existing XML object for the stanza's values.
+ """
+ # StanzaBase overrides self.namespace
+ self.namespace = Failure.namespace
+
+ if StanzaBase.setup(self, xml):
+ #If we had to generate XML then set default values.
+ self['condition'] = 'not-authorized'
+
+ self.xml.tag = self.tag_name()
+
+ def get_condition(self):
+ """Return the condition element's name."""
+ for child in self.xml.getchildren():
+ if "{%s}" % self.namespace in child.tag:
+ cond = child.tag.split('}', 1)[-1]
+ if cond in self.conditions:
+ return cond
+ return 'not-authorized'
+
+ def set_condition(self, value):
+ """
+ Set the tag name of the condition element.
+
+ Arguments:
+ value -- The tag name of the condition element.
+ """
+ if value in self.conditions:
+ del self['condition']
+ self.xml.append(ET.Element("{%s}%s" % (self.namespace, value)))
+ return self
+
+ def del_condition(self):
+ """Remove the condition element."""
+ for child in self.xml.getchildren():
+ if "{%s}" % self.condition_ns in child.tag:
+ tag = child.tag.split('}', 1)[-1]
+ if tag in self.conditions:
+ self.xml.remove(child)
+ return self
diff --git a/sleekxmpp/features/feature_mechanisms/stanza/mechanisms.py b/sleekxmpp/features/feature_mechanisms/stanza/mechanisms.py
new file mode 100644
index 0000000..1189cd8
--- /dev/null
+++ b/sleekxmpp/features/feature_mechanisms/stanza/mechanisms.py
@@ -0,0 +1,55 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2011 Nathanael C. Fritz
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+from sleekxmpp.stanza import StreamFeatures
+from sleekxmpp.xmlstream import ElementBase, StanzaBase, ET
+from sleekxmpp.xmlstream import register_stanza_plugin
+
+
+class Mechanisms(ElementBase):
+
+ """
+ """
+
+ name = 'mechanisms'
+ namespace = 'urn:ietf:params:xml:ns:xmpp-sasl'
+ interfaces = set(('mechanisms', 'required'))
+ plugin_attrib = name
+ is_extension = True
+
+ def get_required(self):
+ """
+ """
+ return True
+
+ def get_mechanisms(self):
+ """
+ """
+ results = []
+ mechs = self.findall('{%s}mechanism' % self.namespace)
+ if mechs:
+ for mech in mechs:
+ results.append(mech.text)
+ return results
+
+ def set_mechanisms(self, values):
+ """
+ """
+ self.del_mechanisms()
+ for val in values:
+ mech = ET.Element('{%s}mechanism' % self.namespace)
+ mech.text = val
+ self.append(mech)
+
+ def del_mechanisms(self):
+ """
+ """
+ mechs = self.findall('{%s}mechanism' % self.namespace)
+ if mechs:
+ for mech in mechs:
+ self.xml.remove(mech)
diff --git a/sleekxmpp/features/feature_mechanisms/stanza/response.py b/sleekxmpp/features/feature_mechanisms/stanza/response.py
new file mode 100644
index 0000000..45bb820
--- /dev/null
+++ b/sleekxmpp/features/feature_mechanisms/stanza/response.py
@@ -0,0 +1,39 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2011 Nathanael C. Fritz
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+import base64
+
+from sleekxmpp.thirdparty.suelta.util import bytes
+
+from sleekxmpp.stanza import StreamFeatures
+from sleekxmpp.xmlstream import ElementBase, StanzaBase, ET
+from sleekxmpp.xmlstream import register_stanza_plugin
+
+
+class Response(StanzaBase):
+
+ """
+ """
+
+ name = 'response'
+ namespace = 'urn:ietf:params:xml:ns:xmpp-sasl'
+ interfaces = set(('value',))
+ plugin_attrib = name
+
+ def setup(self, xml):
+ StanzaBase.setup(self, xml)
+ self.xml.tag = self.tag_name()
+
+ def get_value(self):
+ return base64.b64decode(bytes(self.xml.text))
+
+ def set_value(self, values):
+ self.xml.text = bytes(base64.b64encode(values)).decode('utf-8')
+
+ def del_value(self):
+ self.xml.text = ''
diff --git a/sleekxmpp/features/feature_mechanisms/stanza/success.py b/sleekxmpp/features/feature_mechanisms/stanza/success.py
new file mode 100644
index 0000000..028e28a
--- /dev/null
+++ b/sleekxmpp/features/feature_mechanisms/stanza/success.py
@@ -0,0 +1,26 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2011 Nathanael C. Fritz
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+from sleekxmpp.stanza import StreamFeatures
+from sleekxmpp.xmlstream import ElementBase, StanzaBase, ET
+from sleekxmpp.xmlstream import register_stanza_plugin
+
+
+class Success(StanzaBase):
+
+ """
+ """
+
+ name = 'success'
+ namespace = 'urn:ietf:params:xml:ns:xmpp-sasl'
+ interfaces = set()
+ plugin_attrib = name
+
+ def setup(self, xml):
+ StanzaBase.setup(self, xml)
+ self.xml.tag = self.tag_name()
diff --git a/sleekxmpp/features/feature_session/__init__.py b/sleekxmpp/features/feature_session/__init__.py
new file mode 100644
index 0000000..3c84bae
--- /dev/null
+++ b/sleekxmpp/features/feature_session/__init__.py
@@ -0,0 +1,10 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2011 Nathanael C. Fritz
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+from sleekxmpp.features.feature_session.session import feature_session
+from sleekxmpp.features.feature_session.stanza import Session
diff --git a/sleekxmpp/features/feature_session/session.py b/sleekxmpp/features/feature_session/session.py
new file mode 100644
index 0000000..0daec5d
--- /dev/null
+++ b/sleekxmpp/features/feature_session/session.py
@@ -0,0 +1,56 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2011 Nathanael C. Fritz
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+import logging
+
+from sleekxmpp.stanza import Iq, StreamFeatures
+from sleekxmpp.xmlstream import register_stanza_plugin
+from sleekxmpp.xmlstream.matcher import *
+from sleekxmpp.xmlstream.handler import *
+from sleekxmpp.plugins.base import base_plugin
+
+from sleekxmpp.features.feature_session import stanza
+
+
+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.stanza = stanza
+
+ self.xmpp.register_feature('session',
+ self._handle_start_session,
+ restart=False,
+ order=10001)
+
+ register_stanza_plugin(Iq, stanza.Session)
+ register_stanza_plugin(StreamFeatures, stanza.Session)
+
+ 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)
+
+ self.xmpp.features.add('session')
+
+ 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_session/stanza.py b/sleekxmpp/features/feature_session/stanza.py
new file mode 100644
index 0000000..40ea583
--- /dev/null
+++ b/sleekxmpp/features/feature_session/stanza.py
@@ -0,0 +1,21 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2011 Nathanael C. Fritz
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+from sleekxmpp.stanza import Iq, StreamFeatures
+from sleekxmpp.xmlstream import ElementBase, ET, register_stanza_plugin
+
+
+class Session(ElementBase):
+
+ """
+ """
+
+ name = 'session'
+ namespace = 'urn:ietf:params:xml:ns:xmpp-session'
+ interfaces = set()
+ plugin_attrib = 'session'
diff --git a/sleekxmpp/features/feature_starttls/__init__.py b/sleekxmpp/features/feature_starttls/__init__.py
new file mode 100644
index 0000000..4ae8943
--- /dev/null
+++ b/sleekxmpp/features/feature_starttls/__init__.py
@@ -0,0 +1,10 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2011 Nathanael C. Fritz
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+from sleekxmpp.features.feature_starttls.starttls import feature_starttls
+from sleekxmpp.features.feature_starttls.stanza import *
diff --git a/sleekxmpp/features/feature_starttls/stanza.py b/sleekxmpp/features/feature_starttls/stanza.py
new file mode 100644
index 0000000..8b09ad9
--- /dev/null
+++ b/sleekxmpp/features/feature_starttls/stanza.py
@@ -0,0 +1,47 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2011 Nathanael C. Fritz
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+from sleekxmpp.stanza import StreamFeatures
+from sleekxmpp.xmlstream import StanzaBase, ElementBase
+from sleekxmpp.xmlstream import register_stanza_plugin
+
+
+class STARTTLS(ElementBase):
+
+ """
+ """
+
+ name = 'starttls'
+ namespace = 'urn:ietf:params:xml:ns:xmpp-tls'
+ interfaces = set(('required',))
+ plugin_attrib = name
+
+ def get_required(self):
+ """
+ """
+ return True
+
+
+class Proceed(StanzaBase):
+
+ """
+ """
+
+ name = 'proceed'
+ namespace = 'urn:ietf:params:xml:ns:xmpp-tls'
+ interfaces = set()
+
+
+class Failure(StanzaBase):
+
+ """
+ """
+
+ name = 'failure'
+ namespace = 'urn:ietf:params:xml:ns:xmpp-tls'
+ interfaces = set()
diff --git a/sleekxmpp/features/feature_starttls/starttls.py b/sleekxmpp/features/feature_starttls/starttls.py
new file mode 100644
index 0000000..639788a
--- /dev/null
+++ b/sleekxmpp/features/feature_starttls/starttls.py
@@ -0,0 +1,70 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2011 Nathanael C. Fritz
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+import logging
+
+from sleekxmpp.stanza import StreamFeatures
+from sleekxmpp.xmlstream import RestartStream, register_stanza_plugin
+from sleekxmpp.xmlstream.matcher import *
+from sleekxmpp.xmlstream.handler import *
+from sleekxmpp.plugins.base import base_plugin
+from sleekxmpp.features.feature_starttls import stanza
+
+
+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.stanza = stanza
+
+ self.xmpp.register_handler(
+ Callback('STARTTLS Proceed',
+ MatchXPath(stanza.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))
+
+ self.xmpp.register_stanza(stanza.Proceed)
+ self.xmpp.register_stanza(stanza.Failure)
+ register_stanza_plugin(StreamFeatures, stanza.STARTTLS)
+
+ def _handle_starttls(self, features):
+ """
+ Handle notification that the server supports TLS.
+
+ Arguments:
+ features -- The stream:features element.
+ """
+ if 'starttls' in self.xmpp.features:
+ # We have already negotiated TLS, but the server is
+ # offering it again, against spec.
+ return False
+ elif 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.add('starttls')
+ raise RestartStream()
diff --git a/sleekxmpp/plugins/base.py b/sleekxmpp/plugins/base.py
index 2dd68c8..561421d 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
diff --git a/sleekxmpp/stanza/__init__.py b/sleekxmpp/stanza/__init__.py
index dbf7b86..4bd37dc 100644
--- a/sleekxmpp/stanza/__init__.py
+++ b/sleekxmpp/stanza/__init__.py
@@ -8,7 +8,8 @@
from sleekxmpp.stanza.error import Error
-from sleekxmpp.stanza.stream_error import StreamError
from sleekxmpp.stanza.iq import Iq
from sleekxmpp.stanza.message import Message
from sleekxmpp.stanza.presence import Presence
+from sleekxmpp.stanza.stream_features import StreamFeatures
+from sleekxmpp.stanza.stream_error import StreamError
diff --git a/sleekxmpp/stanza/error.py b/sleekxmpp/stanza/error.py
index 5d1ce50..93231a4 100644
--- a/sleekxmpp/stanza/error.py
+++ b/sleekxmpp/stanza/error.py
@@ -88,7 +88,9 @@ class Error(ElementBase):
"""Return the condition element's name."""
for child in self.xml.getchildren():
if "{%s}" % self.condition_ns in child.tag:
- return child.tag.split('}', 1)[-1]
+ cond = child.tag.split('}', 1)[-1]
+ if cond in self.conditions:
+ return cond
return ''
def set_condition(self, value):
diff --git a/sleekxmpp/stanza/stream_features.py b/sleekxmpp/stanza/stream_features.py
new file mode 100644
index 0000000..5be2e55
--- /dev/null
+++ b/sleekxmpp/stanza/stream_features.py
@@ -0,0 +1,52 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+from sleekxmpp.xmlstream import ElementBase, StanzaBase, ET
+from sleekxmpp.xmlstream import register_stanza_plugin
+
+
+class StreamFeatures(StanzaBase):
+
+ """
+ """
+
+ name = 'features'
+ namespace = 'http://etherx.jabber.org/streams'
+ interfaces = set(('features', 'required', 'optional'))
+ sub_interfaces = interfaces
+
+ def setup(self, xml):
+ StanzaBase.setup(self, xml)
+ self.values = self.values
+
+ def get_features(self):
+ """
+ """
+ return self.plugins
+
+ def set_features(self, value):
+ """
+ """
+ pass
+
+ def del_features(self):
+ """
+ """
+ pass
+
+ def get_required(self):
+ """
+ """
+ features = self['features']
+ return [f for n, f in features.items() if f['required']]
+
+ def get_optional(self):
+ """
+ """
+ features = self['features']
+ return [f for n, f in features.items() if not f['required']]
diff --git a/sleekxmpp/thirdparty/__init__.py b/sleekxmpp/thirdparty/__init__.py
index 276ac3c..3eb6ad7 100644
--- a/sleekxmpp/thirdparty/__init__.py
+++ b/sleekxmpp/thirdparty/__init__.py
@@ -2,3 +2,5 @@ try:
from collections import OrderedDict
except:
from sleekxmpp.thirdparty.ordereddict import OrderedDict
+
+from sleekxmpp.thirdparty import suelta
diff --git a/sleekxmpp/thirdparty/suelta/LICENSE b/sleekxmpp/thirdparty/suelta/LICENSE
new file mode 100644
index 0000000..6eee4f3
--- /dev/null
+++ b/sleekxmpp/thirdparty/suelta/LICENSE
@@ -0,0 +1,21 @@
+This software is subject to "The MIT License"
+
+Copyright 2007-2010 David Alan Cridland
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/sleekxmpp/thirdparty/suelta/PLAYING-NICELY b/sleekxmpp/thirdparty/suelta/PLAYING-NICELY
new file mode 100644
index 0000000..393b807
--- /dev/null
+++ b/sleekxmpp/thirdparty/suelta/PLAYING-NICELY
@@ -0,0 +1,27 @@
+Hi.
+
+This is a short note explaining the license in non-legally-binding terms, and
+describing how I hope to see people work with the licensing.
+
+First off, the license is permissive, and more or less allows you to do
+anything, as long as you leave my credit and copyright intact.
+
+You can, and are very much welcome to, include this in commercial works, and
+in code that has tightly controlled distribution, as well as open-source.
+
+If it doesn't work - and I have no doubt that there are bugs - then this is
+largely your problem.
+
+If you do find a bug, though, do let me know - although you don't have to.
+
+And if you fix it, I'd greatly appreciate a patch, too. Please give me a
+licensing statement, and a copyright statement, along with your patch.
+
+Similarly, any enhancements are welcome, and also will need copyright and
+licensing. Please stick to a license which is compatible with the MIT license,
+and consider assignment (as required) to me to simplify licensing. (Public
+domain does not exist in the UK, sorry).
+
+Thanks,
+
+Dave.
diff --git a/sleekxmpp/thirdparty/suelta/README b/sleekxmpp/thirdparty/suelta/README
new file mode 100644
index 0000000..c32463a
--- /dev/null
+++ b/sleekxmpp/thirdparty/suelta/README
@@ -0,0 +1,8 @@
+Suelta - A pure-Python SASL client library
+
+Suelta is a SASL library, providing you with authentication and in some cases
+security layers.
+
+It supports a wide range of typical SASL mechanisms, including the MTI for
+all known protocols.
+
diff --git a/sleekxmpp/thirdparty/suelta/__init__.py b/sleekxmpp/thirdparty/suelta/__init__.py
new file mode 100644
index 0000000..04f0cba
--- /dev/null
+++ b/sleekxmpp/thirdparty/suelta/__init__.py
@@ -0,0 +1,26 @@
+# Copyright 2007-2010 David Alan Cridland
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+
+from sleekxmpp.thirdparty.suelta.saslprep import saslprep
+from sleekxmpp.thirdparty.suelta.sasl import *
+from sleekxmpp.thirdparty.suelta.mechanisms import *
+
+__version__ = '2.0'
+__version_info__ = (2, 0, 0)
diff --git a/sleekxmpp/thirdparty/suelta/exceptions.py b/sleekxmpp/thirdparty/suelta/exceptions.py
new file mode 100644
index 0000000..625cca0
--- /dev/null
+++ b/sleekxmpp/thirdparty/suelta/exceptions.py
@@ -0,0 +1,31 @@
+class SASLError(Exception):
+
+ def __init__(self, sasl, text, mech=None):
+ """
+ :param sasl: The main `suelta.SASL` object.
+ :param text: Descpription of the error.
+ :param mech: Optional reference to the mechanism object.
+
+ :type sasl: `suelta.SASL`
+ """
+ self.sasl = sasl
+ self.text = text
+ self.mech = mech
+
+ def __str__(self):
+ if self.mech is None:
+ return 'SASL Error: %s' % self.text
+ else:
+ return 'SASL Error (%s): %s' % (self.mech, self.text)
+
+
+class SASLCancelled(SASLError):
+
+ def __init__(self, sasl, mech=None):
+ """
+ :param sasl: The main `suelta.SASL` object.
+ :param mech: Optional reference to the mechanism object.
+
+ :type sasl: `suelta.SASL`
+ """
+ super(SASLCancelled, self).__init__(sasl, "User cancelled", mech)
diff --git a/sleekxmpp/thirdparty/suelta/mechanisms/__init__.py b/sleekxmpp/thirdparty/suelta/mechanisms/__init__.py
new file mode 100644
index 0000000..5cb2ee3
--- /dev/null
+++ b/sleekxmpp/thirdparty/suelta/mechanisms/__init__.py
@@ -0,0 +1,5 @@
+from sleekxmpp.thirdparty.suelta.mechanisms.anonymous import ANONYMOUS
+from sleekxmpp.thirdparty.suelta.mechanisms.plain import PLAIN
+from sleekxmpp.thirdparty.suelta.mechanisms.cram_md5 import CRAM_MD5
+from sleekxmpp.thirdparty.suelta.mechanisms.digest_md5 import DIGEST_MD5
+from sleekxmpp.thirdparty.suelta.mechanisms.scram_hmac import SCRAM_HMAC
diff --git a/sleekxmpp/thirdparty/suelta/mechanisms/anonymous.py b/sleekxmpp/thirdparty/suelta/mechanisms/anonymous.py
new file mode 100644
index 0000000..de89eef
--- /dev/null
+++ b/sleekxmpp/thirdparty/suelta/mechanisms/anonymous.py
@@ -0,0 +1,36 @@
+from sleekxmpp.thirdparty.suelta.sasl import Mechanism, register_mechanism
+from sleekxmpp.thirdparty.suelta.exceptions import SASLError, SASLCancelled
+
+
+class ANONYMOUS(Mechanism):
+
+ """
+ """
+
+ def __init__(self, sasl, name):
+ """
+ """
+ super(ANONYMOUS, self).__init__(self, sasl, name, 0)
+
+ def get_values(self):
+ """
+ """
+ return {}
+
+ def process(self, challenge=None):
+ """
+ """
+ return b'Anonymous, Suelta'
+
+ def okay(self):
+ """
+ """
+ return True
+
+ def get_user(self):
+ """
+ """
+ return 'anonymous'
+
+
+register_mechanism('ANONYMOUS', 0, ANONYMOUS, use_hashes=False)
diff --git a/sleekxmpp/thirdparty/suelta/mechanisms/cram_md5.py b/sleekxmpp/thirdparty/suelta/mechanisms/cram_md5.py
new file mode 100644
index 0000000..ba44bef
--- /dev/null
+++ b/sleekxmpp/thirdparty/suelta/mechanisms/cram_md5.py
@@ -0,0 +1,63 @@
+import sys
+import hmac
+
+from sleekxmpp.thirdparty.suelta.util import hash, bytes
+from sleekxmpp.thirdparty.suelta.sasl import Mechanism, register_mechanism
+from sleekxmpp.thirdparty.suelta.exceptions import SASLError, SASLCancelled
+
+
+class CRAM_MD5(Mechanism):
+
+ """
+ """
+
+ def __init__(self, sasl, name):
+ """
+ """
+ super(CRAM_MD5, self).__init__(sasl, name, 2)
+
+ self.hash = hash(name[5:])
+ if self.hash is None:
+ raise SASLCancelled(self.sasl, self)
+ if not self.sasl.tls_active():
+ if not self.sasl.sec_query(self, 'CRAM-MD5'):
+ raise SASLCancelled(self.sasl, self)
+
+ def prep(self):
+ """
+ """
+ if 'savepass' not in self.values:
+ if self.sasl.sec_query(self, 'CLEAR-PASSWORD'):
+ self.values['savepass'] = True
+
+ if 'savepass' not in self.values:
+ del self.values['password']
+
+ def process(self, challenge):
+ """
+ """
+ if challenge is None:
+ return None
+
+ self.check_values(['username', 'password'])
+ username = bytes(self.values['username'])
+ password = bytes(self.values['password'])
+
+ mac = hmac.HMAC(key=password, digestmod=self.hash)
+
+ mac.update(challenge)
+
+ return username + b' ' + bytes(mac.hexdigest())
+
+ def okay(self):
+ """
+ """
+ return True
+
+ def get_user(self):
+ """
+ """
+ return self.values['username']
+
+
+register_mechanism('CRAM-', 20, CRAM_MD5)
diff --git a/sleekxmpp/thirdparty/suelta/mechanisms/digest_md5.py b/sleekxmpp/thirdparty/suelta/mechanisms/digest_md5.py
new file mode 100644
index 0000000..5492c55
--- /dev/null
+++ b/sleekxmpp/thirdparty/suelta/mechanisms/digest_md5.py
@@ -0,0 +1,273 @@
+import sys
+
+import random
+
+from sleekxmpp.thirdparty.suelta.util import hash, bytes, quote
+from sleekxmpp.thirdparty.suelta.sasl import Mechanism, register_mechanism
+from sleekxmpp.thirdparty.suelta.exceptions import SASLError, SASLCancelled
+
+
+
+def parse_challenge(stuff):
+ """
+ """
+ ret = {}
+ var = b''
+ val = b''
+ in_var = True
+ in_quotes = False
+ new = False
+ escaped = False
+ for c in stuff:
+ if sys.version_info >= (3, 0):
+ c = bytes([c])
+ if in_var:
+ if c.isspace():
+ continue
+ if c == b'=':
+ in_var = False
+ new = True
+ else:
+ var += c
+ else:
+ if new:
+ if c == b'"':
+ in_quotes = True
+ else:
+ val += c
+ new = False
+ elif in_quotes:
+ if escaped:
+ escaped = False
+ val += c
+ else:
+ if c == b'\\':
+ escaped = True
+ elif c == b'"':
+ in_quotes = False
+ else:
+ val += c
+ else:
+ if c == b',':
+ if var:
+ ret[var] = val
+ var = b''
+ val = b''
+ in_var = True
+ else:
+ val += c
+ if var:
+ ret[var] = val
+ return ret
+
+
+class DIGEST_MD5(Mechanism):
+
+ """
+ """
+
+ enc_magic = 'Digest session key to client-to-server signing key magic'
+ dec_magic = 'Digest session key to server-to-client signing key magic'
+
+ def __init__(self, sasl, name):
+ """
+ """
+ super(DIGEST_MD5, self).__init__(sasl, name, 3)
+
+ self.hash = hash(name[7:])
+ if self.hash is None:
+ raise SASLCancelled(self.sasl, self)
+
+ if not self.sasl.tls_active():
+ if not self.sasl.sec_query(self, '-ENCRYPTION, DIGEST-MD5'):
+ raise SASLCancelled(self.sasl, self)
+
+ self._rspauth_okay = False
+ self._digest_uri = None
+ self._a1 = None
+ self._enc_buf = b''
+ self._enc_key = None
+ self._enc_seq = 0
+ self._max_buffer = 65536
+ self._dec_buf = b''
+ self._dec_key = None
+ self._dec_seq = 0
+ self._qops = [b'auth']
+ self._qop = b'auth'
+
+ def MAC(self, seq, msg, key):
+ """
+ """
+ mac = hmac.HMAC(key=key, digestmod=self.hash)
+ seqnum = num_to_bytes(seq)
+ mac.update(seqnum)
+ mac.update(msg)
+ return mac.digest()[:10] + b'\x00\x01' + seqnum
+
+
+ def encode(self, text):
+ """
+ """
+ self._enc_buf += text
+
+ def flush(self):
+ """
+ """
+ result = b''
+ # Leave buffer space for the MAC
+ mbuf = self._max_buffer - 10 - 2 - 4
+
+ while self._enc_buf:
+ msg = self._encbuf[:mbuf]
+ mac = self.MAC(self._enc_seq, msg, self._enc_key, self.hash)
+ self._enc_seq += 1
+ msg += mac
+ result += num_to_bytes(len(msg)) + msg
+ self._enc_buf = self._enc_buf[mbuf:]
+
+ return result
+
+ def decode(self, text):
+ """
+ """
+ self._dec_buf += text
+ result = b''
+
+ while len(self._dec_buf) > 4:
+ num = bytes_to_num(self._dec_buf)
+ if len(self._dec_buf) < (num + 4):
+ return result
+
+ mac = self._dec_buf[4:4 + num]
+ self._dec_buf = self._dec_buf[4 + num:]
+ msg = mac[:-16]
+
+ mac_conf = self.MAC(self._dec_mac, msg, self._dec_key)
+ if mac[-16:] != mac_conf:
+ self._desc_sec = None
+ return result
+
+ self._dec_seq += 1
+ result += msg
+
+ return result
+
+ def response(self):
+ """
+ """
+ vitals = ['username']
+ if not self.has_values(['key_hash']):
+ vitals.append('password')
+ self.check_values(vitals)
+
+ resp = {}
+ if 'auth-int' in self._qops:
+ self._qop = b'auth-int'
+ resp['qop'] = self._qop
+ if 'realm' in self.values:
+ resp['realm'] = quote(self.values['realm'])
+
+ resp['username'] = quote(bytes(self.values['username']))
+ resp['nonce'] = quote(self.values['nonce'])
+ if self.values['nc']:
+ self._cnonce = self.values['cnonce']
+ else:
+ self._cnonce = bytes('%s' % random.random())[2:]
+ resp['cnonce'] = quote(self._cnonce)
+ self.values['nc'] += 1
+ resp['nc'] = bytes('%08x' % self.values['nc'])
+
+ service = bytes(self.sasl.service)
+ host = bytes(self.sasl.host)
+ self._digest_uri = service + b'/' + host
+ resp['digest-uri'] = quote(self._digest_uri)
+
+ a2 = b'AUTHENTICATE:' + self._digest_uri
+ if self._qop != b'auth':
+ a2 += b':00000000000000000000000000000000'
+ resp['maxbuf'] = b'16777215' # 2**24-1
+ resp['response'] = self.gen_hash(a2)
+ return b','.join([bytes(k) + b'=' + bytes(v) for k, v in resp.items()])
+
+ def gen_hash(self, a2):
+ """
+ """
+ if not self.has_values(['key_hash']):
+ key_hash = self.hash()
+ user = bytes(self.values['username'])
+ password = bytes(self.values['password'])
+ realm = bytes(self.values['realm'])
+ kh = user + b':' + realm + b':' + password
+ key_hash.update(kh)
+ self.values['key_hash'] = key_hash.digest()
+
+ a1 = self.hash(self.values['key_hash'])
+ a1h = b':' + self.values['nonce'] + b':' + self._cnonce
+ a1.update(a1h)
+ response = self.hash()
+ self._a1 = a1.digest()
+ rv = bytes(a1.hexdigest().lower())
+ rv += b':' + self.values['nonce']
+ rv += b':' + bytes('%08x' % self.values['nc'])
+ rv += b':' + self._cnonce
+ rv += b':' + self._qop
+ rv += b':' + bytes(self.hash(a2).hexdigest().lower())
+ response.update(rv)
+ return bytes(response.hexdigest().lower())
+
+ def mutual_auth(self, cmp_hash):
+ """
+ """
+ a2 = b':' + self._digest_uri
+ if self._qop != b'auth':
+ a2 += b':00000000000000000000000000000000'
+ if self.gen_hash(a2) == cmp_hash:
+ self._rspauth_okay = True
+
+ def prep(self):
+ """
+ """
+ if 'password' in self.values:
+ del self.values['password']
+ self.values['cnonce'] = self._cnonce
+
+ def process(self, challenge=None):
+ """
+ """
+ if challenge is None:
+ if self.has_values(['username', 'realm', 'nonce', 'key_hash',
+ 'nc', 'cnonce', 'qops']):
+ self._qops = self.values['qops']
+ return self.response()
+ else:
+ return None
+
+ d = parse_challenge(challenge)
+ if b'rspauth' in d:
+ self.mutual_auth(d[b'rspauth'])
+ else:
+ if b'realm' not in d:
+ d[b'realm'] = self.sasl.def_realm
+ for key in ['nonce', 'realm']:
+ if bytes(key) in d:
+ self.values[key] = d[bytes(key)]
+ self.values['nc'] = 0
+ self._qops = [b'auth']
+ if b'qop' in d:
+ self._qops = [x.strip() for x in d[b'qop'].split(b',')]
+ self.values['qops'] = self._qops
+ if b'maxbuf' in d:
+ self._max_buffer = int(d[b'maxbuf'])
+ return self.response()
+
+ def okay(self):
+ """
+ """
+ if self._rspauth_okay and self._qop == b'auth-int':
+ self._enc_key = self.hash(self._a1 + self.enc_magic).digest()
+ self._dec_key = self.hash(self._a1 + self.dec_magic).digest()
+ self.encoding = True
+ return self._rspauth_okay
+
+
+register_mechanism('DIGEST-', 30, DIGEST_MD5)
diff --git a/sleekxmpp/thirdparty/suelta/mechanisms/plain.py b/sleekxmpp/thirdparty/suelta/mechanisms/plain.py
new file mode 100644
index 0000000..ab17095
--- /dev/null
+++ b/sleekxmpp/thirdparty/suelta/mechanisms/plain.py
@@ -0,0 +1,61 @@
+import sys
+
+from sleekxmpp.thirdparty.suelta.util import bytes
+from sleekxmpp.thirdparty.suelta.sasl import Mechanism, register_mechanism
+from sleekxmpp.thirdparty.suelta.exceptions import SASLError, SASLCancelled
+
+
+class PLAIN(Mechanism):
+
+ """
+ """
+
+ def __init__(self, sasl, name):
+ """
+ """
+ super(PLAIN, self).__init__(sasl, name)
+
+ if not self.sasl.tls_active():
+ if not self.sasl.sec_query(self, '-ENCRYPTION, PLAIN'):
+ raise SASLCancelled(self.sasl, self)
+ else:
+ if not self.sasl.sec_query(self, '+ENCRYPTION, PLAIN'):
+ raise SASLCancelled(self.sasl, self)
+
+ self.check_values(['username', 'password'])
+
+ def prep(self):
+ """
+ Prepare for processing by deleting the password if
+ the user has not approved storing it in the clear.
+ """
+ if 'savepass' not in self.values:
+ if self.sasl.sec_query(self, 'CLEAR-PASSWORD'):
+ self.values['savepass'] = True
+
+ if 'savepass' not in self.values:
+ del self.values['password']
+
+ return True
+
+ def process(self, challenge=None):
+ """
+ Process a challenge request and return the response.
+
+ :param challenge: A challenge issued by the server that
+ must be answered for authentication.
+ """
+ user = bytes(self.values['username'])
+ password = bytes(self.values['password'])
+ return b'\x00' + user + b'\x00' + password
+
+ def okay(self):
+ """
+ Mutual authentication is not supported by PLAIN.
+
+ :returns: ``True``
+ """
+ return True
+
+
+register_mechanism('PLAIN', 1, PLAIN, use_hashes=False)
diff --git a/sleekxmpp/thirdparty/suelta/mechanisms/scram_hmac.py b/sleekxmpp/thirdparty/suelta/mechanisms/scram_hmac.py
new file mode 100644
index 0000000..e002032
--- /dev/null
+++ b/sleekxmpp/thirdparty/suelta/mechanisms/scram_hmac.py
@@ -0,0 +1,176 @@
+import sys
+import hmac
+import random
+from base64 import b64encode, b64decode
+
+from sleekxmpp.thirdparty.suelta.util import hash, bytes, num_to_bytes, bytes_to_num, XOR
+from sleekxmpp.thirdparty.suelta.sasl import Mechanism, register_mechanism
+from sleekxmpp.thirdparty.suelta.exceptions import SASLError, SASLCancelled
+
+
+def parse_challenge(challenge):
+ """
+ """
+ items = {}
+ for key, value in [item.split(b'=', 1) for item in challenge.split(b',')]:
+ items[key] = value
+ return items
+
+
+class SCRAM_HMAC(Mechanism):
+
+ """
+ """
+
+ def __init__(self, sasl, name):
+ """
+ """
+ super(SCRAM_HMAC, self).__init__(sasl, name, 0)
+
+ self._cb = False
+ if name[-5:] == '-PLUS':
+ name = name[:-5]
+ self._cb = True
+
+ self.hash = hash(self.name[6:])
+ if self.hash is None:
+ raise SASLCancelled(self.sasl, self)
+ if not self.sasl.tls_active():
+ if not self.sasl.sec_query(self, '-ENCRYPTION, SCRAM'):
+ raise SASLCancelled(self.sasl, self)
+
+ self._step = 0
+ self._rspauth = False
+
+ def HMAC(self, key, msg):
+ """
+ """
+ return hmac.HMAC(key=key, msg=msg, digestmod=self.hash).digest()
+
+ def Hi(self, text, salt, iterations):
+ """
+ """
+ text = bytes(text)
+ ui_1 = self.HMAC(text, salt + b'\0\0\0\01')
+ ui = ui_1
+ for i in range(iterations - 1):
+ ui_1 = self.HMAC(text, ui_1)
+ ui = XOR(ui, ui_1)
+ return ui
+
+ def H(self, text):
+ """
+ """
+ return self.hash(text).digest()
+
+ def prep(self):
+ if 'password' in self.values:
+ del self.values['password']
+
+ def process(self, challenge=None):
+ """
+ """
+ steps = {
+ 0: self.process_one,
+ 1: self.process_two,
+ 2: self.process_three
+ }
+ return steps[self._step](challenge)
+
+ def process_one(self, challenge):
+ """
+ """
+ vitals = ['username']
+ if 'SaltedPassword' not in self.values:
+ vitals.append('password')
+ if 'Iterations' not in self.values:
+ vitals.append('password')
+
+ self.check_values(vitals)
+
+ username = bytes(self.values['username'])
+
+ self._step = 1
+ self._cnonce = bytes(('%s' % random.random())[2:])
+ self._soup = b'n=' + username + b',r=' + self._cnonce
+ self._gs2header = b''
+
+ if not self.sasl.tls_active():
+ if self._cb:
+ self._gs2header = b'p=tls-unique,,'
+ else:
+ self._gs2header = b'y,,'
+ else:
+ self._gs2header = b'n,,'
+
+ return self._gs2header + self._soup
+
+ def process_two(self, challenge):
+ """
+ """
+ data = parse_challenge(challenge)
+
+ self._step = 2
+ self._soup += b',' + challenge + b','
+ self._nonce = data[b'r']
+ self._salt = b64decode(data[b's'])
+ self._iter = int(data[b'i'])
+
+ if self._nonce[:len(self._cnonce)] != self._cnonce:
+ raise SASLCancelled(self.sasl, self)
+
+ cbdata = self.sasl.tls_active()
+ c = self._gs2header
+ if not cbdata and self._cb:
+ c += None
+
+ r = b'c=' + b64encode(c).replace(b'\n', b'')
+ r += b',r=' + self._nonce
+ self._soup += r
+
+ if 'Iterations' in self.values:
+ if self.values['Iterations'] != self._iter:
+ if 'SaltedPassword' in self.values:
+ del self.values['SaltedPassword']
+ if 'Salt' in self.values:
+ if self.values['Salt'] != self._salt:
+ if 'SaltedPassword' in self.values:
+ del self.values['SaltedPassword']
+
+ self.values['Iterations'] = self._iter
+ self.values['Salt'] = self._salt
+
+ if 'SaltedPassword' not in self.values:
+ self.check_values(['password'])
+ password = bytes(self.values['password'])
+ salted_pass = self.Hi(password, self._salt, self._iter)
+ self.values['SaltedPassword'] = salted_pass
+
+ salted_pass = self.values['SaltedPassword']
+ client_key = self.HMAC(salted_pass, b'Client Key')
+ stored_key = self.H(client_key)
+ client_sig = self.HMAC(stored_key, self._soup)
+ client_proof = XOR(client_key, client_sig)
+ r += b',p=' + b64encode(client_proof).replace(b'\n', b'')
+ server_key = self.HMAC(self.values['SaltedPassword'], b'Server Key')
+ self.server_sig = self.HMAC(server_key, self._soup)
+ return r
+
+ def process_three(self, challenge=None):
+ """
+ """
+ data = parse_challenge(challenge)
+ if b64decode(data[b'v']) == self.server_sig:
+ self._rspauth = True
+
+ def okay(self):
+ """
+ """
+ return self._rspauth
+
+ def get_user(self):
+ return self.values['username']
+
+
+register_mechanism('SCRAM-', 60, SCRAM_HMAC)
+register_mechanism('SCRAM-', 70, SCRAM_HMAC, extra='-PLUS')
diff --git a/sleekxmpp/thirdparty/suelta/sasl.py b/sleekxmpp/thirdparty/suelta/sasl.py
new file mode 100644
index 0000000..ec7afe9
--- /dev/null
+++ b/sleekxmpp/thirdparty/suelta/sasl.py
@@ -0,0 +1,402 @@
+from sleekxmpp.thirdparty.suelta.util import hashes
+from sleekxmpp.thirdparty.suelta.saslprep import saslprep
+
+#: Global session storage for user answers to requested mechanism values
+#: and security questions. This allows the user's preferences to be
+#: persisted across multiple SASL authentication attempts made by the
+#: same process.
+SESSION = {'answers': {},
+ 'passwords': {},
+ 'sec_queries': {},
+ 'stash': {},
+ 'stash_file': ''}
+
+#: Global registry mapping mechanism names to implementation classes.
+MECHANISMS = {}
+
+#: Global registry mapping mechanism names to security scores.
+MECH_SEC_SCORES = {}
+
+
+def register_mechanism(basename, basescore, impl, extra=None, use_hashes=True):
+ """
+ Add a SASL mechanism to the registry of available mechanisms.
+
+ :param basename: The base name of the mechanism type, such as ``CRAM-``.
+ :param basescore: The base security score for this type of mechanism.
+ :param impl: The class implementing the mechanism.
+ :param extra: Any additional qualifiers to the mechanism name,
+ such as ``-PLUS``.
+ :param use_hashes: If ``True``, then register the mechanism for use with
+ all available hashes.
+ """
+ n = 0
+ if use_hashes:
+ for hashing_alg in hashes():
+ n += 1
+ name = basename + hashing_alg
+ if extra is not None:
+ name += extra
+ MECHANISMS[name] = impl
+ MECH_SEC_SCORES[name] = basescore + n
+ else:
+ MECHANISMS[basename] = impl
+ MECH_SEC_SCORES[basename] = basescore
+
+
+def set_stash_file(filename):
+ """
+ Enable or disable storing the stash to disk.
+
+ If the filename is ``None``, then disable using a stash file.
+
+ :param filename: The path to the file to store the stash data.
+ """
+ SESSION['stash_file'] = filename
+ try:
+ import marshal
+ stash_file = file(filename)
+ SESSION['stash'] = marshal.load(stash_file)
+ except:
+ SESSION['stash'] = {}
+
+
+def sec_query_allow(mech, query):
+ """
+ Quick default to allow all feature combinations which could
+ negatively affect security.
+
+ :param mech: The chosen SASL mechanism
+ :param query: An encoding of the combination of enabled and
+ disabled features which may affect security.
+
+ :returns: ``True``
+ """
+ return True
+
+
+class SASL(object):
+
+ """
+ """
+
+ def __init__(self, host, service, mech=None, username=None,
+ min_sec=0, request_values=None, sec_query=None,
+ tls_active=None, def_realm=None):
+ """
+ :param string host: The host of the service requiring authentication.
+ :param string service: The name of the underlying protocol in use.
+ :param string mech: Optional name of the SASL mechanism to use.
+ If given, only this mechanism may be used for
+ authentication.
+ :param string username: The username to use when authenticating.
+ :param request_values: Reference to a function for supplying
+ values requested by mechanisms, such
+ as passwords. (See above)
+ :param sec_query: Reference to a function for approving or
+ denying feature combinations which could
+ negatively impact security. (See above)
+ :param tls_active: Function for indicating if TLS has been
+ negotiated. (See above)
+ :param integer min_sec: The minimum security level accepted. This
+ only allows for SASL mechanisms whose
+ security rating is greater than `min_sec`.
+ :param string def_realm: The default realm, if different than `host`.
+
+ :type request_values: :func:`request_values`
+ :type sec_query: :func:`sec_query`
+ :type tls_active: :func:`tls_active`
+ """
+ self.host = host
+ self.def_realm = def_realm or host
+ self.service = service
+ self.user = username
+ self.mech = mech
+ self.min_sec = min_sec - 1
+
+ self.request_values = request_values
+ self._sec_query = sec_query
+ if tls_active is not None:
+ self.tls_active = tls_active
+ else:
+ self.tls_active = lambda: False
+
+ self.try_username = self.user
+ self.try_password = None
+
+ self.stash_id = None
+ self.testkey = None
+
+ def reset_stash_id(self, username):
+ """
+ Reset the ID for the stash for persisting user data.
+
+ :param username: The username to base the new ID on.
+ """
+ username = saslprep(username)
+ self.user = username
+ self.try_username = self.user
+ self.testkey = [self.user, self.host, self.service]
+ self.stash_id = '\0'.join(self.testkey)
+
+ def sec_query(self, mech, query):
+ """
+ Request authorization from the user to use a combination
+ of features which could negatively affect security.
+
+ The ``sec_query`` callback when creating the SASL object will
+ be called if the query has not been answered before. Otherwise,
+ the query response will be pulled from ``SESSION['sec_queries']``.
+
+ If no ``sec_query`` callback was provided, then all queries
+ will be denied.
+
+ :param mech: The chosen SASL mechanism
+ :param query: An encoding of the combination of enabled and
+ disabled features which may affect security.
+ :rtype: bool
+ """
+ if self._sec_query is None:
+ return False
+ if query in SESSION['sec_queries']:
+ return SESSION['sec_queries'][query]
+ resp = self._sec_query(mech, query)
+ if resp:
+ SESSION['sec_queries'][query] = resp
+
+ return resp
+
+ def find_password(self, mech):
+ """
+ Find and return the user's password, if it has been entered before
+ during this session.
+
+ :param mech: The chosen SASL mechanism.
+ """
+ if self.try_password is not None:
+ return self.try_password
+ if self.testkey is None:
+ return
+
+ testkey = self.testkey[:]
+ lockout = 1
+
+ def find_username(self):
+ """Find and return user's username if known."""
+ return self.try_username
+
+ def success(self, mech):
+ mech.preprep()
+ if 'password' in mech.values:
+ testkey = self.testkey[:]
+ while len(testkey):
+ tk = '\0'.join(testkey)
+ if tk in SESSION['passwords']:
+ break
+ SESSION['passwords'][tk] = mech.values['password']
+ testkey = testkey[:-1]
+ mech.prep()
+ mech.save_values()
+
+ def failure(self, mech):
+ mech.clear()
+ self.testkey = self.testkey[:-1]
+
+ def choose_mechanism(self, mechs, force_plain=False):
+ """
+ Choose the most secure mechanism from a list of mechanisms.
+
+ If ``force_plain`` is given, return the ``PLAIN`` mechanism.
+
+ :param mechs: A list of mechanism names.
+ :param force_plain: If ``True``, force the selection of the
+ ``PLAIN`` mechanism.
+ :returns: A SASL mechanism object, or ``None`` if no mechanism
+ could be selected.
+ """
+ # Handle selection of PLAIN and ANONYMOUS
+ if force_plain:
+ return MECHANISMS['PLAIN'](self, 'PLAIN')
+
+ if self.user is not None:
+ requested_mech = '*' if self.mech is None else self.mech
+ else:
+ if self.mech is None:
+ requested_mech = 'ANONYMOUS'
+ else:
+ requested_mech = self.mech
+ if requested_mech == '*' and self.user == 'anonymous':
+ requested_mech = 'ANONYMOUS'
+
+ # If a specific mechanism was requested, try it
+ if requested_mech != '*':
+ if requested_mech in MECHANISMS and \
+ requested_mech in MECH_SEC_SCORES:
+ return MECHANISMS[requested_mech](self, requested_mech)
+ return None
+
+ # Pick the best mechanism based on its security score
+ best_score = self.min_sec
+ best_mech = None
+ for name in mechs:
+ if name in MECH_SEC_SCORES:
+ if MECH_SEC_SCORES[name] > best_score:
+ best_score = MECH_SEC_SCORES[name]
+ best_mech = name
+ if best_mech != None:
+ best_mech = MECHANISMS[best_mech](self, best_mech)
+
+ return best_mech
+
+
+class Mechanism(object):
+
+ """
+ """
+
+ def __init__(self, sasl, name, version=0, use_stash=True):
+ self.name = name
+ self.sasl = sasl
+ self.use_stash = use_stash
+
+ self.encoding = False
+ self.values = {}
+
+ if use_stash:
+ self.load_values()
+
+ def load_values(self):
+ """Retrieve user data from the stash."""
+ self.values = {}
+ if not self.use_stash:
+ return False
+ if self.sasl.stash_id is not None:
+ if self.sasl.stash_id in SESSION['stash']:
+ if SESSION['stash'][self.sasl.stash_id]['mech'] == self.name:
+ values = SESSION['stash'][self.sasl.stash_id]['values']
+ self.values.update(values)
+ if self.sasl.user is not None:
+ if not self.has_values(['username']):
+ self.values['username'] = self.sasl.user
+ return None
+
+ def save_values(self):
+ """
+ Save user data to the session stash.
+
+ If a stash file name has been set using ``SESSION['stash_file']``,
+ the saved values will be persisted to disk.
+ """
+ if not self.use_stash:
+ return False
+ if self.sasl.stash_id is not None:
+ if self.sasl.stash_id not in SESSION['stash']:
+ SESSION['stash'][self.sasl.stash_id] = {}
+ SESSION['stash'][self.sasl.stash_id]['values'] = self.values
+ SESSION['stash'][self.sasl.stash_id]['mech'] = self.name
+ if SESSION['stash_file'] not in ['', None]:
+ import marshal
+ stash_file = file(SESSION['stash_file'], 'wb')
+ marshal.dump(SESSION['stash'], stash_file)
+
+ def clear(self):
+ """Reset all user data, except the username."""
+ username = None
+ if 'username' in self.values:
+ username = self.values['username']
+ self.values = {}
+ if username is not None:
+ self.values['username'] = username
+ self.save_values()
+ self.values = {}
+ self.load_values()
+
+ def okay(self):
+ """
+ Indicate if mutual authentication has completed successfully.
+
+ :rtype: bool
+ """
+ return False
+
+ def preprep(self):
+ """Ensure that the stash ID has been set before processing."""
+ if self.sasl.stash_id is None:
+ if 'username' in self.values:
+ self.sasl.reset_stash_id(self.values['username'])
+
+ def prep(self):
+ """
+ Prepare stored values for processing.
+
+ For example, by removing extra copies of passwords from memory.
+ """
+ pass
+
+ def process(self, challenge=None):
+ """
+ Process a challenge request and return the response.
+
+ :param challenge: A challenge issued by the server that
+ must be answered for authentication.
+ """
+ raise NotImplemented
+
+ def fulfill(self, values):
+ """
+ Provide requested values to the mechanism.
+
+ :param values: A dictionary of requested values.
+ """
+ if 'password' in values:
+ values['password'] = saslprep(values['password'])
+ self.values.update(values)
+
+ def missing_values(self, keys):
+ """
+ Return a dictionary of value names that have not been given values
+ by the user, or retrieved from the stash.
+
+ :param keys: A list of value names to check.
+ :rtype: dict
+ """
+ vals = {}
+ for name in keys:
+ if name not in self.values or self.values[name] is None:
+ if self.use_stash:
+ if name == 'username':
+ value = self.sasl.find_username()
+ if value is not None:
+ self.sasl.reset_stash_id(value)
+ self.values[name] = value
+ break
+ if name == 'password':
+ value = self.sasl.find_password(self)
+ if value is not None:
+ self.values[name] = value
+ break
+ vals[name] = None
+ return vals
+
+ def has_values(self, keys):
+ """
+ Check that the given values have been retrieved from the user,
+ or from the stash.
+
+ :param keys: A list of value names to check.
+ """
+ return len(self.missing_values(keys)) == 0
+
+ def check_values(self, keys):
+ """
+ Request missing values from the user.
+
+ :param keys: A list of value names to request, if missing.
+ """
+ vals = self.missing_values(keys)
+ if vals:
+ self.sasl.request_values(self, vals)
+
+ def get_user(self):
+ """Return the username usd for this mechanism."""
+ return self.values['username']
diff --git a/sleekxmpp/thirdparty/suelta/saslprep.py b/sleekxmpp/thirdparty/suelta/saslprep.py
new file mode 100644
index 0000000..fe58d58
--- /dev/null
+++ b/sleekxmpp/thirdparty/suelta/saslprep.py
@@ -0,0 +1,78 @@
+from __future__ import unicode_literals
+
+import sys
+import stringprep
+import unicodedata
+
+
+def saslprep(text, strict=True):
+ """
+ Return a processed version of the given string, using the SASLPrep
+ profile of stringprep.
+
+ :param text: The string to process, in UTF-8.
+ :param strict: If ``True``, prevent the use of unassigned code points.
+ """
+
+ if sys.version_info < (3, 0):
+ if type(text) == str:
+ text = text.decode('us-ascii')
+
+ # Mapping:
+ #
+ # - non-ASCII space characters [StringPrep, C.1.2] that can be
+ # mapped to SPACE (U+0020), and
+ #
+ # - the 'commonly mapped to nothing' characters [StringPrep, B.1]
+ # that can be mapped to nothing.
+ buffer = ''
+ for char in text:
+ if stringprep.in_table_c12(char):
+ buffer += ' '
+ elif not stringprep.in_table_b1(char):
+ buffer += char
+
+ # Normalization using form KC
+ text = unicodedata.normalize('NFKC', buffer)
+
+ # Check for bidirectional string
+ buffer = ''
+ first_is_randal = False
+ if text:
+ first_is_randal = stringprep.in_table_d1(text[0])
+ if first_is_randal and not stringprep.in_table_d1(text[-1]):
+ raise UnicodeError('Section 6.3 [end]')
+
+ # Check for prohibited characters
+ for x in range(len(text)):
+ if strict and stringprep.in_table_a1(text[x]):
+ raise UnicodeError('Unassigned Codepoint')
+ if stringprep.in_table_c12(text[x]):
+ raise UnicodeError('In table C.1.2')
+ if stringprep.in_table_c21(text[x]):
+ raise UnicodeError('In table C.2.1')
+ if stringprep.in_table_c22(text[x]):
+ raise UnicodeError('In table C.2.2')
+ if stringprep.in_table_c3(text[x]):
+ raise UnicodeError('In table C.3')
+ if stringprep.in_table_c4(text[x]):
+ raise UnicodeError('In table C.4')
+ if stringprep.in_table_c5(text[x]):
+ raise UnicodeError('In table C.5')
+ if stringprep.in_table_c6(text[x]):
+ raise UnicodeError('In table C.6')
+ if stringprep.in_table_c7(text[x]):
+ raise UnicodeError('In table C.7')
+ if stringprep.in_table_c8(text[x]):
+ raise UnicodeError('In table C.8')
+ if stringprep.in_table_c9(text[x]):
+ raise UnicodeError('In table C.9')
+ if x:
+ if first_is_randal and stringprep.in_table_d2(text[x]):
+ raise UnicodeError('Section 6.2')
+ if not first_is_randal and \
+ x != len(text) - 1 and \
+ stringprep.in_table_d1(text[x]):
+ raise UnicodeError('Section 6.3')
+
+ return text
diff --git a/sleekxmpp/thirdparty/suelta/util.py b/sleekxmpp/thirdparty/suelta/util.py
new file mode 100644
index 0000000..7d822a8
--- /dev/null
+++ b/sleekxmpp/thirdparty/suelta/util.py
@@ -0,0 +1,118 @@
+"""
+"""
+
+import sys
+import hashlib
+
+
+def bytes(text):
+ """
+ Convert Unicode text to UTF-8 encoded bytes.
+
+ Since Python 2.6+ and Python 3+ have similar but incompatible
+ signatures, this function unifies the two to keep code sane.
+
+ :param text: Unicode text to convert to bytes
+ :rtype: bytes (Python3), str (Python2.6+)
+ """
+ if sys.version_info < (3, 0):
+ import __builtin__
+ return __builtin__.bytes(text)
+ else:
+ import builtins
+ if isinstance(text, builtins.bytes):
+ # We already have bytes, so do nothing
+ return text
+ if isinstance(text, list):
+ # Convert a list of integers to bytes
+ return builtins.bytes(text)
+ else:
+ # Convert UTF-8 text to bytes
+ return builtins.bytes(text, encoding='utf-8')
+
+
+def quote(text):
+ """
+ Enclose in quotes and escape internal slashes and double quotes.
+
+ :param text: A Unicode or byte string.
+ """
+ text = bytes(text)
+ return b'"' + text.replace(b'\\', b'\\\\').replace(b'"', b'\\"') + b'"'
+
+
+def num_to_bytes(num):
+ """
+ Convert an integer into a four byte sequence.
+
+ :param integer num: An integer to convert to its byte representation.
+ """
+ bval = b''
+ bval += bytes(chr(0xFF & (num >> 24)))
+ bval += bytes(chr(0xFF & (num >> 16)))
+ bval += bytes(chr(0xFF & (num >> 8)))
+ bval += bytes(chr(0xFF & (num >> 0)))
+ return bval
+
+
+def bytes_to_num(bval):
+ """
+ Convert a four byte sequence to an integer.
+
+ :param bytes bval: A four byte sequence to turn into an integer.
+ """
+ num = 0
+ num += ord(bval[0] << 24)
+ num += ord(bval[1] << 16)
+ num += ord(bval[2] << 8)
+ num += ord(bval[3])
+ return num
+
+
+def XOR(x, y):
+ """
+ Return the results of an XOR operation on two equal length byte strings.
+
+ :param bytes x: A byte string
+ :param bytes y: A byte string
+ :rtype: bytes
+ """
+ result = b''
+ for a, b in zip(x, y):
+ if sys.version_info < (3, 0):
+ result += chr((ord(a) ^ ord(b)))
+ else:
+ result += bytes([a ^ b])
+ return result
+
+
+def hash(name):
+ """
+ Return a hash function implementing the given algorithm.
+
+ :param name: The name of the hashing algorithm to use.
+ :type name: string
+
+ :rtype: function
+ """
+ name = name.lower()
+ if name.startswith('sha-'):
+ name = 'sha' + name[4:]
+ if name in dir(hashlib):
+ return getattr(hashlib, name)
+ return None
+
+
+def hashes():
+ """
+ Return a list of available hashing algorithms.
+
+ :rtype: list of strings
+ """
+ t = []
+ if 'md5' in dir(hashlib):
+ t = ['MD5']
+ if 'md2' in dir(hashlib):
+ t += ['MD2']
+ hashes = ['SHA-' + h[3:] for h in dir(hashlib) if h.startswith('sha')]
+ return t + hashes
diff --git a/sleekxmpp/xmlstream/stanzabase.py b/sleekxmpp/xmlstream/stanzabase.py
index d9a4636..f1a9e1f 100644
--- a/sleekxmpp/xmlstream/stanzabase.py
+++ b/sleekxmpp/xmlstream/stanzabase.py
@@ -1064,7 +1064,9 @@ class ElementBase(object):
Defaults to True.
"""
stanza_ns = '' if top_level_ns else self.namespace
- return tostring(self.xml, xmlns='', stanza_ns=stanza_ns)
+ return tostring(self.xml, xmlns='',
+ stanza_ns=stanza_ns,
+ top_level = not top_level_ns)
def __repr__(self):
"""
@@ -1282,7 +1284,8 @@ class StanzaBase(ElementBase):
stanza_ns = '' if top_level_ns else self.namespace
return tostring(self.xml, xmlns='',
stanza_ns=stanza_ns,
- stream=self.stream)
+ stream=self.stream,
+ top_level = not top_level_ns)
# To comply with PEP8, method names now use underscores.
diff --git a/sleekxmpp/xmlstream/tostring/tostring.py b/sleekxmpp/xmlstream/tostring/tostring.py
index 38b08d8..a6bb6eb 100644
--- a/sleekxmpp/xmlstream/tostring/tostring.py
+++ b/sleekxmpp/xmlstream/tostring/tostring.py
@@ -7,7 +7,8 @@
"""
-def tostring(xml=None, xmlns='', stanza_ns='', stream=None, outbuffer=''):
+def tostring(xml=None, xmlns='', stanza_ns='', stream=None,
+ outbuffer='', top_level=False):
"""
Serialize an XML object to a Unicode string.
@@ -26,6 +27,8 @@ def tostring(xml=None, xmlns='', stanza_ns='', stream=None, outbuffer=''):
stream -- The XML stream that generated the XML object.
outbuffer -- Optional buffer for storing serializations during
recursive calls.
+ top_level -- Indicates that the element is the outermost
+ element.
"""
# Add previous results to the start of the output.
output = [outbuffer]
@@ -39,14 +42,21 @@ def tostring(xml=None, xmlns='', stanza_ns='', stream=None, outbuffer=''):
else:
tag_xmlns = ''
+ default_ns = ''
+ stream_ns = ''
+ if stream:
+ default_ns = stream.default_ns
+ stream_ns = stream.stream_ns
+
# Output the tag name and derived namespace of the element.
namespace = ''
- if tag_xmlns not in ['', xmlns, stanza_ns]:
+ if top_level and tag_xmlns not in ['', default_ns, stream_ns] or \
+ tag_xmlns not in ['', xmlns, stanza_ns, stream_ns]:
namespace = ' xmlns="%s"' % tag_xmlns
- if stream and tag_xmlns in stream.namespace_map:
- mapped_namespace = stream.namespace_map[tag_xmlns]
- if mapped_namespace:
- tag_name = "%s:%s" % (mapped_namespace, tag_name)
+ if stream and tag_xmlns in stream.namespace_map:
+ mapped_namespace = stream.namespace_map[tag_xmlns]
+ if mapped_namespace:
+ tag_name = "%s:%s" % (mapped_namespace, tag_name)
output.append("<%s" % tag_name)
output.append(namespace)
diff --git a/sleekxmpp/xmlstream/tostring/tostring26.py b/sleekxmpp/xmlstream/tostring/tostring26.py
index 1150178..3d1ca3d 100644
--- a/sleekxmpp/xmlstream/tostring/tostring26.py
+++ b/sleekxmpp/xmlstream/tostring/tostring26.py
@@ -10,7 +10,8 @@ from __future__ import unicode_literals
import types
-def tostring(xml=None, xmlns='', stanza_ns='', stream=None, outbuffer=''):
+def tostring(xml=None, xmlns='', stanza_ns='', stream=None,
+ outbuffer='', top_level=False):
"""
Serialize an XML object to a Unicode string.
@@ -29,6 +30,8 @@ def tostring(xml=None, xmlns='', stanza_ns='', stream=None, outbuffer=''):
stream -- The XML stream that generated the XML object.
outbuffer -- Optional buffer for storing serializations during
recursive calls.
+ top_level -- Indicates that the element is the outermost
+ element.
"""
# Add previous results to the start of the output.
output = [outbuffer]
@@ -42,14 +45,21 @@ def tostring(xml=None, xmlns='', stanza_ns='', stream=None, outbuffer=''):
else:
tag_xmlns = u''
+ default_ns = ''
+ stream_ns = ''
+ if stream:
+ default_ns = stream.default_ns
+ stream_ns = stream.stream_ns
+
# Output the tag name and derived namespace of the element.
namespace = u''
- if tag_xmlns not in ['', xmlns, stanza_ns]:
+ if top_level and tag_xmlns not in ['', default_ns, stream_ns] or \
+ tag_xmlns not in ['', xmlns, stanza_ns, stream_ns]:
namespace = u' xmlns="%s"' % tag_xmlns
- if stream and tag_xmlns in stream.namespace_map:
- mapped_namespace = stream.namespace_map[tag_xmlns]
- if mapped_namespace:
- tag_name = u"%s:%s" % (mapped_namespace, tag_name)
+ if stream and tag_xmlns in stream.namespace_map:
+ mapped_namespace = stream.namespace_map[tag_xmlns]
+ if mapped_namespace:
+ tag_name = u"%s:%s" % (mapped_namespace, tag_name)
output.append(u"<%s" % tag_name)
output.append(namespace)
diff --git a/tests/test_tostring.py b/tests/test_tostring.py
index 638e613..e456d28 100644
--- a/tests/test_tostring.py
+++ b/tests/test_tostring.py
@@ -102,11 +102,13 @@ class TestToString(SleekTest):
"""
Test that stanza objects are serialized properly.
"""
+ self.stream_start()
+
utf8_message = '\xe0\xb2\xa0_\xe0\xb2\xa0'
if not hasattr(utf8_message, 'decode'):
# Python 3
utf8_message = bytes(utf8_message, encoding='utf-8')
- msg = Message()
+ msg = self.Message()
msg['body'] = utf8_message.decode('utf-8')
expected = '\xe0\xb2\xa0_\xe0\xb2\xa0'
result = msg.__str__()