diff options
Diffstat (limited to 'sleekxmpp/features/feature_mechanisms')
9 files changed, 445 insertions, 0 deletions
diff --git a/sleekxmpp/features/feature_mechanisms/__init__.py b/sleekxmpp/features/feature_mechanisms/__init__.py new file mode 100644 index 00000000..5379ef4e --- /dev/null +++ b/sleekxmpp/features/feature_mechanisms/__init__.py @@ -0,0 +1,13 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2011 Nathanael C. Fritz + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from sleekxmpp.features.feature_mechanisms.mechanisms import feature_mechanisms +from sleekxmpp.features.feature_mechanisms.stanza import Mechanisms +from sleekxmpp.features.feature_mechanisms.stanza import Auth +from sleekxmpp.features.feature_mechanisms.stanza import Success +from sleekxmpp.features.feature_mechanisms.stanza import Failure diff --git a/sleekxmpp/features/feature_mechanisms/mechanisms.py b/sleekxmpp/features/feature_mechanisms/mechanisms.py new file mode 100644 index 00000000..deff5d30 --- /dev/null +++ b/sleekxmpp/features/feature_mechanisms/mechanisms.py @@ -0,0 +1,131 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2011 Nathanael C. Fritz + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +import logging + +from sleekxmpp.thirdparty import suelta + +from sleekxmpp.stanza import StreamFeatures +from sleekxmpp.xmlstream import RestartStream, register_stanza_plugin +from sleekxmpp.xmlstream.matcher import * +from sleekxmpp.xmlstream.handler import * +from sleekxmpp.plugins.base import base_plugin +from sleekxmpp.features.feature_mechanisms import stanza + + +log = logging.getLogger(__name__) + + +class feature_mechanisms(base_plugin): + + def plugin_init(self): + self.name = 'SASL Mechanisms' + self.rfc = '6120' + self.description = "SASL Stream Feature" + self.stanza = stanza + + self.use_mech = self.config.get('use_mech', None) + + def tls_active(): + return 'starttls' in self.xmpp.features + + def basic_callback(mech, values): + if 'username' in values: + values['username'] = self.xmpp.boundjid.user + if 'password' in values: + values['password'] = self.xmpp.password + if 'access_token' in values: + values['access_token'] = self.xmpp.password + mech.fulfill(values) + + sasl_callback = self.config.get('sasl_callback', None) + if sasl_callback is None: + sasl_callback = basic_callback + + 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) + + 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_handler( + Callback('SASL Success', + MatchXPath(stanza.Success.tag_name()), + self._handle_success, + instream=True, + once=True)) + self.xmpp.register_handler( + Callback('SASL Failure', + MatchXPath(stanza.Failure.tag_name()), + self._handle_fail, + instream=True, + once=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.config.get('order', 100)) + + 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 + + mech_list = features['mechanisms'] + self.mech = self.sasl.choose_mechanism(mech_list) + + if self.mech is not None: + resp = stanza.Auth(self.xmpp) + resp['mechanism'] = self.mech.name + resp['value'] = self.mech.process() + resp.send(now=True) + else: + log.error("No appropriate login method.") + self.xmpp.event("no_auth", direct=True) + self.xmpp.disconnect() + return True + + def _handle_challenge(self, stanza): + """SASL challenge received. Process and send response.""" + resp = self.stanza.Response(self.xmpp) + resp['value'] = self.mech.process(stanza['value']) + resp.send(now=True) + + def _handle_success(self, stanza): + """SASL authentication succeeded. Restart the stream.""" + self.xmpp.authenticated = True + self.xmpp.features.add('mechanisms') + raise RestartStream() + + def _handle_fail(self, stanza): + """SASL authentication failed. Disconnect and shutdown.""" + log.info("Authentication failed: %s", stanza['condition']) + self.xmpp.event("failed_auth", stanza, direct=True) + self.xmpp.disconnect() + return True diff --git a/sleekxmpp/features/feature_mechanisms/stanza/__init__.py b/sleekxmpp/features/feature_mechanisms/stanza/__init__.py new file mode 100644 index 00000000..8b80f358 --- /dev/null +++ b/sleekxmpp/features/feature_mechanisms/stanza/__init__.py @@ -0,0 +1,15 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2011 Nathanael C. Fritz + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + + +from sleekxmpp.features.feature_mechanisms.stanza.mechanisms import Mechanisms +from sleekxmpp.features.feature_mechanisms.stanza.auth import Auth +from sleekxmpp.features.feature_mechanisms.stanza.success import Success +from sleekxmpp.features.feature_mechanisms.stanza.failure import Failure +from sleekxmpp.features.feature_mechanisms.stanza.challenge import Challenge +from sleekxmpp.features.feature_mechanisms.stanza.response import Response diff --git a/sleekxmpp/features/feature_mechanisms/stanza/auth.py b/sleekxmpp/features/feature_mechanisms/stanza/auth.py new file mode 100644 index 00000000..d2a981f9 --- /dev/null +++ b/sleekxmpp/features/feature_mechanisms/stanza/auth.py @@ -0,0 +1,49 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2011 Nathanael C. Fritz + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +import base64 + +from sleekxmpp.thirdparty.suelta.util import bytes + +from sleekxmpp.stanza import StreamFeatures +from sleekxmpp.xmlstream import ElementBase, StanzaBase, ET +from sleekxmpp.xmlstream import register_stanza_plugin + + +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: + self.xml.text = bytes(base64.b64encode(values)).decode('utf-8') + else: + self.xml.text = bytes(values).decode('utf-8') + + def del_value(self): + self.xml.text = '' diff --git a/sleekxmpp/features/feature_mechanisms/stanza/challenge.py b/sleekxmpp/features/feature_mechanisms/stanza/challenge.py new file mode 100644 index 00000000..82af869f --- /dev/null +++ b/sleekxmpp/features/feature_mechanisms/stanza/challenge.py @@ -0,0 +1,39 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2011 Nathanael C. Fritz + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +import base64 + +from sleekxmpp.thirdparty.suelta.util import bytes + +from sleekxmpp.stanza import StreamFeatures +from sleekxmpp.xmlstream import ElementBase, StanzaBase, ET +from sleekxmpp.xmlstream import register_stanza_plugin + + +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): + self.xml.text = bytes(base64.b64encode(values)).decode('utf-8') + + def del_value(self): + self.xml.text = '' diff --git a/sleekxmpp/features/feature_mechanisms/stanza/failure.py b/sleekxmpp/features/feature_mechanisms/stanza/failure.py new file mode 100644 index 00000000..027cc5af --- /dev/null +++ b/sleekxmpp/features/feature_mechanisms/stanza/failure.py @@ -0,0 +1,78 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2011 Nathanael C. Fritz + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from sleekxmpp.stanza import StreamFeatures +from sleekxmpp.xmlstream import ElementBase, StanzaBase, ET +from sleekxmpp.xmlstream import register_stanza_plugin + + +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.getchildren(): + 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.getchildren(): + 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/sleekxmpp/features/feature_mechanisms/stanza/mechanisms.py b/sleekxmpp/features/feature_mechanisms/stanza/mechanisms.py new file mode 100644 index 00000000..c09cafbd --- /dev/null +++ b/sleekxmpp/features/feature_mechanisms/stanza/mechanisms.py @@ -0,0 +1,55 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2011 Nathanael C. Fritz + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from sleekxmpp.stanza import StreamFeatures +from sleekxmpp.xmlstream import ElementBase, StanzaBase, ET +from sleekxmpp.xmlstream import register_stanza_plugin + + +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/sleekxmpp/features/feature_mechanisms/stanza/response.py b/sleekxmpp/features/feature_mechanisms/stanza/response.py new file mode 100644 index 00000000..45bb8207 --- /dev/null +++ b/sleekxmpp/features/feature_mechanisms/stanza/response.py @@ -0,0 +1,39 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2011 Nathanael C. Fritz + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +import base64 + +from sleekxmpp.thirdparty.suelta.util import bytes + +from sleekxmpp.stanza import StreamFeatures +from sleekxmpp.xmlstream import ElementBase, StanzaBase, ET +from sleekxmpp.xmlstream import register_stanza_plugin + + +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): + self.xml.text = bytes(base64.b64encode(values)).decode('utf-8') + + def del_value(self): + self.xml.text = '' diff --git a/sleekxmpp/features/feature_mechanisms/stanza/success.py b/sleekxmpp/features/feature_mechanisms/stanza/success.py new file mode 100644 index 00000000..028e28a3 --- /dev/null +++ b/sleekxmpp/features/feature_mechanisms/stanza/success.py @@ -0,0 +1,26 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2011 Nathanael C. Fritz + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from sleekxmpp.stanza import StreamFeatures +from sleekxmpp.xmlstream import ElementBase, StanzaBase, ET +from sleekxmpp.xmlstream import register_stanza_plugin + + +class Success(StanzaBase): + + """ + """ + + name = 'success' + 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() |