diff options
Diffstat (limited to 'sleekxmpp/features')
-rw-r--r-- | sleekxmpp/features/__init__.py | 3 | ||||
-rw-r--r-- | sleekxmpp/features/feature_mechanisms/mechanisms.py | 179 | ||||
-rw-r--r-- | sleekxmpp/features/feature_mechanisms/stanza/auth.py | 3 | ||||
-rw-r--r-- | sleekxmpp/features/feature_mechanisms/stanza/challenge.py | 3 | ||||
-rw-r--r-- | sleekxmpp/features/feature_mechanisms/stanza/response.py | 3 | ||||
-rw-r--r-- | sleekxmpp/features/feature_mechanisms/stanza/success.py | 18 | ||||
-rw-r--r-- | sleekxmpp/features/feature_preapproval/__init__.py | 15 | ||||
-rw-r--r-- | sleekxmpp/features/feature_preapproval/preapproval.py | 42 | ||||
-rw-r--r-- | sleekxmpp/features/feature_preapproval/stanza.py | 17 |
9 files changed, 215 insertions, 68 deletions
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' |