diff options
Diffstat (limited to 'sleekxmpp/features')
-rw-r--r-- | sleekxmpp/features/__init__.py | 3 | ||||
-rw-r--r-- | sleekxmpp/features/feature_bind/bind.py | 17 | ||||
-rw-r--r-- | sleekxmpp/features/feature_mechanisms/mechanisms.py | 203 | ||||
-rw-r--r-- | sleekxmpp/features/feature_mechanisms/stanza/auth.py | 5 | ||||
-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 | ||||
-rw-r--r-- | sleekxmpp/features/feature_rosterver/rosterver.py | 2 | ||||
-rw-r--r-- | sleekxmpp/features/feature_session/session.py | 2 | ||||
-rw-r--r-- | sleekxmpp/features/feature_starttls/starttls.py | 6 |
13 files changed, 246 insertions, 90 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_bind/bind.py b/sleekxmpp/features/feature_bind/bind.py index 2253d5ae..ee4c1e9b 100644 --- a/sleekxmpp/features/feature_bind/bind.py +++ b/sleekxmpp/features/feature_bind/bind.py @@ -8,10 +8,11 @@ import logging +from sleekxmpp.jid import JID from sleekxmpp.stanza import Iq, StreamFeatures from sleekxmpp.features.feature_bind import stanza from sleekxmpp.xmlstream import register_stanza_plugin -from sleekxmpp.plugins import BasePlugin, register_plugin +from sleekxmpp.plugins import BasePlugin log = logging.getLogger(__name__) @@ -40,25 +41,25 @@ class FeatureBind(BasePlugin): Arguments: features -- The stream features stanza. """ - log.debug("Requesting resource: %s", self.xmpp.boundjid.resource) + log.debug("Requesting resource: %s", self.xmpp.requested_jid.resource) iq = self.xmpp.Iq() iq['type'] = 'set' iq.enable('bind') - if self.xmpp.boundjid.resource: - iq['bind']['resource'] = self.xmpp.boundjid.resource + if self.xmpp.requested_jid.resource: + iq['bind']['resource'] = self.xmpp.requested_jid.resource response = iq.send(now=True) - self.xmpp.set_jid(response['bind']['jid']) + self.xmpp.boundjid = JID(response['bind']['jid'], cache_lock=True) self.xmpp.bound = True - self.xmpp.event('session_bind', self.xmpp.boundjid.full, direct=True) + self.xmpp.event('session_bind', self.xmpp.boundjid, direct=True) self.xmpp.session_bind_event.set() self.xmpp.features.add('bind') - log.info("Node set to: %s", self.xmpp.boundjid.full) + log.info("JID set to: %s", self.xmpp.boundjid.full) if 'session' not in features['features']: log.debug("Established Session") self.xmpp.sessionstarted = True self.xmpp.session_started_event.set() - self.xmpp.event("session_start") + self.xmpp.event('session_start') diff --git a/sleekxmpp/features/feature_mechanisms/mechanisms.py b/sleekxmpp/features/feature_mechanisms/mechanisms.py index 930aa8fe..1d8f8798 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 ssl 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 @@ -29,42 +28,32 @@ class FeatureMechanisms(BasePlugin): description = 'RFC 6120: Stream Feature: SASL' dependencies = set() 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 + } def plugin_init(self): - self.use_mech = self.config.get('use_mech', None) + if self.sasl_callback is None: + self.sasl_callback = self._default_credentials - if not self.use_mech and not self.xmpp.boundjid.user: - self.use_mech = 'ANONYMOUS' + if self.security_callback is None: + self.security_callback = self._default_security - 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) - - sasl_callback = self.config.get('sasl_callback', None) - if sasl_callback is None: - sasl_callback = basic_callback + creds = self.sasl_callback(set(['username']), set()) + if not self.use_mech and not creds['username']: + self.use_mech = 'ANONYMOUS' self.mech = None - self.sasl = suelta.SASL(self.xmpp.boundjid.domain, 'xmpp', - username=self.xmpp.boundjid.user, - sec_query=suelta.sec_query_allow, - request_values=sasl_callback, - tls_active=tls_active, - mech=self.use_mech) - self.mech_list = set() self.attempted_mechs = set() @@ -95,7 +84,51 @@ class FeatureMechanisms(BasePlugin): self.xmpp.register_feature('mechanisms', self._handle_sasl_auth, restart=True, - order=self.config.get('order', 100)) + 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] = creds.get('username', self.xmpp.requested_jid.user) + elif value == 'email': + jid = self.xmpp.requested_jid.bare + result[value] = creds.get('email', jid) + elif value == 'channel_binding': + if hasattr(self.xmpp.socket, 'get_channel_binding'): + result[value] = self.xmpp.socket.get_channel_binding() + else: + log.debug("Channel binding not supported.") + log.debug("Use Python 3.3+ for channel binding and " + \ + "SCRAM-SHA-1-PLUS support") + result[value] = None + elif value == 'host': + result[value] = creds.get('host', self.xmpp.requested_jid.domain) + elif value == 'realm': + result[value] = creds.get('realm', self.xmpp.requested_jid.domain) + elif value == 'service-name': + result[value] = creds.get('service-name', self.xmpp._service_name) + elif value == 'service': + result[value] = creds.get('service', 'xmpp') + elif value in creds: + result[value] = creds[value] + return result + + def _default_security(self, values): + result = {} + for value in values: + if value == 'encrypted': + if 'starttls' in self.xmpp.features: + result[value] = True + elif isinstance(self.xmpp.socket, ssl.SSLSocket): + result[value] = True + else: + result[value] = False + else: + result[value] = self.config.get(value, False) + return result def _handle_sasl_auth(self, features): """ @@ -109,37 +142,62 @@ 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.xmpp.event("failed_auth", direct=True) self.attempted_mechs = set() + return self.xmpp.disconnect() + except StringPrepError: + log.exception("A credential value did not pass SASLprep.") + 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.SASLMutualAuthFailed: + log.error("Mutual authentication failed! " + \ + "A security breach is possible.") + self.attempted_mechs.add(self.mech.name) self.xmpp.disconnect() + except sasl.SASLFailed: + self.attempted_mechs.add(self.mech.name) + self._send_auth() + else: + resp.send(now=True) + return True def _handle_challenge(self, stanza): @@ -147,20 +205,35 @@ 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.SASLMutualAuthFailed: + log.error("Mutual authentication failed! " + \ + "A security breach is possible.") + self.attempted_mechs.add(self.mech.name) + self.xmpp.disconnect() + except sasl.SASLFailed: self.stanza.Abort(self.xmpp).send() else: + if resp.get_value() == '': + resp.del_value() 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..6b6f85a3 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 @@ -41,7 +40,7 @@ class Auth(StanzaBase): if not self['mechanism'] in self.plain_mechs: if values: self.xml.text = bytes(base64.b64encode(values)).decode('utf-8') - else: + elif values == b'': self.xml.text = '=' else: self.xml.text = bytes(values).decode('utf-8') 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..c7106ed3 --- /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 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/features/feature_rosterver/rosterver.py b/sleekxmpp/features/feature_rosterver/rosterver.py index 9e0bb8e8..2991f587 100644 --- a/sleekxmpp/features/feature_rosterver/rosterver.py +++ b/sleekxmpp/features/feature_rosterver/rosterver.py @@ -8,7 +8,7 @@ import logging -from sleekxmpp.stanza import Iq, StreamFeatures +from sleekxmpp.stanza import StreamFeatures from sleekxmpp.features.feature_rosterver import stanza from sleekxmpp.xmlstream import register_stanza_plugin from sleekxmpp.plugins.base import BasePlugin diff --git a/sleekxmpp/features/feature_session/session.py b/sleekxmpp/features/feature_session/session.py index c799a763..ceadd5f3 100644 --- a/sleekxmpp/features/feature_session/session.py +++ b/sleekxmpp/features/feature_session/session.py @@ -51,4 +51,4 @@ class FeatureSession(BasePlugin): log.debug("Established Session") self.xmpp.sessionstarted = True self.xmpp.session_started_event.set() - self.xmpp.event("session_start") + self.xmpp.event('session_start') diff --git a/sleekxmpp/features/feature_starttls/starttls.py b/sleekxmpp/features/feature_starttls/starttls.py index 212b9da5..eb5eee1d 100644 --- a/sleekxmpp/features/feature_starttls/starttls.py +++ b/sleekxmpp/features/feature_starttls/starttls.py @@ -54,13 +54,9 @@ class FeatureSTARTTLS(BasePlugin): return False elif not self.xmpp.use_tls: return False - elif self.xmpp.ssl_support: + else: self.xmpp.send(features['starttls'], now=True) return True - else: - log.warning("The module tlslite is required to log in" + \ - " to some servers, and has not been found.") - return False def _handle_starttls_proceed(self, proceed): """Restart the XML stream when TLS is accepted.""" |