diff options
author | Florent Le Coz <louiz@louiz.org> | 2014-07-17 14:19:04 +0200 |
---|---|---|
committer | Florent Le Coz <louiz@louiz.org> | 2014-07-17 14:19:04 +0200 |
commit | 5ab77c745270d7d5c016c1dc7ef2a82533a4b16e (patch) | |
tree | 259377cc666f8b9c7954fc4e7b8f7a912bcfe101 /slixmpp/features/feature_mechanisms | |
parent | e5582694c07236e6830c20361840360a1dde37f3 (diff) | |
download | slixmpp-5ab77c745270d7d5c016c1dc7ef2a82533a4b16e.tar.gz slixmpp-5ab77c745270d7d5c016c1dc7ef2a82533a4b16e.tar.bz2 slixmpp-5ab77c745270d7d5c016c1dc7ef2a82533a4b16e.tar.xz slixmpp-5ab77c745270d7d5c016c1dc7ef2a82533a4b16e.zip |
Rename to slixmpp
Diffstat (limited to 'slixmpp/features/feature_mechanisms')
10 files changed, 600 insertions, 0 deletions
diff --git a/slixmpp/features/feature_mechanisms/__init__.py b/slixmpp/features/feature_mechanisms/__init__.py new file mode 100644 index 00000000..7532eaa2 --- /dev/null +++ b/slixmpp/features/feature_mechanisms/__init__.py @@ -0,0 +1,22 @@ +""" + Slixmpp: The Slick XMPP Library + Copyright (C) 2011 Nathanael C. Fritz + This file is part of Slixmpp. + + See the file LICENSE for copying permission. +""" + +from slixmpp.plugins.base import register_plugin + +from slixmpp.features.feature_mechanisms.mechanisms import FeatureMechanisms +from slixmpp.features.feature_mechanisms.stanza import Mechanisms +from slixmpp.features.feature_mechanisms.stanza import Auth +from slixmpp.features.feature_mechanisms.stanza import Success +from slixmpp.features.feature_mechanisms.stanza import Failure + + +register_plugin(FeatureMechanisms) + + +# Retain some backwards compatibility +feature_mechanisms = FeatureMechanisms diff --git a/slixmpp/features/feature_mechanisms/mechanisms.py b/slixmpp/features/feature_mechanisms/mechanisms.py new file mode 100644 index 00000000..663bfe57 --- /dev/null +++ b/slixmpp/features/feature_mechanisms/mechanisms.py @@ -0,0 +1,244 @@ +""" + Slixmpp: The Slick XMPP Library + Copyright (C) 2011 Nathanael C. Fritz + This file is part of Slixmpp. + + See the file LICENSE for copying permission. +""" + +import ssl +import logging + +from slixmpp.util import sasl +from slixmpp.util.stringprep_profiles import StringPrepError +from slixmpp.stanza import StreamFeatures +from slixmpp.xmlstream import RestartStream, register_stanza_plugin +from slixmpp.plugins import BasePlugin +from slixmpp.xmlstream.matcher import MatchXPath +from slixmpp.xmlstream.handler import Callback +from slixmpp.features.feature_mechanisms import stanza + + +log = logging.getLogger(__name__) + + +class FeatureMechanisms(BasePlugin): + + name = 'feature_mechanisms' + 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): + if self.sasl_callback is None: + self.sasl_callback = self._default_credentials + + if self.security_callback is None: + self.security_callback = self._default_security + + creds = self.sasl_callback(set(['username']), set()) + if not self.use_mech and not creds['username']: + self.use_mech = 'ANONYMOUS' + + self.mech = None + self.mech_list = set() + self.attempted_mechs = set() + + register_stanza_plugin(StreamFeatures, stanza.Mechanisms) + + self.xmpp.register_stanza(stanza.Success) + self.xmpp.register_stanza(stanza.Failure) + self.xmpp.register_stanza(stanza.Auth) + self.xmpp.register_stanza(stanza.Challenge) + self.xmpp.register_stanza(stanza.Response) + self.xmpp.register_stanza(stanza.Abort) + + self.xmpp.register_handler( + Callback('SASL Success', + MatchXPath(stanza.Success.tag_name()), + self._handle_success, + instream=True)) + self.xmpp.register_handler( + Callback('SASL Failure', + MatchXPath(stanza.Failure.tag_name()), + self._handle_fail, + instream=True)) + self.xmpp.register_handler( + Callback('SASL Challenge', + MatchXPath(stanza.Challenge.tag_name()), + self._handle_challenge)) + + self.xmpp.register_feature('mechanisms', + self._handle_sasl_auth, + restart=True, + order=self.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): + """ + Handle authenticating using SASL. + + Arguments: + features -- The stream features stanza. + """ + if 'mechanisms' in self.xmpp.features: + # SASL authentication has already succeeded, but the + # server has incorrectly offered it again. + return False + + 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 + 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.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() + else: + resp.send(now=True) + + return True + + def _handle_challenge(self, stanza): + """SASL challenge received. Process and send response.""" + resp = self.stanza.Response(self.xmpp) + try: + resp['value'] = self.mech.process(stanza['value']) + except sasl.SASLCancelled: + self.stanza.Abort(self.xmpp).send() + 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: + if resp.get_value() == '': + resp.del_value() + resp.send(now=True) + + def _handle_success(self, stanza): + """SASL authentication succeeded. Restart the stream.""" + 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.""" + self.attempted_mechs.add(self.mech.name) + log.info("Authentication failed: %s", stanza['condition']) + self.xmpp.event("failed_auth", stanza, direct=True) + self._send_auth() + return True diff --git a/slixmpp/features/feature_mechanisms/stanza/__init__.py b/slixmpp/features/feature_mechanisms/stanza/__init__.py new file mode 100644 index 00000000..4d515bf2 --- /dev/null +++ b/slixmpp/features/feature_mechanisms/stanza/__init__.py @@ -0,0 +1,16 @@ +""" + Slixmpp: The Slick XMPP Library + Copyright (C) 2011 Nathanael C. Fritz + This file is part of Slixmpp. + + See the file LICENSE for copying permission. +""" + + +from slixmpp.features.feature_mechanisms.stanza.mechanisms import Mechanisms +from slixmpp.features.feature_mechanisms.stanza.auth import Auth +from slixmpp.features.feature_mechanisms.stanza.success import Success +from slixmpp.features.feature_mechanisms.stanza.failure import Failure +from slixmpp.features.feature_mechanisms.stanza.challenge import Challenge +from slixmpp.features.feature_mechanisms.stanza.response import Response +from slixmpp.features.feature_mechanisms.stanza.abort import Abort diff --git a/slixmpp/features/feature_mechanisms/stanza/abort.py b/slixmpp/features/feature_mechanisms/stanza/abort.py new file mode 100644 index 00000000..fca29aee --- /dev/null +++ b/slixmpp/features/feature_mechanisms/stanza/abort.py @@ -0,0 +1,24 @@ +""" + Slixmpp: The Slick XMPP Library + Copyright (C) 2011 Nathanael C. Fritz + This file is part of Slixmpp. + + See the file LICENSE for copying permission. +""" + +from slixmpp.xmlstream import StanzaBase + + +class Abort(StanzaBase): + + """ + """ + + name = 'abort' + namespace = 'urn:ietf:params:xml:ns:xmpp-sasl' + interfaces = set() + plugin_attrib = name + + def setup(self, xml): + StanzaBase.setup(self, xml) + self.xml.tag = self.tag_name() diff --git a/slixmpp/features/feature_mechanisms/stanza/auth.py b/slixmpp/features/feature_mechanisms/stanza/auth.py new file mode 100644 index 00000000..c32069ec --- /dev/null +++ b/slixmpp/features/feature_mechanisms/stanza/auth.py @@ -0,0 +1,49 @@ +""" + Slixmpp: The Slick XMPP Library + Copyright (C) 2011 Nathanael C. Fritz + This file is part of Slixmpp. + + See the file LICENSE for copying permission. +""" + +import base64 + +from slixmpp.util import bytes +from slixmpp.xmlstream import StanzaBase + + +class Auth(StanzaBase): + + """ + """ + + name = 'auth' + namespace = 'urn:ietf:params:xml:ns:xmpp-sasl' + interfaces = set(('mechanism', 'value')) + plugin_attrib = name + + #: Some SASL mechs require sending values as is, + #: without converting base64. + plain_mechs = set(['X-MESSENGER-OAUTH2']) + + def setup(self, xml): + StanzaBase.setup(self, xml) + self.xml.tag = self.tag_name() + + def get_value(self): + if not self['mechanism'] in self.plain_mechs: + return base64.b64decode(bytes(self.xml.text)) + else: + return self.xml.text + + def set_value(self, values): + if not self['mechanism'] in self.plain_mechs: + if values: + self.xml.text = bytes(base64.b64encode(values)).decode('utf-8') + elif values == b'': + self.xml.text = '=' + else: + self.xml.text = bytes(values).decode('utf-8') + + def del_value(self): + self.xml.text = '' diff --git a/slixmpp/features/feature_mechanisms/stanza/challenge.py b/slixmpp/features/feature_mechanisms/stanza/challenge.py new file mode 100644 index 00000000..21a061ee --- /dev/null +++ b/slixmpp/features/feature_mechanisms/stanza/challenge.py @@ -0,0 +1,39 @@ +""" + Slixmpp: The Slick XMPP Library + Copyright (C) 2011 Nathanael C. Fritz + This file is part of Slixmpp. + + See the file LICENSE for copying permission. +""" + +import base64 + +from slixmpp.util import bytes +from slixmpp.xmlstream import StanzaBase + + +class Challenge(StanzaBase): + + """ + """ + + name = 'challenge' + namespace = 'urn:ietf:params:xml:ns:xmpp-sasl' + interfaces = set(('value',)) + plugin_attrib = name + + def setup(self, xml): + StanzaBase.setup(self, xml) + self.xml.tag = self.tag_name() + + def get_value(self): + return base64.b64decode(bytes(self.xml.text)) + + def set_value(self, values): + 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/slixmpp/features/feature_mechanisms/stanza/failure.py b/slixmpp/features/feature_mechanisms/stanza/failure.py new file mode 100644 index 00000000..cc0ac877 --- /dev/null +++ b/slixmpp/features/feature_mechanisms/stanza/failure.py @@ -0,0 +1,76 @@ +""" + Slixmpp: The Slick XMPP Library + Copyright (C) 2011 Nathanael C. Fritz + This file is part of Slixmpp. + + See the file LICENSE for copying permission. +""" + +from slixmpp.xmlstream import StanzaBase, ET + + +class Failure(StanzaBase): + + """ + """ + + name = 'failure' + namespace = 'urn:ietf:params:xml:ns:xmpp-sasl' + interfaces = set(('condition', 'text')) + plugin_attrib = name + sub_interfaces = set(('text',)) + conditions = set(('aborted', 'account-disabled', 'credentials-expired', + 'encryption-required', 'incorrect-encoding', 'invalid-authzid', + 'invalid-mechanism', 'malformed-request', 'mechansism-too-weak', + 'not-authorized', 'temporary-auth-failure')) + + def setup(self, xml=None): + """ + Populate the stanza object using an optional XML object. + + Overrides ElementBase.setup. + + Sets a default error type and condition, and changes the + parent stanza's type to 'error'. + + Arguments: + xml -- Use an existing XML object for the stanza's values. + """ + # StanzaBase overrides self.namespace + self.namespace = Failure.namespace + + if StanzaBase.setup(self, xml): + #If we had to generate XML then set default values. + self['condition'] = 'not-authorized' + + self.xml.tag = self.tag_name() + + def get_condition(self): + """Return the condition element's name.""" + for child in self.xml: + if "{%s}" % self.namespace in child.tag: + cond = child.tag.split('}', 1)[-1] + if cond in self.conditions: + return cond + return 'not-authorized' + + def set_condition(self, value): + """ + Set the tag name of the condition element. + + Arguments: + value -- The tag name of the condition element. + """ + if value in self.conditions: + del self['condition'] + self.xml.append(ET.Element("{%s}%s" % (self.namespace, value))) + return self + + def del_condition(self): + """Remove the condition element.""" + for child in self.xml: + if "{%s}" % self.condition_ns in child.tag: + tag = child.tag.split('}', 1)[-1] + if tag in self.conditions: + self.xml.remove(child) + return self diff --git a/slixmpp/features/feature_mechanisms/stanza/mechanisms.py b/slixmpp/features/feature_mechanisms/stanza/mechanisms.py new file mode 100644 index 00000000..4437e155 --- /dev/null +++ b/slixmpp/features/feature_mechanisms/stanza/mechanisms.py @@ -0,0 +1,53 @@ +""" + Slixmpp: The Slick XMPP Library + Copyright (C) 2011 Nathanael C. Fritz + This file is part of Slixmpp. + + See the file LICENSE for copying permission. +""" + +from slixmpp.xmlstream import ElementBase, ET + + +class Mechanisms(ElementBase): + + """ + """ + + name = 'mechanisms' + namespace = 'urn:ietf:params:xml:ns:xmpp-sasl' + interfaces = set(('mechanisms', 'required')) + plugin_attrib = name + is_extension = True + + def get_required(self): + """ + """ + return True + + def get_mechanisms(self): + """ + """ + results = [] + mechs = self.findall('{%s}mechanism' % self.namespace) + if mechs: + for mech in mechs: + results.append(mech.text) + return results + + def set_mechanisms(self, values): + """ + """ + self.del_mechanisms() + for val in values: + mech = ET.Element('{%s}mechanism' % self.namespace) + mech.text = val + self.append(mech) + + def del_mechanisms(self): + """ + """ + mechs = self.findall('{%s}mechanism' % self.namespace) + if mechs: + for mech in mechs: + self.xml.remove(mech) diff --git a/slixmpp/features/feature_mechanisms/stanza/response.py b/slixmpp/features/feature_mechanisms/stanza/response.py new file mode 100644 index 00000000..8da236ba --- /dev/null +++ b/slixmpp/features/feature_mechanisms/stanza/response.py @@ -0,0 +1,39 @@ +""" + Slixmpp: The Slick XMPP Library + Copyright (C) 2011 Nathanael C. Fritz + This file is part of Slixmpp. + + See the file LICENSE for copying permission. +""" + +import base64 + +from slixmpp.util import bytes +from slixmpp.xmlstream import StanzaBase + + +class Response(StanzaBase): + + """ + """ + + name = 'response' + namespace = 'urn:ietf:params:xml:ns:xmpp-sasl' + interfaces = set(('value',)) + plugin_attrib = name + + def setup(self, xml): + StanzaBase.setup(self, xml) + self.xml.tag = self.tag_name() + + def get_value(self): + return base64.b64decode(bytes(self.xml.text)) + + def set_value(self, values): + 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/slixmpp/features/feature_mechanisms/stanza/success.py b/slixmpp/features/feature_mechanisms/stanza/success.py new file mode 100644 index 00000000..f7cde0f8 --- /dev/null +++ b/slixmpp/features/feature_mechanisms/stanza/success.py @@ -0,0 +1,38 @@ +""" + Slixmpp: The Slick XMPP Library + Copyright (C) 2011 Nathanael C. Fritz + This file is part of Slixmpp. + + See the file LICENSE for copying permission. +""" + +import base64 + +from slixmpp.util import bytes +from slixmpp.xmlstream import StanzaBase + +class Success(StanzaBase): + + """ + """ + + name = 'success' + namespace = 'urn:ietf:params:xml:ns:xmpp-sasl' + interfaces = set(['value']) + plugin_attrib = name + + def setup(self, xml): + StanzaBase.setup(self, xml) + self.xml.tag = self.tag_name() + + def get_value(self): + return base64.b64decode(bytes(self.xml.text)) + + def set_value(self, values): + if values: + self.xml.text = bytes(base64.b64encode(values)).decode('utf-8') + else: + self.xml.text = '=' + + def del_value(self): + self.xml.text = '' |