summaryrefslogtreecommitdiff
path: root/sleekxmpp
diff options
context:
space:
mode:
Diffstat (limited to 'sleekxmpp')
-rw-r--r--sleekxmpp/clientxmpp.py23
-rw-r--r--sleekxmpp/features/__init__.py3
-rw-r--r--sleekxmpp/features/feature_mechanisms/mechanisms.py179
-rw-r--r--sleekxmpp/features/feature_mechanisms/stanza/auth.py3
-rw-r--r--sleekxmpp/features/feature_mechanisms/stanza/challenge.py3
-rw-r--r--sleekxmpp/features/feature_mechanisms/stanza/response.py3
-rw-r--r--sleekxmpp/features/feature_mechanisms/stanza/success.py18
-rw-r--r--sleekxmpp/features/feature_preapproval/__init__.py15
-rw-r--r--sleekxmpp/features/feature_preapproval/preapproval.py42
-rw-r--r--sleekxmpp/features/feature_preapproval/stanza.py17
-rw-r--r--sleekxmpp/plugins/__init__.py2
-rw-r--r--sleekxmpp/plugins/xep_0016/__init__.py16
-rw-r--r--sleekxmpp/plugins/xep_0016/privacy.py110
-rw-r--r--sleekxmpp/plugins/xep_0016/stanza.py103
-rw-r--r--sleekxmpp/plugins/xep_0047/stanza.py2
-rw-r--r--sleekxmpp/plugins/xep_0050/adhoc.py6
-rw-r--r--sleekxmpp/plugins/xep_0054/stanza.py3
-rw-r--r--sleekxmpp/plugins/xep_0084/stanza.py2
-rw-r--r--sleekxmpp/plugins/xep_0242.py21
-rw-r--r--sleekxmpp/plugins/xep_0258/stanza.py3
-rw-r--r--sleekxmpp/thirdparty/__init__.py2
-rw-r--r--sleekxmpp/thirdparty/suelta/LICENSE21
-rw-r--r--sleekxmpp/thirdparty/suelta/PLAYING-NICELY27
-rw-r--r--sleekxmpp/thirdparty/suelta/README8
-rw-r--r--sleekxmpp/thirdparty/suelta/__init__.py26
-rw-r--r--sleekxmpp/thirdparty/suelta/exceptions.py35
-rw-r--r--sleekxmpp/thirdparty/suelta/mechanisms/__init__.py8
-rw-r--r--sleekxmpp/thirdparty/suelta/mechanisms/anonymous.py36
-rw-r--r--sleekxmpp/thirdparty/suelta/mechanisms/cram_md5.py63
-rw-r--r--sleekxmpp/thirdparty/suelta/mechanisms/digest_md5.py275
-rw-r--r--sleekxmpp/thirdparty/suelta/mechanisms/facebook_platform.py43
-rw-r--r--sleekxmpp/thirdparty/suelta/mechanisms/google_token.py22
-rw-r--r--sleekxmpp/thirdparty/suelta/mechanisms/messenger_oauth2.py17
-rw-r--r--sleekxmpp/thirdparty/suelta/mechanisms/plain.py61
-rw-r--r--sleekxmpp/thirdparty/suelta/mechanisms/scram_hmac.py176
-rw-r--r--sleekxmpp/thirdparty/suelta/sasl.py402
-rw-r--r--sleekxmpp/thirdparty/suelta/saslprep.py81
-rw-r--r--sleekxmpp/util/__init__.py4
-rw-r--r--sleekxmpp/util/misc_ops.py (renamed from sleekxmpp/thirdparty/suelta/util.py)13
-rw-r--r--sleekxmpp/util/sasl/__init__.py15
-rw-r--r--sleekxmpp/util/sasl/client.py168
-rw-r--r--sleekxmpp/util/sasl/mechanisms.py531
-rw-r--r--sleekxmpp/util/stringprep_profiles.py12
-rw-r--r--sleekxmpp/xmlstream/xmlstream.py13
44 files changed, 1226 insertions, 1407 deletions
diff --git a/sleekxmpp/clientxmpp.py b/sleekxmpp/clientxmpp.py
index e3b434e9..9aa64256 100644
--- a/sleekxmpp/clientxmpp.py
+++ b/sleekxmpp/clientxmpp.py
@@ -113,9 +113,10 @@ class ClientXMPP(BaseXMPP):
self.register_plugin('feature_starttls')
self.register_plugin('feature_bind')
self.register_plugin('feature_session')
+ self.register_plugin('feature_rosterver')
+ self.register_plugin('feature_preapproval')
self.register_plugin('feature_mechanisms',
pconfig={'use_mech': sasl_mech} if sasl_mech else None)
- self.register_plugin('feature_rosterver')
@property
def password(self):
@@ -286,15 +287,17 @@ class ClientXMPP(BaseXMPP):
if iq['roster']['ver']:
roster.version = iq['roster']['ver']
items = iq['roster']['items']
- for jid in items:
- item = items[jid]
- roster[jid]['name'] = item['name']
- roster[jid]['groups'] = item['groups']
- roster[jid]['from'] = item['subscription'] in ['from', 'both']
- roster[jid]['to'] = item['subscription'] in ['to', 'both']
- roster[jid]['pending_out'] = (item['ask'] == 'subscribe')
-
- roster[jid].save(remove=(item['subscription'] == 'remove'))
+
+ valid_subscriptions = ('to', 'from', 'both', 'none', 'remove')
+ for jid, item in items.items():
+ if item['subscription'] in valid_subscriptions:
+ roster[jid]['name'] = item['name']
+ roster[jid]['groups'] = item['groups']
+ roster[jid]['from'] = item['subscription'] in ('from', 'both')
+ roster[jid]['to'] = item['subscription'] in ('to', 'both')
+ roster[jid]['pending_out'] = (item['ask'] == 'subscribe')
+
+ roster[jid].save(remove=(item['subscription'] == 'remove'))
self.event("roster_update", iq)
if iq['type'] == 'set':
diff --git a/sleekxmpp/features/__init__.py b/sleekxmpp/features/__init__.py
index 1ef1e0cf..869de7e9 100644
--- a/sleekxmpp/features/__init__.py
+++ b/sleekxmpp/features/__init__.py
@@ -11,5 +11,6 @@ __all__ = [
'feature_mechanisms',
'feature_bind',
'feature_session',
- 'feature_rosterver'
+ 'feature_rosterver',
+ 'feature_preapproval'
]
diff --git a/sleekxmpp/features/feature_mechanisms/mechanisms.py b/sleekxmpp/features/feature_mechanisms/mechanisms.py
index 2ab7b0a4..0132765f 100644
--- a/sleekxmpp/features/feature_mechanisms/mechanisms.py
+++ b/sleekxmpp/features/feature_mechanisms/mechanisms.py
@@ -6,12 +6,11 @@
See the file LICENSE for copying permission.
"""
+import sys
import logging
-from sleekxmpp.thirdparty import suelta
-from sleekxmpp.thirdparty.suelta.exceptions import SASLCancelled, SASLError
-from sleekxmpp.thirdparty.suelta.exceptions import SASLPrepFailure
-
+from sleekxmpp.util import sasl
+from sleekxmpp.util.stringprep_profiles import StringPrepError
from sleekxmpp.stanza import StreamFeatures
from sleekxmpp.xmlstream import RestartStream, register_stanza_plugin
from sleekxmpp.plugins import BasePlugin
@@ -31,7 +30,15 @@ class FeatureMechanisms(BasePlugin):
stanza = stanza
default_config = {
'use_mech': None,
+ 'use_mechs': None,
+ 'min_mech': None,
'sasl_callback': None,
+ 'security_callback': None,
+ 'encrypted_plain': True,
+ 'unencrypted_plain': False,
+ 'unencrypted_digest': False,
+ 'unencrypted_cram': False,
+ 'unencrypted_scram': True,
'order': 100
}
@@ -39,34 +46,13 @@ class FeatureMechanisms(BasePlugin):
if not self.use_mech and not self.xmpp.boundjid.user:
self.use_mech = 'ANONYMOUS'
- def tls_active():
- return 'starttls' in self.xmpp.features
-
- def basic_callback(mech, values):
- creds = self.xmpp.credentials
- for value in values:
- if value == 'username':
- values['username'] = self.xmpp.boundjid.user
- elif value == 'password':
- values['password'] = creds['password']
- elif value == 'email':
- jid = self.xmpp.boundjid.bare
- values['email'] = creds.get('email', jid)
- elif value in creds:
- values[value] = creds[value]
- mech.fulfill(values)
-
if self.sasl_callback is None:
- self.sasl_callback = basic_callback
+ self.sasl_callback = self._default_credentials
- 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=self.sasl_callback,
- tls_active=tls_active,
- mech=self.use_mech)
+ if self.security_callback is None:
+ self.security_callback = self._default_security
+ self.mech = None
self.mech_list = set()
self.attempted_mechs = set()
@@ -99,6 +85,44 @@ class FeatureMechanisms(BasePlugin):
restart=True,
order=self.order)
+ def _default_credentials(self, required_values, optional_values):
+ creds = self.xmpp.credentials
+ result = {}
+ values = required_values.union(optional_values)
+ for value in values:
+ if value == 'username':
+ result[value] = self.xmpp.boundjid.user
+ elif value == 'password':
+ result[value] = creds['password']
+ elif value == 'email':
+ jid = self.xmpp.boundjid.bare
+ result[value] = creds.get('email', jid)
+ elif value == 'channel_binding':
+ if sys.version_info >= (3, 3):
+ result[value] = self.xmpp.socket.channel_binding()
+ else:
+ result[value] = None
+ elif value == 'host':
+ result[value] = self.xmpp.boundjid.domain
+ elif value == 'realm':
+ result[value] = self.xmpp.boundjid.domain
+ elif value == 'service-name':
+ result[value] = self.xmpp.address[0]
+ elif value == 'service':
+ result[value] = 'xmpp'
+ elif value in creds:
+ result[value] = creds[value]
+ return result
+
+ def _default_security(self, values):
+ result = {}
+ for value in values:
+ if value == 'encrypted':
+ result[value] = 'starttls' in self.xmpp.features
+ else:
+ result[value] = self.config.get(value, False)
+ return result
+
def _handle_sasl_auth(self, features):
"""
Handle authenticating using SASL.
@@ -111,37 +135,61 @@ class FeatureMechanisms(BasePlugin):
# server has incorrectly offered it again.
return False
- if not self.use_mech:
- self.mech_list = set(features['mechanisms'])
- else:
- self.mech_list = set([self.use_mech])
+ enforce_limit = False
+ limited_mechs = self.use_mechs
+
+ if limited_mechs is None:
+ limited_mechs = set()
+ elif limited_mechs and not isinstance(limited_mechs, set):
+ limited_mechs = set(limited_mechs)
+ enforce_limit = True
+
+ if self.use_mech:
+ limited_mechs.add(self.use_mech)
+ enforce_limit = True
+
+ if enforce_limit:
+ self.use_mechs = limited_mechs
+
+ self.mech_list = set(features['mechanisms'])
+
return self._send_auth()
def _send_auth(self):
mech_list = self.mech_list - self.attempted_mechs
- self.mech = self.sasl.choose_mechanism(mech_list)
-
- if mech_list and self.mech is not None:
- resp = stanza.Auth(self.xmpp)
- resp['mechanism'] = self.mech.name
- try:
- resp['value'] = self.mech.process()
- except SASLCancelled:
- self.attempted_mechs.add(self.mech.name)
- self._send_auth()
- except SASLError:
- self.attempted_mechs.add(self.mech.name)
- self._send_auth()
- except SASLPrepFailure:
- log.exception("A credential value did not pass SASLprep.")
- self.xmpp.disconnect()
- else:
- resp.send(now=True)
- else:
+ try:
+ self.mech = sasl.choose(mech_list,
+ self.sasl_callback,
+ self.security_callback,
+ limit=self.use_mechs,
+ min_mech=self.min_mech)
+ except sasl.SASLNoAppropriateMechanism:
log.error("No appropriate login method.")
self.xmpp.event("no_auth", direct=True)
self.attempted_mechs = set()
+ return self.xmpp.disconnect()
+
+ resp = stanza.Auth(self.xmpp)
+ resp['mechanism'] = self.mech.name
+ try:
+ resp['value'] = self.mech.process()
+ except sasl.SASLCancelled:
+ self.attempted_mechs.add(self.mech.name)
+ self._send_auth()
+ except sasl.SASLFailed:
+ self.attempted_mechs.add(self.mech.name)
+ self._send_auth()
+ except sasl.SASLMutualAuthFailed:
+ log.error("Mutual authentication failed! " + \
+ "A security breach is possible.")
+ self.attempted_mechs.add(self.mech.name)
+ self.xmpp.disconnect()
+ except StringPrepError:
+ log.exception("A credential value did not pass SASLprep.")
self.xmpp.disconnect()
+ else:
+ resp.send(now=True)
+
return True
def _handle_challenge(self, stanza):
@@ -149,20 +197,33 @@ class FeatureMechanisms(BasePlugin):
resp = self.stanza.Response(self.xmpp)
try:
resp['value'] = self.mech.process(stanza['value'])
- except SASLCancelled:
+ except sasl.SASLCancelled:
self.stanza.Abort(self.xmpp).send()
- except SASLError:
+ except sasl.SASLFailed:
self.stanza.Abort(self.xmpp).send()
+ except sasl.SASLMutualAuthFailed:
+ log.error("Mutual authentication failed! " + \
+ "A security breach is possible.")
+ self.attempted_mechs.add(self.mech.name)
+ self.xmpp.disconnect()
else:
resp.send(now=True)
def _handle_success(self, stanza):
"""SASL authentication succeeded. Restart the stream."""
- self.attempted_mechs = set()
- self.xmpp.authenticated = True
- self.xmpp.features.add('mechanisms')
- self.xmpp.event('auth_success', stanza, direct=True)
- raise RestartStream()
+ try:
+ final = self.mech.process(stanza['value'])
+ except sasl.SASLMutualAuthFailed:
+ log.error("Mutual authentication failed! " + \
+ "A security breach is possible.")
+ self.attempted_mechs.add(self.mech.name)
+ self.xmpp.disconnect()
+ else:
+ self.attempted_mechs = set()
+ self.xmpp.authenticated = True
+ self.xmpp.features.add('mechanisms')
+ self.xmpp.event('auth_success', stanza, direct=True)
+ raise RestartStream()
def _handle_fail(self, stanza):
"""SASL authentication failed. Disconnect and shutdown."""
diff --git a/sleekxmpp/features/feature_mechanisms/stanza/auth.py b/sleekxmpp/features/feature_mechanisms/stanza/auth.py
index 8b9d18b6..7b665345 100644
--- a/sleekxmpp/features/feature_mechanisms/stanza/auth.py
+++ b/sleekxmpp/features/feature_mechanisms/stanza/auth.py
@@ -8,8 +8,7 @@
import base64
-from sleekxmpp.thirdparty.suelta.util import bytes
-
+from sleekxmpp.util import bytes
from sleekxmpp.xmlstream import StanzaBase
diff --git a/sleekxmpp/features/feature_mechanisms/stanza/challenge.py b/sleekxmpp/features/feature_mechanisms/stanza/challenge.py
index 85d65403..24290281 100644
--- a/sleekxmpp/features/feature_mechanisms/stanza/challenge.py
+++ b/sleekxmpp/features/feature_mechanisms/stanza/challenge.py
@@ -8,8 +8,7 @@
import base64
-from sleekxmpp.thirdparty.suelta.util import bytes
-
+from sleekxmpp.util import bytes
from sleekxmpp.xmlstream import StanzaBase
diff --git a/sleekxmpp/features/feature_mechanisms/stanza/response.py b/sleekxmpp/features/feature_mechanisms/stanza/response.py
index 78636c9e..ca7624f1 100644
--- a/sleekxmpp/features/feature_mechanisms/stanza/response.py
+++ b/sleekxmpp/features/feature_mechanisms/stanza/response.py
@@ -8,8 +8,7 @@
import base64
-from sleekxmpp.thirdparty.suelta.util import bytes
-
+from sleekxmpp.util import bytes
from sleekxmpp.xmlstream import StanzaBase
diff --git a/sleekxmpp/features/feature_mechanisms/stanza/success.py b/sleekxmpp/features/feature_mechanisms/stanza/success.py
index 7a5a73f2..7a4eab8e 100644
--- a/sleekxmpp/features/feature_mechanisms/stanza/success.py
+++ b/sleekxmpp/features/feature_mechanisms/stanza/success.py
@@ -6,8 +6,10 @@
See the file LICENSE for copying permission.
"""
-from sleekxmpp.xmlstream import StanzaBase
+import base64
+from sleekxmpp.util import bytes
+from sleekxmpp.xmlstream import StanzaBase
class Success(StanzaBase):
@@ -16,9 +18,21 @@ class Success(StanzaBase):
name = 'success'
namespace = 'urn:ietf:params:xml:ns:xmpp-sasl'
- interfaces = set()
+ 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):
+ if values:
+ self.xml.text = bytes(base64.b64encode(values)).decode('utf-8')
+ else:
+ self.xml.text = '='
+
+ def del_value(self):
+ self.xml.text = ''
diff --git a/sleekxmpp/features/feature_preapproval/__init__.py b/sleekxmpp/features/feature_preapproval/__init__.py
new file mode 100644
index 00000000..ae8b6b70
--- /dev/null
+++ b/sleekxmpp/features/feature_preapproval/__init__.py
@@ -0,0 +1,15 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2012 Nathanael C. Fritz
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+from sleekxmpp.plugins.base import register_plugin
+
+from sleekxmpp.features.feature_preapproval.preapproval import FeaturePreApproval
+from sleekxmpp.features.feature_preapproval.stanza import PreApproval
+
+
+register_plugin(FeaturePreApproval)
diff --git a/sleekxmpp/features/feature_preapproval/preapproval.py b/sleekxmpp/features/feature_preapproval/preapproval.py
new file mode 100644
index 00000000..3823c472
--- /dev/null
+++ b/sleekxmpp/features/feature_preapproval/preapproval.py
@@ -0,0 +1,42 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2012 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_preapproval import stanza
+from sleekxmpp.xmlstream import register_stanza_plugin
+from sleekxmpp.plugins.base import BasePlugin
+
+
+log = logging.getLogger(__name__)
+
+
+class FeaturePreApproval(BasePlugin):
+
+ name = 'feature_preapproval'
+ description = 'RFC 6121: Stream Feature: Subscription Pre-Approval'
+ dependences = set()
+ stanza = stanza
+
+ def plugin_init(self):
+ self.xmpp.register_feature('preapproval',
+ self._handle_preapproval,
+ restart=False,
+ order=9001)
+
+ register_stanza_plugin(StreamFeatures, stanza.PreApproval)
+
+ def _handle_preapproval(self, features):
+ """Save notice that the server support subscription pre-approvals.
+
+ Arguments:
+ features -- The stream features stanza.
+ """
+ log.debug("Server supports subscription pre-approvals.")
+ self.xmpp.features.add('preapproval')
diff --git a/sleekxmpp/features/feature_preapproval/stanza.py b/sleekxmpp/features/feature_preapproval/stanza.py
new file mode 100644
index 00000000..4a59bd16
--- /dev/null
+++ b/sleekxmpp/features/feature_preapproval/stanza.py
@@ -0,0 +1,17 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2012 Nathanael C. Fritz
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+from sleekxmpp.xmlstream import ElementBase
+
+
+class PreApproval(ElementBase):
+
+ name = 'sub'
+ namespace = 'urn:xmpp:features:pre-approval'
+ interfaces = set()
+ plugin_attrib = 'preapproval'
diff --git a/sleekxmpp/plugins/__init__.py b/sleekxmpp/plugins/__init__.py
index 15e8cb25..1db5a9e0 100644
--- a/sleekxmpp/plugins/__init__.py
+++ b/sleekxmpp/plugins/__init__.py
@@ -18,6 +18,7 @@ __all__ = [
'xep_0004', # Data Forms
'xep_0009', # Jabber-RPC
'xep_0012', # Last Activity
+ 'xep_0016', # Privacy Lists
'xep_0027', # Current Jabber OpenPGP Usage
'xep_0030', # Service Discovery
'xep_0033', # Extended Stanza Addresses
@@ -60,6 +61,7 @@ __all__ = [
'xep_0223', # Persistent Storage of Private Data via Pubsub
'xep_0224', # Attention
'xep_0231', # Bits of Binary
+ 'xep_0242', # XMPP Client Compliance 2009
'xep_0249', # Direct MUC Invitations
'xep_0256', # Last Activity in Presence
'xep_0258', # Security Labels in XMPP
diff --git a/sleekxmpp/plugins/xep_0016/__init__.py b/sleekxmpp/plugins/xep_0016/__init__.py
new file mode 100644
index 00000000..06704d26
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0016/__init__.py
@@ -0,0 +1,16 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+from sleekxmpp.plugins.base import register_plugin
+
+from sleekxmpp.plugins.xep_0016 import stanza
+from sleekxmpp.plugins.xep_0016.stanza import Privacy
+from sleekxmpp.plugins.xep_0016.privacy import XEP_0016
+
+
+register_plugin(XEP_0016)
diff --git a/sleekxmpp/plugins/xep_0016/privacy.py b/sleekxmpp/plugins/xep_0016/privacy.py
new file mode 100644
index 00000000..79fd68f0
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0016/privacy.py
@@ -0,0 +1,110 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+from sleekxmpp import Iq
+from sleekxmpp.xmlstream import register_stanza_plugin
+from sleekxmpp.plugins import BasePlugin
+from sleekxmpp.plugins.xep_0016 import stanza
+from sleekxmpp.plugins.xep_0016.stanza import Privacy, Item
+
+
+class XEP_0016(BasePlugin):
+
+ name = 'xep_0016'
+ description = 'XEP-0016: Privacy Lists'
+ dependencies = set(['xep_0030'])
+ stanza = stanza
+
+ def plugin_init(self):
+ register_stanza_plugin(Iq, Privacy)
+
+ def plugin_end(self):
+ self.xmpp['xep_0030'].del_feature(feature=Privacy.namespace)
+
+ def session_bind(self, jid):
+ self.xmpp['xep_0030'].add_feature(Privacy.namespace)
+
+ def get_privacy_lists(self, block=True, timeout=None, callback=None):
+ iq = self.xmpp.Iq()
+ iq['type'] = 'get'
+ iq.enable('privacy')
+ return iq.send(block=block, timeout=timeout, callback=callback)
+
+ def get_list(self, name, block=True, timeout=None, callback=None):
+ iq = self.xmpp.Iq()
+ iq['type'] = 'get'
+ iq['privacy']['list']['name'] = name
+ return iq.send(block=block, timeout=timeout, callback=callback)
+
+ def get_active(self, block=True, timeout=None, callback=None):
+ iq = self.xmpp.Iq()
+ iq['type'] = 'get'
+ iq['privacy'].enable('active')
+ return iq.send(block=block, timeout=timeout, callback=callback)
+
+ def get_default(self, block=True, timeout=None, callback=None):
+ iq = self.xmpp.Iq()
+ iq['type'] = 'get'
+ iq['privacy'].enable('default')
+ return iq.send(block=block, timeout=timeout, callback=callback)
+
+ def activate(self, name, block=True, timeout=None, callback=None):
+ iq = self.xmpp.Iq()
+ iq['type'] = 'set'
+ iq['privacy']['active']['name'] = name
+ return iq.send(block=block, timeout=timeout, callback=callback)
+
+ def deactivate(self, block=True, timeout=None, callback=None):
+ iq = self.xmpp.Iq()
+ iq['type'] = 'set'
+ iq['privacy'].enable('active')
+ return iq.send(block=block, timeout=timeout, callback=callback)
+
+ def make_default(self, name, block=True, timeout=None, callback=None):
+ iq = self.xmpp.Iq()
+ iq['type'] = 'set'
+ iq['privacy']['default']['name'] = name
+ return iq.send(block=block, timeout=timeout, callback=callback)
+
+ def remove_default(self, block=True, timeout=None, callback=None):
+ iq = self.xmpp.Iq()
+ iq['type'] = 'set'
+ iq['privacy'].enable('default')
+ return iq.send(block=block, timeout=timeout, callback=callback)
+
+ def edit_list(self, name, rules, block=True, timeout=None, callback=None):
+ iq = self.xmpp.Iq()
+ iq['type'] = 'set'
+ iq['privacy']['list']['name'] = name
+ priv_list = iq['privacy']['list']
+
+ if not rules:
+ rules = []
+
+ for rule in rules:
+ if isinstance(rule, Item):
+ priv_list.append(rule)
+ continue
+
+ priv_list.add_item(
+ rule['value'],
+ rule['action'],
+ rule['order'],
+ itype=rule.get('type', None),
+ iq=rule.get('iq', None),
+ message=rule.get('message', None),
+ presence_in=rule.get('presence_in',
+ rule.get('presence-in', None)),
+ presence_out=rule.get('presence_out',
+ rule.get('presence-out', None)))
+
+ def remove_list(self, name, block=True, timeout=None, callback=None):
+ iq = self.xmpp.Iq()
+ iq['type'] = 'set'
+ iq['privacy']['list']['name'] = name
+ return iq.send(block=block, timeout=timeout, callback=callback)
diff --git a/sleekxmpp/plugins/xep_0016/stanza.py b/sleekxmpp/plugins/xep_0016/stanza.py
new file mode 100644
index 00000000..3f9977fc
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0016/stanza.py
@@ -0,0 +1,103 @@
+from sleekxmpp.xmlstream import ET, ElementBase, register_stanza_plugin
+
+
+class Privacy(ElementBase):
+ name = 'query'
+ namespace = 'jabber:iq:privacy'
+ plugin_attrib = 'privacy'
+ interfaces = set()
+
+ def add_list(self, name):
+ priv_list = List()
+ priv_list['name'] = name
+ self.append(priv_list)
+ return priv_list
+
+
+class Active(ElementBase):
+ name = 'active'
+ namespace = 'jabber:iq:privacy'
+ plugin_attrib = name
+ interfaces = set(['name'])
+
+
+class Default(ElementBase):
+ name = 'default'
+ namespace = 'jabber:iq:privacy'
+ plugin_attrib = name
+ interfaces = set(['name'])
+
+
+class List(ElementBase):
+ name = 'list'
+ namespace = 'jabber:iq:privacy'
+ plugin_attrib = name
+ plugin_multi_attrib = 'lists'
+ interfaces = set(['name'])
+
+ def add_item(self, value, action, order, itype=None, iq=False,
+ message=False, presence_in=False, presence_out=False):
+ item = Item()
+ item.values = {'type': itype,
+ 'value': value,
+ 'action': action,
+ 'order': order,
+ 'message': message,
+ 'iq': iq,
+ 'presence_in': presence_in,
+ 'presence_out': presence_out}
+ self.append(item)
+ return item
+
+
+class Item(ElementBase):
+ name = 'item'
+ namespace = 'jabber:iq:privacy'
+ plugin_attrib = name
+ plugin_multi_attrib = 'items'
+ interfaces = set(['type', 'value', 'action', 'order', 'iq',
+ 'message', 'presence_in', 'presence_out'])
+ bool_interfaces = set(['message', 'iq', 'presence_in', 'presence_out'])
+
+ type_values = ('', 'jid', 'group', 'subscription')
+ action_values = ('allow', 'deny')
+
+ def set_type(self, value):
+ if value and value not in self.type_values:
+ raise ValueError('Unknown type value: %s' % value)
+ else:
+ self._set_attr('type', value)
+
+ def set_action(self, value):
+ if value not in self.action_values:
+ raise ValueError('Unknown action value: %s' % value)
+ else:
+ self._set_attr('action', value)
+
+ def set_presence_in(self, value):
+ keep = True if value else False
+ self._set_sub_text('presence-in', '', keep=keep)
+
+ def get_presence_in(self):
+ pres = self.xml.find('{%s}presence-in' % self.namespace)
+ return pres is not None
+
+ def del_presence_in(self):
+ self._del_sub('{%s}presence-in' % self.namespace)
+
+ def set_presence_out(self, value):
+ keep = True if value else False
+ self._set_sub_text('presence-in', '', keep=keep)
+
+ def get_presence_out(self):
+ pres = self.xml.find('{%s}presence-in' % self.namespace)
+ return pres is not None
+
+ def del_presence_out(self):
+ self._del_sub('{%s}presence-in' % self.namespace)
+
+
+register_stanza_plugin(Privacy, Active)
+register_stanza_plugin(Privacy, Default)
+register_stanza_plugin(Privacy, List, iterable=True)
+register_stanza_plugin(List, Item, iterable=True)
diff --git a/sleekxmpp/plugins/xep_0047/stanza.py b/sleekxmpp/plugins/xep_0047/stanza.py
index afba07a8..e4a32f87 100644
--- a/sleekxmpp/plugins/xep_0047/stanza.py
+++ b/sleekxmpp/plugins/xep_0047/stanza.py
@@ -1,9 +1,9 @@
import re
import base64
+from sleekxmpp.util import bytes
from sleekxmpp.exceptions import XMPPError
from sleekxmpp.xmlstream import ElementBase
-from sleekxmpp.thirdparty.suelta.util import bytes
VALID_B64 = re.compile(r'[A-Za-z0-9\+\/]*=*')
diff --git a/sleekxmpp/plugins/xep_0050/adhoc.py b/sleekxmpp/plugins/xep_0050/adhoc.py
index 032b987a..90256228 100644
--- a/sleekxmpp/plugins/xep_0050/adhoc.py
+++ b/sleekxmpp/plugins/xep_0050/adhoc.py
@@ -187,12 +187,6 @@ class XEP_0050(BasePlugin):
jid = JID(jid)
item_jid = jid.full
- # Client disco uses only the bare JID
- if self.xmpp.is_component:
- jid = jid.full
- else:
- jid = jid.bare
-
self.xmpp['xep_0030'].add_identity(category='automation',
itype='command-list',
name='Ad-Hoc commands',
diff --git a/sleekxmpp/plugins/xep_0054/stanza.py b/sleekxmpp/plugins/xep_0054/stanza.py
index 75b69d3e..512e1dd8 100644
--- a/sleekxmpp/plugins/xep_0054/stanza.py
+++ b/sleekxmpp/plugins/xep_0054/stanza.py
@@ -1,8 +1,7 @@
import base64
import datetime as dt
-from sleekxmpp.thirdparty.suelta.util import bytes
-
+from sleekxmpp.util import bytes
from sleekxmpp.xmlstream import ElementBase, ET, register_stanza_plugin, JID
from sleekxmpp.plugins import xep_0082
diff --git a/sleekxmpp/plugins/xep_0084/stanza.py b/sleekxmpp/plugins/xep_0084/stanza.py
index e9133998..22f11b72 100644
--- a/sleekxmpp/plugins/xep_0084/stanza.py
+++ b/sleekxmpp/plugins/xep_0084/stanza.py
@@ -7,8 +7,8 @@
"""
from base64 import b64encode, b64decode
-from sleekxmpp.thirdparty.suelta.util import bytes
+from sleekxmpp.util import bytes
from sleekxmpp.xmlstream import ET, ElementBase, register_stanza_plugin
diff --git a/sleekxmpp/plugins/xep_0242.py b/sleekxmpp/plugins/xep_0242.py
new file mode 100644
index 00000000..c1bada27
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0242.py
@@ -0,0 +1,21 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+from sleekxmpp.plugins import BasePlugin, register_plugin
+
+
+class XEP_0242(BasePlugin):
+
+ name = 'xep_0242'
+ description = 'XEP-0242: XMPP Client Compliance 2009'
+ dependencies = set(['xep_0030', 'xep_0115', 'xep_0054',
+ 'xep_0045', 'xep_0085', 'xep_0016',
+ 'xep_0191'])
+
+
+register_plugin(XEP_0242)
diff --git a/sleekxmpp/plugins/xep_0258/stanza.py b/sleekxmpp/plugins/xep_0258/stanza.py
index 4d828a46..a506064b 100644
--- a/sleekxmpp/plugins/xep_0258/stanza.py
+++ b/sleekxmpp/plugins/xep_0258/stanza.py
@@ -8,8 +8,7 @@
from base64 import b64encode, b64decode
-from sleekxmpp.thirdparty.suelta.util import bytes
-
+from sleekxmpp.util import bytes
from sleekxmpp.xmlstream import ElementBase, ET, register_stanza_plugin
diff --git a/sleekxmpp/thirdparty/__init__.py b/sleekxmpp/thirdparty/__init__.py
index 7ec045a6..2a1db717 100644
--- a/sleekxmpp/thirdparty/__init__.py
+++ b/sleekxmpp/thirdparty/__init__.py
@@ -8,5 +8,5 @@ try:
except:
from sleekxmpp.thirdparty.gnupg import GPG
-from sleekxmpp.thirdparty import suelta, socks
+from sleekxmpp.thirdparty import socks
from sleekxmpp.thirdparty.mini_dateutil import tzutc, tzoffset, parse_iso
diff --git a/sleekxmpp/thirdparty/suelta/LICENSE b/sleekxmpp/thirdparty/suelta/LICENSE
deleted file mode 100644
index 6eee4f33..00000000
--- a/sleekxmpp/thirdparty/suelta/LICENSE
+++ /dev/null
@@ -1,21 +0,0 @@
-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
deleted file mode 100644
index 393b8078..00000000
--- a/sleekxmpp/thirdparty/suelta/PLAYING-NICELY
+++ /dev/null
@@ -1,27 +0,0 @@
-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
deleted file mode 100644
index c32463a4..00000000
--- a/sleekxmpp/thirdparty/suelta/README
+++ /dev/null
@@ -1,8 +0,0 @@
-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
deleted file mode 100644
index 04f0cbad..00000000
--- a/sleekxmpp/thirdparty/suelta/__init__.py
+++ /dev/null
@@ -1,26 +0,0 @@
-# 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
deleted file mode 100644
index 40d8bad3..00000000
--- a/sleekxmpp/thirdparty/suelta/exceptions.py
+++ /dev/null
@@ -1,35 +0,0 @@
-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)
-
-
-class SASLPrepFailure(UnicodeError):
- pass
diff --git a/sleekxmpp/thirdparty/suelta/mechanisms/__init__.py b/sleekxmpp/thirdparty/suelta/mechanisms/__init__.py
deleted file mode 100644
index 2044ff80..00000000
--- a/sleekxmpp/thirdparty/suelta/mechanisms/__init__.py
+++ /dev/null
@@ -1,8 +0,0 @@
-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
-from sleekxmpp.thirdparty.suelta.mechanisms.messenger_oauth2 import X_MESSENGER_OAUTH2
-from sleekxmpp.thirdparty.suelta.mechanisms.facebook_platform import X_FACEBOOK_PLATFORM
-from sleekxmpp.thirdparty.suelta.mechanisms.google_token import X_GOOGLE_TOKEN
diff --git a/sleekxmpp/thirdparty/suelta/mechanisms/anonymous.py b/sleekxmpp/thirdparty/suelta/mechanisms/anonymous.py
deleted file mode 100644
index e44e91a2..00000000
--- a/sleekxmpp/thirdparty/suelta/mechanisms/anonymous.py
+++ /dev/null
@@ -1,36 +0,0 @@
-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__(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
deleted file mode 100644
index e07bb883..00000000
--- a/sleekxmpp/thirdparty/suelta/mechanisms/cram_md5.py
+++ /dev/null
@@ -1,63 +0,0 @@
-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=None):
- """
- """
- 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
deleted file mode 100644
index 890f3e24..00000000
--- a/sleekxmpp/thirdparty/suelta/mechanisms/digest_md5.py
+++ /dev/null
@@ -1,275 +0,0 @@
-import sys
-
-import random
-import hmac
-
-from sleekxmpp.thirdparty.suelta.util import hash, bytes, quote
-from sleekxmpp.thirdparty.suelta.util import num_to_bytes, bytes_to_num
-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/facebook_platform.py b/sleekxmpp/thirdparty/suelta/mechanisms/facebook_platform.py
deleted file mode 100644
index af6a78eb..00000000
--- a/sleekxmpp/thirdparty/suelta/mechanisms/facebook_platform.py
+++ /dev/null
@@ -1,43 +0,0 @@
-from sleekxmpp.thirdparty.suelta.util import bytes
-from sleekxmpp.thirdparty.suelta.sasl import Mechanism, register_mechanism
-
-try:
- import urlparse
-except ImportError:
- import urllib.parse as urlparse
-
-
-
-class X_FACEBOOK_PLATFORM(Mechanism):
-
- def __init__(self, sasl, name):
- super(X_FACEBOOK_PLATFORM, self).__init__(sasl, name)
- self.check_values(['access_token', 'api_key'])
-
- def process(self, challenge=None):
- if challenge is not None:
- values = {}
- for kv in challenge.split(b'&'):
- key, value = kv.split(b'=')
- values[key] = value
-
- resp_data = {
- 'method': values[b'method'],
- 'v': '1.0',
- 'call_id': '1.0',
- 'nonce': values[b'nonce'],
- 'access_token': self.values['access_token'],
- 'api_key': self.values['api_key']
- }
-
- for k, v in resp_data.items():
- resp_data[k] = bytes(v).decode('utf-8')
-
- resp = '&'.join(['%s=%s' % (k, v) for k, v in resp_data.items()])
- return bytes(resp)
- return b''
-
- def okay(self):
- return True
-
-register_mechanism('X-FACEBOOK-PLATFORM', 40, X_FACEBOOK_PLATFORM, use_hashes=False)
diff --git a/sleekxmpp/thirdparty/suelta/mechanisms/google_token.py b/sleekxmpp/thirdparty/suelta/mechanisms/google_token.py
deleted file mode 100644
index e641bb91..00000000
--- a/sleekxmpp/thirdparty/suelta/mechanisms/google_token.py
+++ /dev/null
@@ -1,22 +0,0 @@
-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 X_GOOGLE_TOKEN(Mechanism):
-
- def __init__(self, sasl, name):
- super(X_GOOGLE_TOKEN, self).__init__(sasl, name)
- self.check_values(['email', 'access_token'])
-
- def process(self, challenge=None):
- email = bytes(self.values['email'])
- token = bytes(self.values['access_token'])
- return b'\x00' + email + b'\x00' + token
-
- def okay(self):
- return True
-
-
-register_mechanism('X-GOOGLE-TOKEN', 3, X_GOOGLE_TOKEN, use_hashes=False)
diff --git a/sleekxmpp/thirdparty/suelta/mechanisms/messenger_oauth2.py b/sleekxmpp/thirdparty/suelta/mechanisms/messenger_oauth2.py
deleted file mode 100644
index f5b0ddec..00000000
--- a/sleekxmpp/thirdparty/suelta/mechanisms/messenger_oauth2.py
+++ /dev/null
@@ -1,17 +0,0 @@
-from sleekxmpp.thirdparty.suelta.util import bytes
-from sleekxmpp.thirdparty.suelta.sasl import Mechanism, register_mechanism
-
-
-class X_MESSENGER_OAUTH2(Mechanism):
-
- def __init__(self, sasl, name):
- super(X_MESSENGER_OAUTH2, self).__init__(sasl, name)
- self.check_values(['access_token'])
-
- def process(self, challenge=None):
- return bytes(self.values['access_token'])
-
- def okay(self):
- return True
-
-register_mechanism('X-MESSENGER-OAUTH2', 10, X_MESSENGER_OAUTH2, use_hashes=False)
diff --git a/sleekxmpp/thirdparty/suelta/mechanisms/plain.py b/sleekxmpp/thirdparty/suelta/mechanisms/plain.py
deleted file mode 100644
index accae54a..00000000
--- a/sleekxmpp/thirdparty/suelta/mechanisms/plain.py
+++ /dev/null
@@ -1,61 +0,0 @@
-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', 5, PLAIN, use_hashes=False)
diff --git a/sleekxmpp/thirdparty/suelta/mechanisms/scram_hmac.py b/sleekxmpp/thirdparty/suelta/mechanisms/scram_hmac.py
deleted file mode 100644
index b70ac9a4..00000000
--- a/sleekxmpp/thirdparty/suelta/mechanisms/scram_hmac.py
+++ /dev/null
@@ -1,176 +0,0 @@
-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(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
deleted file mode 100644
index 2ae9ae61..00000000
--- a/sleekxmpp/thirdparty/suelta/sasl.py
+++ /dev/null
@@ -1,402 +0,0 @@
-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 in ['', 'anonymous', None]:
- 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 is not 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
deleted file mode 100644
index 0e72fcb1..00000000
--- a/sleekxmpp/thirdparty/suelta/saslprep.py
+++ /dev/null
@@ -1,81 +0,0 @@
-from __future__ import unicode_literals
-
-import sys
-import stringprep
-import unicodedata
-
-
-from sleekxmpp.thirdparty.suelta.exceptions import SASLPrepFailure
-
-
-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('utf-8')
-
- # 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 SASLPrepFailure('Section 6.3 [end]')
-
- # Check for prohibited characters
- for x in range(len(text)):
- if strict and stringprep.in_table_a1(text[x]):
- raise SASLPrepFailure('Unassigned Codepoint')
- if stringprep.in_table_c12(text[x]):
- raise SASLPrepFailure('In table C.1.2')
- if stringprep.in_table_c21(text[x]):
- raise SASLPrepFailure('In table C.2.1')
- if stringprep.in_table_c22(text[x]):
- raise SASLPrepFailure('In table C.2.2')
- if stringprep.in_table_c3(text[x]):
- raise SASLPrepFailure('In table C.3')
- if stringprep.in_table_c4(text[x]):
- raise SASLPrepFailure('In table C.4')
- if stringprep.in_table_c5(text[x]):
- raise SASLPrepFailure('In table C.5')
- if stringprep.in_table_c6(text[x]):
- raise SASLPrepFailure('In table C.6')
- if stringprep.in_table_c7(text[x]):
- raise SASLPrepFailure('In table C.7')
- if stringprep.in_table_c8(text[x]):
- raise SASLPrepFailure('In table C.8')
- if stringprep.in_table_c9(text[x]):
- raise SASLPrepFailure('In table C.9')
- if x:
- if first_is_randal and stringprep.in_table_d2(text[x]):
- raise SASLPrepFailure('Section 6.2')
- if not first_is_randal and \
- x != len(text) - 1 and \
- stringprep.in_table_d1(text[x]):
- raise SASLPrepFailure('Section 6.3')
-
- return text
diff --git a/sleekxmpp/util/__init__.py b/sleekxmpp/util/__init__.py
index 86a87222..7637dda0 100644
--- a/sleekxmpp/util/__init__.py
+++ b/sleekxmpp/util/__init__.py
@@ -10,6 +10,10 @@
"""
+from sleekxmpp.util.misc_ops import bytes, unicode, hashes, hash, \
+ num_to_bytes, bytes_to_num, quote, XOR
+
+
# =====================================================================
# Standardize import of Queue class:
diff --git a/sleekxmpp/thirdparty/suelta/util.py b/sleekxmpp/util/misc_ops.py
index cd2439d5..9ed535d9 100644
--- a/sleekxmpp/thirdparty/suelta/util.py
+++ b/sleekxmpp/util/misc_ops.py
@@ -1,10 +1,14 @@
-"""
-"""
-
import sys
import hashlib
+def unicode(text):
+ if sys.version_info < (3, 0):
+ import __builtin__
+ return __builtin__.unicode(text)
+ return str(text)
+
+
def bytes(text):
"""
Convert Unicode text to UTF-8 encoded bytes.
@@ -15,9 +19,6 @@ def bytes(text):
:param text: Unicode text to convert to bytes
:rtype: bytes (Python3), str (Python2.6+)
"""
- if text is None:
- return b''
-
if sys.version_info < (3, 0):
import __builtin__
return __builtin__.bytes(text)
diff --git a/sleekxmpp/util/sasl/__init__.py b/sleekxmpp/util/sasl/__init__.py
new file mode 100644
index 00000000..d054ce09
--- /dev/null
+++ b/sleekxmpp/util/sasl/__init__.py
@@ -0,0 +1,15 @@
+# -*- coding: utf-8 -*-
+"""
+ sleekxmpp.util.sasl
+ ~~~~~~~~~~~~~~~~~~~
+
+ This module was originally based on Dave Cridland's Suelta library.
+
+ Part of SleekXMPP: The Sleek XMPP Library
+
+ :copyright: (c) 2012 Nathanael C. Fritz, Lance J.T. Stout
+ :license: MIT, see LICENSE for more details
+"""
+
+from sleekxmpp.util.sasl.client import *
+from sleekxmpp.util.sasl.mechanisms import *
diff --git a/sleekxmpp/util/sasl/client.py b/sleekxmpp/util/sasl/client.py
new file mode 100644
index 00000000..36f8b7a7
--- /dev/null
+++ b/sleekxmpp/util/sasl/client.py
@@ -0,0 +1,168 @@
+# -*- coding: utf-8 -*-
+"""
+ sleekxmpp.util.sasl.client
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+ This module was originally based on Dave Cridland's Suelta library.
+
+ Part of SleekXMPP: The Sleek XMPP Library
+
+ :copyright: (c) 2012 Nathanael C. Fritz, Lance J.T. Stout
+ :license: MIT, see LICENSE for more details
+"""
+
+import logging
+import stringprep
+
+from sleekxmpp.util import hashes, bytes, stringprep_profiles
+
+
+log = logging.getLogger(__name__)
+
+
+#: Global registry mapping mechanism names to implementation classes.
+MECHANISMS = {}
+
+
+#: Global registry mapping mechanism names to security scores.
+MECH_SEC_SCORES = {}
+
+
+#: The SASLprep profile of stringprep used to validate simple username
+#: and password credentials.
+saslprep = stringprep_profiles.create(
+ nfkc=True,
+ bidi=True,
+ mappings=[
+ stringprep_profiles.b1_mapping,
+ stringprep_profiles.c12_mapping],
+ prohibited=[
+ stringprep.in_table_c12,
+ stringprep.in_table_c21,
+ stringprep.in_table_c22,
+ stringprep.in_table_c3,
+ stringprep.in_table_c4,
+ stringprep.in_table_c5,
+ stringprep.in_table_c6,
+ stringprep.in_table_c7,
+ stringprep.in_table_c8,
+ stringprep.in_table_c9],
+ unassigned=[stringprep.in_table_a1])
+
+
+def sasl_mech(score):
+ sec_score = score
+ def register(mech):
+ n = 0
+ mech.score = sec_score
+ if mech.use_hashes:
+ for hashing_alg in hashes():
+ n += 1
+ score = mech.score + n
+ name = '%s-%s' % (mech.name, hashing_alg)
+ MECHANISMS[name] = mech
+ MECH_SEC_SCORES[name] = score
+
+ if mech.channel_binding:
+ name += '-PLUS'
+ score += 10
+ MECHANISMS[name] = mech
+ MECH_SEC_SCORES[name] = score
+ else:
+ MECHANISMS[mech.name] = mech
+ MECH_SEC_SCORES[mech.name] = mech.score
+ if mech.channel_binding:
+ MECHANISMS[mech.name + '-PLUS'] = mech
+ MECH_SEC_SCORES[name] = mech.score + 10
+ return mech
+ return register
+
+
+class SASLNoAppropriateMechanism(Exception):
+ pass
+
+
+class SASLCancelled(Exception):
+ pass
+
+
+class SASLFailed(Exception):
+ pass
+
+
+class SASLMutualAuthFailed(SASLFailed):
+ pass
+
+
+class Mech(object):
+
+ name = 'GENERIC'
+ score = -1
+ use_hashes = False
+ channel_binding = False
+ required_credentials = set()
+ optional_credentials = set()
+ security = set()
+
+ def __init__(self, name, credentials, security_settings):
+ self.credentials = credentials
+ self.security_settings = security_settings
+ self.values = {}
+ self.base_name = self.name
+ self.name = name
+ self.setup(name)
+
+ def setup(self, name):
+ pass
+
+ def process(self, challenge=b''):
+ return b''
+
+
+def choose(mech_list, credentials, security_settings, limit=None, min_mech=None):
+ available_mechs = set(MECHANISMS.keys())
+ if limit is None:
+ limit = set(mech_list)
+ if not isinstance(limit, set):
+ limit = set(limit)
+ if not isinstance(mech_list, set):
+ mech_list = set(mech_list)
+
+ mech_list = mech_list.intersection(limit)
+ available_mechs = available_mechs.intersection(mech_list)
+
+ best_score = MECH_SEC_SCORES.get(min_mech, -1)
+ best_mech = None
+ for name in available_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 is None:
+ raise SASLNoAppropriateMechanism()
+
+ mech_class = MECHANISMS[best_mech]
+
+ try:
+ creds = credentials(mech_class.required_credentials,
+ mech_class.optional_credentials)
+ for req in mech_class.required_credentials:
+ if req not in creds:
+ raise SASLCancelled('Missing credential: %s' % req)
+ for opt in mech_class.optional_credentials:
+ if opt not in creds:
+ creds[opt] = b''
+ for cred in creds:
+ if cred in ('username', 'password', 'authzid'):
+ creds[cred] = bytes(saslprep(creds[cred]))
+ else:
+ creds[cred] = bytes(creds[cred])
+ security_opts = security_settings(mech_class.security)
+
+ return mech_class(best_mech, creds, security_opts)
+ except SASLCancelled as e:
+ log.info('SASL: %s: %s', best_mech, e.message)
+ mech_list.remove(best_mech)
+ return choose(mech_list, credentials, security_settings,
+ limit=limit,
+ min_mech=min_mech)
diff --git a/sleekxmpp/util/sasl/mechanisms.py b/sleekxmpp/util/sasl/mechanisms.py
new file mode 100644
index 00000000..5822a6e4
--- /dev/null
+++ b/sleekxmpp/util/sasl/mechanisms.py
@@ -0,0 +1,531 @@
+# -*- coding: utf-8 -*-
+"""
+ sleekxmpp.util.sasl.mechanisms
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+ A collection of supported SASL mechanisms.
+
+ This module was originally based on Dave Cridland's Suelta library.
+
+ Part of SleekXMPP: The Sleek XMPP Library
+
+ :copyright: (c) 2012 Nathanael C. Fritz, Lance J.T. Stout
+ :license: MIT, see LICENSE for more details
+"""
+
+import sys
+import hmac
+import random
+
+from base64 import b64encode, b64decode
+
+from sleekxmpp.util import bytes, hash, XOR, quote, num_to_bytes
+from sleekxmpp.util.sasl.client import sasl_mech, Mech, \
+ SASLCancelled, SASLFailed
+
+
+@sasl_mech(0)
+class ANONYMOUS(Mech):
+
+ name = 'ANONYMOUS'
+
+ def process(self, challenge=b''):
+ return b'Anonymous, Suelta'
+
+
+@sasl_mech(1)
+class LOGIN(Mech):
+
+ name = 'LOGIN'
+ required_credentials = set(['username', 'password'])
+
+ def setup(self, name):
+ self.step = 0
+
+ def process(self, challenge=b''):
+ if not challenge:
+ return b''
+
+ if self.step == 0:
+ self.step = 1
+ return self.credentials['username']
+ else:
+ return self.credentials['password']
+
+
+@sasl_mech(2)
+class PLAIN(Mech):
+
+ name = 'PLAIN'
+ required_credentials = set(['username', 'password'])
+ optional_credentials = set(['authzid'])
+ security = set(['encrypted', 'encrypted_plain', 'unencrypted_plain'])
+
+ def setup(self, name):
+ if not self.security_settings['encrypted']:
+ if not self.security_settings['unencrypted_plain']:
+ raise SASLCancelled('PLAIN without encryption')
+ else:
+ if not self.security_settings['encrypted_plain']:
+ raise SASLCancelled('PLAIN with encryption')
+
+ def process(self, challenge=b''):
+ authzid = self.credentials['authzid']
+ authcid = self.credentials['username']
+ password = self.credentials['password']
+ return authzid + b'\x00' + authcid + b'\x00' + password
+
+
+@sasl_mech(100)
+class EXTERNAL(Mech):
+
+ name = 'EXTERNAL'
+ optional_credentials = set(['authzid'])
+
+ def process(self, challenge=b''):
+ return self.credentials['authzid']
+
+
+@sasl_mech(30)
+class X_FACEBOOK_PLATFORM(Mech):
+
+ name = 'X-FACEBOOK-PLATFORM'
+ required_credentials = set(['api_key', 'access_token'])
+
+ def process(self, challenge=b''):
+ if challenge:
+ values = {}
+ for kv in challenge.split(b'&'):
+ key, value = kv.split(b'=')
+ values[key] = value
+
+ resp_data = {
+ b'method': values[b'method'],
+ b'v': b'1.0',
+ b'call_id': b'1.0',
+ b'nonce': values[b'nonce'],
+ b'access_token': self.credentials['access_token'],
+ b'api_key': self.credentials['api_key']
+ }
+
+ resp = '&'.join(['%s=%s' % (k, v) for k, v in resp_data.items()])
+ return bytes(resp)
+ return b''
+
+
+@sasl_mech(10)
+class X_MESSENGER_OAUTH2(Mech):
+
+ name = 'X-MESSENGER-OAUTH2'
+ required_credentials = set(['access_token'])
+
+ def process(self, challenge=b''):
+ return self.credentials['access_token']
+
+
+@sasl_mech(3)
+class X_GOOGLE_TOKEN(Mech):
+
+ name = 'X-GOOGLE-TOKEN'
+ required_credentials = set(['email', 'access_token'])
+
+ def process(self, challenge=b''):
+ email = self.credentials['email']
+ token = self.credentials['access_token']
+ return b'\x00' + email + b'\x00' + token
+
+
+@sasl_mech(20)
+class CRAM(Mech):
+
+ name = 'CRAM'
+ use_hashes = True
+ required_credentials = set(['username', 'password'])
+ security = set(['encrypted', 'unencrypted_cram'])
+
+ def setup(self, name):
+ self.hash_name = name[5:]
+ self.hash = hash(self.hash_name)
+ if self.hash is None:
+ raise SASLCancelled('Unknown hash: %s' % self.hash_name)
+ if not self.security_settings['encrypted']:
+ if not self.security_settings['unencrypted_cram']:
+ raise SASLCancelled('Unecrypted CRAM-%s' % self.hash_name)
+
+ def process(self, challenge=b''):
+ if not challenge:
+ return None
+
+ username = self.credentials['username']
+ password = self.credentials['password']
+
+ mac = hmac.HMAC(key=password, digestmod=self.hash)
+ mac.update(challenge)
+
+ return username + b' ' + bytes(mac.hexdigest())
+
+
+@sasl_mech(60)
+class SCRAM(Mech):
+
+ name = 'SCRAM'
+ use_hashes = True
+ channel_binding = True
+ required_credentials = set(['username', 'password'])
+ optional_credentials = set(['authzid', 'channel_binding'])
+ security = set(['encrypted', 'unencrypted_scram'])
+
+ def setup(self, name):
+ self.use_channel_binding = False
+ if name[-5:] == '-PLUS':
+ name = name[:-5]
+ self.use_channel_binding = True
+
+ self.hash_name = name[6:]
+ self.hash = hash(self.hash_name)
+
+ if self.hash is None:
+ raise SASLCancelled('Unknown hash: %s' % self.hash_name)
+ if not self.security_settings['encrypted']:
+ if not self.security_settings['unencrypted_scram']:
+ raise SASLCancelled('Unencrypted SCRAM')
+
+ self.step = 0
+ self._mutual_auth = 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)
+ ui1 = self.HMAC(text, salt + b'\0\0\0\01')
+ ui = ui1
+ for i in range(iterations - 1):
+ ui1 = self.HMAC(text, ui1)
+ ui = XOR(ui, ui1)
+ return ui
+
+ def H(self, text):
+ return self.hash(text).digest()
+
+ def saslname(self, value):
+ escaped = b''
+ for char in bytes(value):
+ if char == b',':
+ escaped += b'=2C'
+ elif char == b'=':
+ escaped += b'=3D'
+ else:
+ if isinstance(char, int):
+ char = chr(char)
+ escaped += bytes(char)
+ return escaped
+
+ def parse(self, challenge):
+ items = {}
+ for key, value in [item.split(b'=', 1) for item in challenge.split(b',')]:
+ items[key] = value
+ return items
+
+ def process(self, challenge=b''):
+ steps = [self.process_1, self.process_2, self.process_3]
+ return steps[self.step](challenge)
+
+ def process_1(self, challenge):
+ self.step = 1
+ data = {}
+
+ self.cnonce = bytes(('%s' % random.random())[2:])
+
+ gs2_cbind_flag = b'n'
+ if self.credentials['channel_binding']:
+ if self.use_channel_binding:
+ gs2_cbind_flag = b'p=tls-unique'
+ else:
+ gs2_cbind_flag = b'y'
+
+ authzid = b''
+ if self.credentials['authzid']:
+ authzid = b'a=' + self.saslname(self.credentials['authzid'])
+
+ self.gs2_header = gs2_cbind_flag + b',' + authzid + b','
+
+ nonce = b'r=' + self.cnonce
+ username = b'n=' + self.saslname(self.credentials['username'])
+
+ self.client_first_message_bare = username + b',' + nonce
+ self.client_first_message = self.gs2_header + \
+ self.client_first_message_bare
+
+ return self.client_first_message
+
+ def process_2(self, challenge):
+ self.step = 2
+
+ data = self.parse(challenge)
+ if b'm' in data:
+ raise SASLCancelled('Received reserved attribute.')
+
+ salt = b64decode(data[b's'])
+ iteration_count = int(data[b'i'])
+ nonce = data[b'r']
+
+ if nonce[:len(self.cnonce)] != self.cnonce:
+ raise SASLCancelled('Invalid nonce')
+
+ cbind_data = self.credentials['channel_binding']
+ cbind_input = self.gs2_header + cbind_data
+ channel_binding = b'c=' + b64encode(cbind_input).replace(b'\n', b'')
+
+ client_final_message_without_proof = channel_binding + b',' + \
+ b'r=' + nonce
+
+ salted_password = self.Hi(self.credentials['password'],
+ salt,
+ iteration_count)
+ client_key = self.HMAC(salted_password, b'Client Key')
+ stored_key = self.H(client_key)
+ auth_message = self.client_first_message_bare + b',' + \
+ challenge + b',' + \
+ client_final_message_without_proof
+ client_signature = self.HMAC(stored_key, auth_message)
+ client_proof = XOR(client_key, client_signature)
+ server_key = self.HMAC(salted_password, b'Server Key')
+
+ self.server_signature = self.HMAC(server_key, auth_message)
+
+ client_final_message = client_final_message_without_proof + \
+ b',p=' + b64encode(client_proof)
+
+ return client_final_message
+
+ def process_3(self, challenge):
+ data = self.parse(challenge)
+ verifier = data.get(b'v', None)
+ error = data.get(b'e', 'Unknown error')
+
+ if not verifier:
+ raise SASLFailed(error)
+
+ if b64decode(verifier) != self.server_signature:
+ raise SASLMutualAuthFailed()
+
+ self._mutual_auth = True
+
+ return b''
+
+
+@sasl_mech(30)
+class DIGEST(Mech):
+
+ name = 'DIGEST'
+ use_hashes = True
+ required_credentials = set(['username', 'password', 'realm', 'service', 'host'])
+ optional_credentials = set(['authzid', 'service-name'])
+ security = set(['encrypted', 'unencrypted_digest'])
+
+ def setup(self, name):
+ self.hash_name = name[7:]
+ self.hash = hash(self.hash_name)
+ if self.hash is None:
+ raise SASLCancelled('Unknown hash: %s' % self.hash_name)
+ if not self.security_settings['encrypted']:
+ if not self.security_settings['unencrypted_digest']:
+ raise SASLCancelled('Unencrypted DIGEST')
+
+ self.qops = [b'auth']
+ self.qop = b'auth'
+ self.maxbuf = b'65536'
+ self.nonce = b''
+ self.cnonce = b''
+ self.nonce_count = 1
+
+ def parse(self, challenge=b''):
+ data = {}
+ var_name = b''
+ var_value = b''
+
+ # States: var, new_var, end, quote, escaped_quote
+ state = 'var'
+
+
+ for char in challenge:
+ if sys.version_info >= (3, 0):
+ char = bytes([char])
+
+ if state == 'var':
+ if char.isspace():
+ continue
+ if char == b'=':
+ state = 'value'
+ else:
+ var_name += char
+ elif state == 'value':
+ if char == b'"':
+ state = 'quote'
+ elif char == b',':
+ if var_name:
+ data[var_name.decode('utf-8')] = var_value
+ var_name = b''
+ var_value = b''
+ state = 'var'
+ else:
+ var_value += char
+ elif state == 'escaped':
+ var_value += char
+ elif state == 'quote':
+ if char == b'\\':
+ state = 'escaped'
+ elif char == b'"':
+ state = 'end'
+ else:
+ var_value += char
+ else:
+ if char == b',':
+ if var_name:
+ data[var_name.decode('utf-8')] = var_value
+ var_name = b''
+ var_value = b''
+ state = 'var'
+ else:
+ var_value += char
+
+ if var_name:
+ data[var_name.decode('utf-8')] = var_value
+ var_name = b''
+ var_value = b''
+ state = 'var'
+ return data
+
+ def MAC(self, key, seq, msg):
+ 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 A1(self):
+ username = self.credentials['username']
+ password = self.credentials['password']
+ authzid = self.credentials['authzid']
+ realm = self.credentials['realm']
+
+ a1 = self.hash()
+ a1.update(username + b':' + realm + b':' + password)
+ a1 = a1.digest()
+ a1 += b':' + self.nonce + b':' + self.cnonce
+ if authzid:
+ a1 += b':' + authzid
+
+ return bytes(a1)
+
+ def A2(self, prefix=b''):
+ a2 = prefix + b':' + self.digest_uri()
+ if self.qop in (b'auth-int', b'auth-conf'):
+ a2 += b':00000000000000000000000000000000'
+ return bytes(a2)
+
+ def response(self, prefix=b''):
+ nc = bytes('%08x' % self.nonce_count)
+
+ a1 = bytes(self.hash(self.A1()).hexdigest().lower())
+ a2 = bytes(self.hash(self.A2(prefix)).hexdigest().lower())
+ s = self.nonce + b':' + nc + b':' + self.cnonce + \
+ b':' + self.qop + b':' + a2
+
+ return bytes(self.hash(a1 + b':' + s).hexdigest().lower())
+
+ def digest_uri(self):
+ serv_type = self.credentials['service']
+ serv_name = self.credentials['service-name']
+ host = self.credentials['host']
+
+ uri = serv_type + b'/' + host
+ if serv_name and host != serv_name:
+ uri += b'/' + serv_name
+ return uri
+
+ def respond(self):
+ data = {
+ 'username': quote(self.credentials['username']),
+ 'authzid': quote(self.credentials['authzid']),
+ 'realm': quote(self.credentials['realm']),
+ 'nonce': quote(self.nonce),
+ 'cnonce': quote(self.cnonce),
+ 'nc': bytes('%08x' % self.nonce_count),
+ 'qop': self.qop,
+ 'digest-uri': quote(self.digest_uri()),
+ 'response': self.response(b'AUTHENTICATE'),
+ 'maxbuf': self.maxbuf
+ }
+ resp = b''
+ for key, value in data.items():
+ if value and value != b'""':
+ resp += b',' + bytes(key) + b'=' + bytes(value)
+ return resp[1:]
+
+ def process(self, challenge=b''):
+ if not challenge:
+ if self.cnonce and self.nonce and self.nonce_count and self.qop:
+ self.nonce_count += 1
+ return self.respond()
+ return b''
+
+ data = self.parse(challenge)
+ if 'rspauth' in data:
+ if data['rspauth'] != self.response():
+ raise SASLMutualAuthFailed()
+ else:
+ self.nonce_count = 1
+ self.cnonce = bytes('%s' % random.random())[2:]
+ self.qops = data.get('qop', [b'auth'])
+ self.qop = b'auth'
+ if 'nonce' in data:
+ self.nonce = data['nonce']
+ if 'realm' in data and not self.credentials['realm']:
+ self.credentials['realm'] = data['realm']
+
+ return self.respond()
+
+
+try:
+ import kerberos
+except ImportError:
+ pass
+else:
+ @sasl_mech(75)
+ class GSSAPI(Mech):
+
+ name = 'GSSAPI'
+ required_credentials = set(['username', 'service-name'])
+ optional_credentials = set(['authzid'])
+
+ def setup(self, name):
+ authzid = self.credentials['authzid']
+ if not authzid:
+ authzid = 'xmpp@%s' % self.credentials['service-name']
+
+ _, self.gss = kerberos.authGSSClientInit(authzid)
+ self.step = 0
+
+ def process(self, challenge=b''):
+ b64_challenge = b64encode(challenge)
+ try:
+ if self.step == 0:
+ result = kerberos.authGSSClientStep(self.gss, b64_challenge)
+ if result != kerberos.AUTH_GSS_CONTINUE:
+ self.step = 1
+ elif self.step == 1:
+ username = self.credentials['username']
+
+ kerberos.authGSSClientUnwrap(self.gss, b64_challenge)
+ resp = kerberos.authGSSClientResponse(self.gss)
+ kerberos.authGSSClientWrap(self.gss, resp, username)
+
+ resp = kerberos.authGSSClientResponse(self.gss)
+ except kerberos.GSSError as e:
+ raise SASLCancelled('Kerberos error: %s' % e.message)
+ if not resp:
+ return b''
+ else:
+ return b64decode(resp)
diff --git a/sleekxmpp/util/stringprep_profiles.py b/sleekxmpp/util/stringprep_profiles.py
index 08278d6c..ad89d4cc 100644
--- a/sleekxmpp/util/stringprep_profiles.py
+++ b/sleekxmpp/util/stringprep_profiles.py
@@ -20,19 +20,13 @@ import sys
import stringprep
import unicodedata
+from sleekxmpp.util import unicode
+
class StringPrepError(UnicodeError):
pass
-def to_unicode(data):
- """Ensure that a given string is Unicode, regardless of Python version."""
- if sys.version_info < (3, 0):
- return unicode(data)
- else:
- return str(data)
-
-
def b1_mapping(char):
"""Map characters that are commonly mapped to nothing."""
return '' if stringprep.in_table_b1(char) else None
@@ -143,7 +137,7 @@ def create(nfkc=True, bidi=True, mappings=None,
"""
def profile(data, query=False):
try:
- data = to_unicode(data)
+ data = unicode(data)
except UnicodeError:
raise StringPrepError
diff --git a/sleekxmpp/xmlstream/xmlstream.py b/sleekxmpp/xmlstream/xmlstream.py
index 4cc9e169..51dc25ed 100644
--- a/sleekxmpp/xmlstream/xmlstream.py
+++ b/sleekxmpp/xmlstream/xmlstream.py
@@ -138,6 +138,15 @@ class XMLStream(object):
#: be consulted, even if they are not in the provided file.
self.ca_certs = None
+ #: Path to a file containing a client certificate to use for
+ #: authenticating via SASL EXTERNAL. If set, there must also
+ #: be a corresponding `:attr:keyfile` value.
+ self.certfile = None
+
+ #: Path to a file containing the private key for the selected
+ #: client certificate to use for authenticating via SASL EXTERNAL.
+ self.keyfile = None
+
#: The time in seconds to wait for events from the event queue,
#: and also the time between checks for the process stop signal.
self.wait_timeout = WAIT_TIMEOUT
@@ -499,6 +508,8 @@ class XMLStream(object):
cert_policy = ssl.CERT_REQUIRED
ssl_socket = ssl.wrap_socket(self.socket,
+ certfile=self.certfile,
+ keyfile=self.keyfile,
ca_certs=self.ca_certs,
cert_reqs=cert_policy,
do_handshake_on_connect=False)
@@ -799,6 +810,8 @@ class XMLStream(object):
cert_policy = ssl.CERT_REQUIRED
ssl_socket = ssl.wrap_socket(self.socket,
+ certfile=self.certfile,
+ keyfile=self.keyfile,
ssl_version=self.ssl_version,
do_handshake_on_connect=False,
ca_certs=self.ca_certs,