diff options
author | Lance Stout <lancestout@gmail.com> | 2012-07-31 21:33:19 -0700 |
---|---|---|
committer | Lance Stout <lancestout@gmail.com> | 2012-07-31 21:33:19 -0700 |
commit | 06a9d9fc3075df8e07960716c25d1eca2eb47f05 (patch) | |
tree | 94adfe3467fb50e3710548d6c97f1a6b36ed57a8 /sleekxmpp | |
parent | 44ce01a70b7926a1e1f4af6692be3bdc671f7318 (diff) | |
parent | 1383ca19b50ae7463a1f310f007ed120f811b574 (diff) | |
download | slixmpp-06a9d9fc3075df8e07960716c25d1eca2eb47f05.tar.gz slixmpp-06a9d9fc3075df8e07960716c25d1eca2eb47f05.tar.bz2 slixmpp-06a9d9fc3075df8e07960716c25d1eca2eb47f05.tar.xz slixmpp-06a9d9fc3075df8e07960716c25d1eca2eb47f05.zip |
Merge branch 'master' into develop
Conflicts:
sleekxmpp/thirdparty/__init__.py
Diffstat (limited to 'sleekxmpp')
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, |