diff options
57 files changed, 3147 insertions, 914 deletions
@@ -42,6 +42,9 @@ Main Author: Nathan Fritz fritz@netflint.net Contributors: Kevin Smith & Lance Stout Patches: Remko Tronçon +Dave Cridland, for his Suelta SASL library. + + Feel free to add fritzy@netflint.net to your roster for direct support and comments. Join sleekxmpp-discussion@googlegroups.com / http://groups.google.com/group/sleekxmpp-discussion for email discussion. Join sleek@conference.jabber.org for groupchat discussion. @@ -45,8 +45,6 @@ packages = [ 'sleekxmpp', 'sleekxmpp/xmlstream', 'sleekxmpp/xmlstream/matcher', 'sleekxmpp/xmlstream/handler', - 'sleekxmpp/thirdparty', - 'sleekxmpp/roster', 'sleekxmpp/plugins', 'sleekxmpp/plugins/xep_0009', 'sleekxmpp/plugins/xep_0009/stanza', @@ -54,11 +52,22 @@ packages = [ 'sleekxmpp', 'sleekxmpp/plugins/xep_0030/stanza', 'sleekxmpp/plugins/xep_0050', 'sleekxmpp/plugins/xep_0059', + 'sleekxmpp/plugins/xep_0060', + 'sleekxmpp/plugins/xep_0060/stanza', 'sleekxmpp/plugins/xep_0085', 'sleekxmpp/plugins/xep_0086', 'sleekxmpp/plugins/xep_0092', 'sleekxmpp/plugins/xep_0128', 'sleekxmpp/plugins/xep_0199', + 'sleekxmpp/features', + 'sleekxmpp/features/feature_mechanisms', + 'sleekxmpp/features/feature_mechanisms/stanza', + 'sleekxmpp/features/feature_starttls', + 'sleekxmpp/features/feature_bind', + 'sleekxmpp/features/feature_session', + 'sleekxmpp/thirdparty', + 'sleekxmpp/thirdparty/suelta', + 'sleekxmpp/thirdparty/suelta/mechanisms', ] if sys.version_info < (3, 0): @@ -80,4 +89,3 @@ setup( py_modules = py_modules, requires = [ 'tlslite', 'pythondns' ], ) - diff --git a/sleekxmpp/basexmpp.py b/sleekxmpp/basexmpp.py index ef6906a6..c2267535 100644 --- a/sleekxmpp/basexmpp.py +++ b/sleekxmpp/basexmpp.py @@ -93,6 +93,7 @@ class BaseXMPP(XMLStream): # Deprecated method names are re-mapped for backwards compatibility. self.default_ns = default_ns self.stream_ns = 'http://etherx.jabber.org/streams' + self.namespace_map[self.stream_ns] = 'stream' self.boundjid = JID(jid) @@ -110,6 +111,8 @@ class BaseXMPP(XMLStream): self.sentpresence = False + self.stanza = sleekxmpp.stanza + self.register_handler( Callback('IM', MatchXPath('{%s}message/{%s}body' % (self.default_ns, @@ -187,9 +190,14 @@ class BaseXMPP(XMLStream): try: # Import the given module that contains the plugin. if not module: - module = sleekxmpp.plugins - module = __import__("%s.%s" % (module.__name__, plugin), - globals(), locals(), [plugin]) + try: + module = sleekxmpp.plugins + module = __import__(str("%s.%s" % (module.__name__, plugin)), + globals(), locals(), [str(plugin)]) + except ImportError: + module = sleekxmpp.features + module = __import__(str("%s.%s" % (module.__name__, plugin)), + globals(), locals(), [str(plugin)]) if isinstance(module, str): # We probably want to load a module from outside # the sleekxmpp package, so leave out the globals(). @@ -198,12 +206,14 @@ class BaseXMPP(XMLStream): # Load the plugin class from the module. self.plugin[plugin] = getattr(module, plugin)(self, pconfig) - # Let XEP implementing plugins have some extra logging info. - xep = '' - if hasattr(self.plugin[plugin], 'xep'): - xep = "(XEP-%s) " % self.plugin[plugin].xep + # Let XEP/RFC implementing plugins have some extra logging info. + spec = '(CUSTOM) ' + if self.plugin[plugin].xep: + spec = "(XEP-%s) " % self.plugin[plugin].xep + elif self.plugin[plugin].rfc: + spec = "(RFC-%s) " % self.plugin[plugin].rfc - desc = (xep, self.plugin[plugin].description) + desc = (spec, self.plugin[plugin].description) log.debug("Loaded Plugin %s%s" % desc) except: log.exception("Unable to load plugin: %s", plugin) diff --git a/sleekxmpp/clientxmpp.py b/sleekxmpp/clientxmpp.py index 0328e393..8193d0a0 100644 --- a/sleekxmpp/clientxmpp.py +++ b/sleekxmpp/clientxmpp.py @@ -15,12 +15,14 @@ import hashlib import random import threading +import sleekxmpp from sleekxmpp import plugins from sleekxmpp import stanza +from sleekxmpp import features from sleekxmpp.basexmpp import BaseXMPP -from sleekxmpp.stanza import Message, Presence, Iq +from sleekxmpp.stanza import * from sleekxmpp.xmlstream import XMLStream, RestartStream -from sleekxmpp.xmlstream import StanzaBase, ET +from sleekxmpp.xmlstream import StanzaBase, ET, register_stanza_plugin from sleekxmpp.xmlstream.matcher import * from sleekxmpp.xmlstream.handler import * @@ -81,15 +83,19 @@ class ClientXMPP(BaseXMPP): "xmlns='%s'" % self.default_ns) self.stream_footer = "</stream:stream>" - self.features = [] - self.registered_features = [] + self.features = set() + self._stream_feature_handlers = {} + self._stream_feature_order = [] #TODO: Use stream state here self.authenticated = False self.sessionstarted = False self.bound = False self.bindfail = False - self.add_event_handler('connected', self.handle_connected) + + self.add_event_handler('connected', self._handle_connected) + + self.register_stanza(StreamFeatures) self.register_handler( Callback('Stream Features', @@ -102,32 +108,11 @@ class ClientXMPP(BaseXMPP): 'jabber:iq:roster')), self._handle_roster)) - self.register_feature( - "<starttls xmlns='urn:ietf:params:xml:ns:xmpp-tls' />", - self._handle_starttls, True) - self.register_feature( - "<mechanisms xmlns='urn:ietf:params:xml:ns:xmpp-sasl' />", - self._handle_sasl_auth, True) - self.register_feature( - "<bind xmlns='urn:ietf:params:xml:ns:xmpp-bind' />", - self._handle_bind_resource) - self.register_feature( - "<session xmlns='urn:ietf:params:xml:ns:xmpp-session' />", - self._handle_start_session) - - def handle_connected(self, event=None): - #TODO: Use stream state here - self.authenticated = False - self.sessionstarted = False - self.bound = False - self.bindfail = False - self.schedule("session timeout checker", 15, - self._session_timeout_check) - - def _session_timeout_check(self): - if not self.session_started_event.isSet(): - log.debug("Session start has taken more than 15 seconds") - self.disconnect(reconnect=self.auto_reconnect) + # Setup default stream features + self.register_plugin('feature_starttls') + self.register_plugin('feature_mechanisms') + self.register_plugin('feature_bind') + self.register_plugin('feature_session') def connect(self, address=tuple(), reattempt=True, use_tls=True): """ @@ -194,19 +179,22 @@ class ClientXMPP(BaseXMPP): return XMLStream.connect(self, address[0], address[1], use_tls=use_tls, reattempt=reattempt) - def register_feature(self, mask, pointer, breaker=False): + def register_feature(self, name, handler, restart=False, order=5000): """ Register a stream feature. Arguments: - mask -- An XML string matching the feature's element. - pointer -- The function to execute if the feature is received. - breaker -- Indicates if feature processing should halt with + name -- The name of the stream feature. + handler -- The function to execute if the feature is received. + restart -- Indicates if feature processing should halt with this feature. Defaults to False. + order -- The relative ordering in which the feature should + be negotiated. Lower values will be attempted + earlier when available. """ - self.registered_features.append((MatchXMLMask(mask), - pointer, - breaker)) + self._stream_feature_handlers[name] = (handler, restart) + self._stream_feature_order.append((order, name)) + self._stream_feature_order.sort() def update_roster(self, jid, name=None, subscription=None, groups=[], block=True, timeout=None, callback=None): @@ -271,179 +259,35 @@ class ClientXMPP(BaseXMPP): else: return self._handle_roster(response, request=True) - def _handle_stream_features(self, features): - """ - Process the received stream features. - - Arguments: - features -- The features stanza. - """ - # Record all of the features. - self.features = [] - for sub in features.xml: - self.features.append(sub.tag) - - # Process the features. - for sub in features.xml: - for feature in self.registered_features: - mask, handler, halt = feature - if mask.match(sub): - if handler(sub) and halt: - # Don't continue if the feature was - # marked as a breaker. - return True - - def _handle_starttls(self, xml): - """ - Handle notification that the server supports TLS. - - Arguments: - xml -- The STARTLS proceed element. - """ - if not self.use_tls: - return False - elif not self.authenticated and self.ssl_support: - tls_ns = 'urn:ietf:params:xml:ns:xmpp-tls' - self.add_handler("<proceed xmlns='%s' />" % tls_ns, - self._handle_tls_start, - name='TLS Proceed', - instream=True) - self.send_xml(xml, 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_tls_start(self, xml): - """ - Handle encrypting the stream using TLS. - - Restarts the stream. - """ - log.debug("Starting TLS") - if self.start_tls(): - raise RestartStream() - - def _handle_sasl_auth(self, xml): - """ - Handle authenticating using SASL. - - Arguments: - xml -- The SASL mechanisms stanza. - """ - if self.use_tls and \ - '{urn:ietf:params:xml:ns:xmpp-tls}starttls' in self.features: - return False - - log.debug("Starting SASL Auth") - sasl_ns = 'urn:ietf:params:xml:ns:xmpp-sasl' - self.add_handler("<success xmlns='%s' />" % sasl_ns, - self._handle_auth_success, - name='SASL Sucess', - instream=True) - self.add_handler("<failure xmlns='%s' />" % sasl_ns, - self._handle_auth_fail, - name='SASL Failure', - instream=True) - - sasl_mechs = xml.findall('{%s}mechanism' % sasl_ns) - if sasl_mechs: - for sasl_mech in sasl_mechs: - self.features.append("sasl:%s" % sasl_mech.text) - if 'sasl:PLAIN' in self.features and self.boundjid.user: - if sys.version_info < (3, 0): - user = bytes(self.boundjid.user) - password = bytes(self.password) - else: - user = bytes(self.boundjid.user, 'utf-8') - password = bytes(self.password, 'utf-8') - - auth = base64.b64encode(b'\x00' + user + \ - b'\x00' + password).decode('utf-8') - - self.send("<auth xmlns='%s' mechanism='PLAIN'>%s</auth>" % ( - sasl_ns, - auth), - now=True) - elif 'sasl:ANONYMOUS' in self.features and not self.boundjid.user: - self.send("<auth xmlns='%s' mechanism='%s' />" % ( - sasl_ns, - 'ANONYMOUS'), - now=True) - else: - log.error("No appropriate login method.") - self.disconnect() - return True - - def _handle_auth_success(self, xml): - """ - SASL authentication succeeded. Restart the stream. - - Arguments: - xml -- The SASL authentication success element. - """ - self.authenticated = True - self.features = [] - raise RestartStream() - - def _handle_auth_fail(self, xml): - """ - SASL authentication failed. Disconnect and shutdown. + def _handle_connected(self, event=None): + #TODO: Use stream state here + self.authenticated = False + self.sessionstarted = False + self.bound = False + self.bindfail = False + self.features = set() - Arguments: - xml -- The SASL authentication failure element. - """ - log.info("Authentication failed.") - self.event("failed_auth", direct=True) - self.disconnect() + def session_timeout(): + if not self.session_started_event.isSet(): + log.debug("Session start has taken more than 15 seconds") + self.disconnect(reconnect=self.auto_reconnect) - def _handle_bind_resource(self, xml): - """ - Handle requesting a specific resource. + self.schedule("session timeout checker", 15, session_timeout) - Arguments: - xml -- The bind feature element. - """ - log.debug("Requesting resource: %s" % self.boundjid.resource) - xml.clear() - iq = self.Iq(stype='set') - if self.boundjid.resource: - res = ET.Element('resource') - res.text = self.boundjid.resource - xml.append(res) - iq.append(xml) - response = iq.send(now=True) - - bind_ns = 'urn:ietf:params:xml:ns:xmpp-bind' - self.set_jid(response.xml.find('{%s}bind/{%s}jid' % (bind_ns, - bind_ns)).text) - self.bound = True - log.info("Node set to: %s" % self.boundjid.full) - session_ns = 'urn:ietf:params:xml:ns:xmpp-session' - if "{%s}session" % session_ns not in self.features or self.bindfail: - log.debug("Established Session") - self.sessionstarted = True - self.session_started_event.set() - self.event("session_start") - - def _handle_start_session(self, xml): + def _handle_stream_features(self, features): """ - Handle the start of the session. + Process the received stream features. Arguments: - xml -- The session feature element. + features -- The features stanza. """ - if self.authenticated and self.bound: - iq = self.makeIqSet(xml) - response = iq.send(now=True) - log.debug("Established Session") - self.sessionstarted = True - self.session_started_event.set() - self.event("session_start") - else: - # Bind probably hasn't happened yet. - self.bindfail = True + for order, name in self._stream_feature_order: + if name in features['features']: + handler, restart = self._stream_feature_handlers[name] + if handler(features) and restart: + # Don't continue if the feature requires + # restarting the XML stream. + return True def _handle_roster(self, iq, request=False): """ diff --git a/sleekxmpp/features/__init__.py b/sleekxmpp/features/__init__.py new file mode 100644 index 00000000..5c86cfea --- /dev/null +++ b/sleekxmpp/features/__init__.py @@ -0,0 +1,11 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2011 Nathanael C. Fritz + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +__all__ = ['feature_starttls', 'feature_mechanisms', + 'feature_bind', 'feature_session', + 'sasl_plain', 'sasl_anonymous'] diff --git a/sleekxmpp/features/feature_bind/__init__.py b/sleekxmpp/features/feature_bind/__init__.py new file mode 100644 index 00000000..aa854f87 --- /dev/null +++ b/sleekxmpp/features/feature_bind/__init__.py @@ -0,0 +1,10 @@ +""" + 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_bind.bind import feature_bind +from sleekxmpp.features.feature_bind.stanza import Bind diff --git a/sleekxmpp/features/feature_bind/bind.py b/sleekxmpp/features/feature_bind/bind.py new file mode 100644 index 00000000..de03192c --- /dev/null +++ b/sleekxmpp/features/feature_bind/bind.py @@ -0,0 +1,64 @@ +""" + 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.stanza import Iq, StreamFeatures +from sleekxmpp.features.feature_bind import stanza +from sleekxmpp.xmlstream import register_stanza_plugin +from sleekxmpp.xmlstream.matcher import * +from sleekxmpp.xmlstream.handler import * +from sleekxmpp.plugins.base import base_plugin + + +log = logging.getLogger(__name__) + + +class feature_bind(base_plugin): + + def plugin_init(self): + self.name = 'Bind Resource' + self.rfc = '6120' + self.description = 'Resource Binding Stream Feature' + self.stanza = stanza + + self.xmpp.register_feature('bind', + self._handle_bind_resource, + restart=False, + order=10000) + + register_stanza_plugin(Iq, stanza.Bind) + register_stanza_plugin(StreamFeatures, stanza.Bind) + + def _handle_bind_resource(self, features): + """ + Handle requesting a specific resource. + + Arguments: + features -- The stream features stanza. + """ + log.debug("Requesting resource: %s" % self.xmpp.boundjid.resource) + iq = self.xmpp.Iq() + iq['type'] = 'set' + iq.enable('bind') + if self.xmpp.boundjid.resource: + iq['bind']['resource'] = self.xmpp.boundjid.resource + response = iq.send(now=True) + + self.xmpp.set_jid(response['bind']['jid']) + self.xmpp.bound = True + + self.xmpp.features.add('bind') + + log.info("Node 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") diff --git a/sleekxmpp/features/feature_bind/stanza.py b/sleekxmpp/features/feature_bind/stanza.py new file mode 100644 index 00000000..2c1484e0 --- /dev/null +++ b/sleekxmpp/features/feature_bind/stanza.py @@ -0,0 +1,22 @@ +""" + 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 Iq, StreamFeatures +from sleekxmpp.xmlstream import ElementBase, ET, register_stanza_plugin + + +class Bind(ElementBase): + + """ + """ + + name = 'bind' + namespace = 'urn:ietf:params:xml:ns:xmpp-bind' + interfaces = set(('resource', 'jid')) + sub_interfaces = interfaces + plugin_attrib = 'bind' 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..d60818bb --- /dev/null +++ b/sleekxmpp/features/feature_mechanisms/mechanisms.py @@ -0,0 +1,127 @@ +""" + 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 + + + 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 + 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) + + 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..e069b57f --- /dev/null +++ b/sleekxmpp/features/feature_mechanisms/stanza/auth.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 Auth(StanzaBase): + + """ + """ + + name = 'auth' + namespace = 'urn:ietf:params:xml:ns:xmpp-sasl' + interfaces = set(('mechanism', '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/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..1189cd80 --- /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() diff --git a/sleekxmpp/features/feature_session/__init__.py b/sleekxmpp/features/feature_session/__init__.py new file mode 100644 index 00000000..3c84baed --- /dev/null +++ b/sleekxmpp/features/feature_session/__init__.py @@ -0,0 +1,10 @@ +""" + 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_session.session import feature_session +from sleekxmpp.features.feature_session.stanza import Session diff --git a/sleekxmpp/features/feature_session/session.py b/sleekxmpp/features/feature_session/session.py new file mode 100644 index 00000000..0daec5da --- /dev/null +++ b/sleekxmpp/features/feature_session/session.py @@ -0,0 +1,56 @@ +""" + 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.stanza import Iq, StreamFeatures +from sleekxmpp.xmlstream import register_stanza_plugin +from sleekxmpp.xmlstream.matcher import * +from sleekxmpp.xmlstream.handler import * +from sleekxmpp.plugins.base import base_plugin + +from sleekxmpp.features.feature_session import stanza + + +log = logging.getLogger(__name__) + + +class feature_session(base_plugin): + + def plugin_init(self): + self.name = 'Start Session' + self.rfc = '3920' + self.description = 'Start Session Stream Feature' + self.stanza = stanza + + self.xmpp.register_feature('session', + self._handle_start_session, + restart=False, + order=10001) + + register_stanza_plugin(Iq, stanza.Session) + register_stanza_plugin(StreamFeatures, stanza.Session) + + def _handle_start_session(self, features): + """ + Handle the start of the session. + + Arguments: + feature -- The stream features element. + """ + iq = self.xmpp.Iq() + iq['type'] = 'set' + iq.enable('session') + response = iq.send(now=True) + + self.xmpp.features.add('session') + + log.debug("Established Session") + self.xmpp.sessionstarted = True + self.xmpp.session_started_event.set() + self.xmpp.event("session_start") diff --git a/sleekxmpp/features/feature_session/stanza.py b/sleekxmpp/features/feature_session/stanza.py new file mode 100644 index 00000000..40ea583d --- /dev/null +++ b/sleekxmpp/features/feature_session/stanza.py @@ -0,0 +1,21 @@ +""" + 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 Iq, StreamFeatures +from sleekxmpp.xmlstream import ElementBase, ET, register_stanza_plugin + + +class Session(ElementBase): + + """ + """ + + name = 'session' + namespace = 'urn:ietf:params:xml:ns:xmpp-session' + interfaces = set() + plugin_attrib = 'session' diff --git a/sleekxmpp/features/feature_starttls/__init__.py b/sleekxmpp/features/feature_starttls/__init__.py new file mode 100644 index 00000000..4ae89433 --- /dev/null +++ b/sleekxmpp/features/feature_starttls/__init__.py @@ -0,0 +1,10 @@ +""" + 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_starttls.starttls import feature_starttls +from sleekxmpp.features.feature_starttls.stanza import * diff --git a/sleekxmpp/features/feature_starttls/stanza.py b/sleekxmpp/features/feature_starttls/stanza.py new file mode 100644 index 00000000..8b09ad94 --- /dev/null +++ b/sleekxmpp/features/feature_starttls/stanza.py @@ -0,0 +1,47 @@ +""" + 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 StanzaBase, ElementBase +from sleekxmpp.xmlstream import register_stanza_plugin + + +class STARTTLS(ElementBase): + + """ + """ + + name = 'starttls' + namespace = 'urn:ietf:params:xml:ns:xmpp-tls' + interfaces = set(('required',)) + plugin_attrib = name + + def get_required(self): + """ + """ + return True + + +class Proceed(StanzaBase): + + """ + """ + + name = 'proceed' + namespace = 'urn:ietf:params:xml:ns:xmpp-tls' + interfaces = set() + + +class Failure(StanzaBase): + + """ + """ + + name = 'failure' + namespace = 'urn:ietf:params:xml:ns:xmpp-tls' + interfaces = set() diff --git a/sleekxmpp/features/feature_starttls/starttls.py b/sleekxmpp/features/feature_starttls/starttls.py new file mode 100644 index 00000000..639788a0 --- /dev/null +++ b/sleekxmpp/features/feature_starttls/starttls.py @@ -0,0 +1,70 @@ +""" + 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.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_starttls import stanza + + +log = logging.getLogger(__name__) + + +class feature_starttls(base_plugin): + + def plugin_init(self): + self.name = "STARTTLS" + self.rfc = '6120' + self.description = "STARTTLS Stream Feature" + self.stanza = stanza + + self.xmpp.register_handler( + Callback('STARTTLS Proceed', + MatchXPath(stanza.Proceed.tag_name()), + self._handle_starttls_proceed, + instream=True)) + self.xmpp.register_feature('starttls', + self._handle_starttls, + restart=True, + order=self.config.get('order', 0)) + + self.xmpp.register_stanza(stanza.Proceed) + self.xmpp.register_stanza(stanza.Failure) + register_stanza_plugin(StreamFeatures, stanza.STARTTLS) + + def _handle_starttls(self, features): + """ + Handle notification that the server supports TLS. + + Arguments: + features -- The stream:features element. + """ + if 'starttls' in self.xmpp.features: + # We have already negotiated TLS, but the server is + # offering it again, against spec. + return False + elif not self.xmpp.use_tls: + return False + elif self.xmpp.ssl_support: + 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.""" + log.debug("Starting TLS") + if self.xmpp.start_tls(): + self.xmpp.features.add('starttls') + raise RestartStream() diff --git a/sleekxmpp/plugins/base.py b/sleekxmpp/plugins/base.py index 2dd68c8d..561421d8 100644 --- a/sleekxmpp/plugins/base.py +++ b/sleekxmpp/plugins/base.py @@ -66,7 +66,8 @@ class base_plugin(object): """ if config is None: config = {} - self.xep = 'base' + self.xep = None + self.rfc = None self.description = 'Base Plugin' self.xmpp = xmpp self.config = config diff --git a/sleekxmpp/plugins/xep_0060.py b/sleekxmpp/plugins/old_0060.py index 93124fca..93124fca 100644 --- a/sleekxmpp/plugins/xep_0060.py +++ b/sleekxmpp/plugins/old_0060.py diff --git a/sleekxmpp/plugins/stanza_pubsub.py b/sleekxmpp/plugins/stanza_pubsub.py deleted file mode 100644 index b5964537..00000000 --- a/sleekxmpp/plugins/stanza_pubsub.py +++ /dev/null @@ -1,557 +0,0 @@ -from .. xmlstream.stanzabase import registerStanzaPlugin, ElementBase, ET, JID -from .. stanza.iq import Iq -from .. stanza.message import Message -from .. basexmpp import basexmpp -from .. xmlstream.xmlstream import XMLStream -import logging -from . import xep_0004 - - -class PubsubState(ElementBase): - namespace = 'http://jabber.org/protocol/psstate' - name = 'state' - plugin_attrib = 'psstate' - interfaces = set(('node', 'item', 'payload')) - plugin_attrib_map = {} - plugin_tag_map = {} - - def setPayload(self, value): - self.xml.append(value) - - def getPayload(self): - childs = self.xml.getchildren() - if len(childs) > 0: - return childs[0] - - def delPayload(self): - for child in self.xml.getchildren(): - self.xml.remove(child) - -registerStanzaPlugin(Iq, PubsubState) - -class PubsubStateEvent(ElementBase): - namespace = 'http://jabber.org/protocol/psstate#event' - name = 'event' - plugin_attrib = 'psstate_event' - intefaces = set(tuple()) - plugin_attrib_map = {} - plugin_tag_map = {} - -registerStanzaPlugin(Message, PubsubStateEvent) -registerStanzaPlugin(PubsubStateEvent, PubsubState) - -class Pubsub(ElementBase): - namespace = 'http://jabber.org/protocol/pubsub' - name = 'pubsub' - plugin_attrib = 'pubsub' - interfaces = set(tuple()) - plugin_attrib_map = {} - plugin_tag_map = {} - -registerStanzaPlugin(Iq, Pubsub) - -class PubsubOwner(ElementBase): - namespace = 'http://jabber.org/protocol/pubsub#owner' - name = 'pubsub' - plugin_attrib = 'pubsub_owner' - interfaces = set(tuple()) - plugin_attrib_map = {} - plugin_tag_map = {} - -registerStanzaPlugin(Iq, PubsubOwner) - -class Affiliation(ElementBase): - namespace = 'http://jabber.org/protocol/pubsub' - name = 'affiliation' - plugin_attrib = name - interfaces = set(('node', 'affiliation')) - plugin_attrib_map = {} - plugin_tag_map = {} - -class Affiliations(ElementBase): - namespace = 'http://jabber.org/protocol/pubsub' - name = 'affiliations' - plugin_attrib = 'affiliations' - interfaces = set(tuple()) - plugin_attrib_map = {} - plugin_tag_map = {} - subitem = (Affiliation,) - - def append(self, affiliation): - if not isinstance(affiliation, Affiliation): - raise TypeError - self.xml.append(affiliation.xml) - return self.iterables.append(affiliation) - -registerStanzaPlugin(Pubsub, Affiliations) - - -class Subscription(ElementBase): - namespace = 'http://jabber.org/protocol/pubsub' - name = 'subscription' - plugin_attrib = name - interfaces = set(('jid', 'node', 'subscription', 'subid')) - plugin_attrib_map = {} - plugin_tag_map = {} - - def setjid(self, value): - self._setattr('jid', str(value)) - - def getjid(self): - return jid(self._getattr('jid')) - -registerStanzaPlugin(Pubsub, Subscription) - -class Subscriptions(ElementBase): - namespace = 'http://jabber.org/protocol/pubsub' - name = 'subscriptions' - plugin_attrib = 'subscriptions' - interfaces = set(tuple()) - plugin_attrib_map = {} - plugin_tag_map = {} - subitem = (Subscription,) - -registerStanzaPlugin(Pubsub, Subscriptions) - -class OptionalSetting(object): - interfaces = set(('required',)) - - def setRequired(self, value): - value = bool(value) - if value and not self['required']: - self.xml.append(ET.Element("{%s}required" % self.namespace)) - elif not value and self['required']: - self.delRequired() - - def getRequired(self): - required = self.xml.find("{%s}required" % self.namespace) - if required is not None: - return True - else: - return False - - def delRequired(self): - required = self.xml.find("{%s}required" % self.namespace) - if required is not None: - self.xml.remove(required) - - -class SubscribeOptions(ElementBase, OptionalSetting): - namespace = 'http://jabber.org/protocol/pubsub' - name = 'subscribe-options' - plugin_attrib = 'suboptions' - plugin_attrib_map = {} - plugin_tag_map = {} - interfaces = set(('required',)) - -registerStanzaPlugin(Subscription, SubscribeOptions) - -class Item(ElementBase): - namespace = 'http://jabber.org/protocol/pubsub' - name = 'item' - plugin_attrib = name - interfaces = set(('id', 'payload')) - plugin_attrib_map = {} - plugin_tag_map = {} - - def setPayload(self, value): - self.xml.append(value) - - def getPayload(self): - childs = self.xml.getchildren() - if len(childs) > 0: - return childs[0] - - def delPayload(self): - for child in self.xml.getchildren(): - self.xml.remove(child) - -class Items(ElementBase): - namespace = 'http://jabber.org/protocol/pubsub' - name = 'items' - plugin_attrib = 'items' - interfaces = set(('node',)) - plugin_attrib_map = {} - plugin_tag_map = {} - subitem = (Item,) - -registerStanzaPlugin(Pubsub, Items) - -class Create(ElementBase): - namespace = 'http://jabber.org/protocol/pubsub' - name = 'create' - plugin_attrib = name - interfaces = set(('node',)) - plugin_attrib_map = {} - plugin_tag_map = {} - -registerStanzaPlugin(Pubsub, Create) - -#class Default(ElementBase): -# namespace = 'http://jabber.org/protocol/pubsub' -# name = 'default' -# plugin_attrib = name -# interfaces = set(('node', 'type')) -# plugin_attrib_map = {} -# plugin_tag_map = {} -# -# def getType(self): -# t = self._getAttr('type') -# if not t: t == 'leaf' -# return t -# -#registerStanzaPlugin(Pubsub, Default) - -class Publish(Items): - namespace = 'http://jabber.org/protocol/pubsub' - name = 'publish' - plugin_attrib = name - interfaces = set(('node',)) - plugin_attrib_map = {} - plugin_tag_map = {} - subitem = (Item,) - -registerStanzaPlugin(Pubsub, Publish) - -class Retract(Items): - namespace = 'http://jabber.org/protocol/pubsub' - name = 'retract' - plugin_attrib = name - interfaces = set(('node', 'notify')) - plugin_attrib_map = {} - plugin_tag_map = {} - -registerStanzaPlugin(Pubsub, Retract) - -class Unsubscribe(ElementBase): - namespace = 'http://jabber.org/protocol/pubsub' - name = 'unsubscribe' - plugin_attrib = name - interfaces = set(('node', 'jid')) - plugin_attrib_map = {} - plugin_tag_map = {} - - def setJid(self, value): - self._setAttr('jid', str(value)) - - def getJid(self): - return JID(self._getAttr('jid')) - -registerStanzaPlugin(Pubsub, Unsubscribe) - -class Subscribe(ElementBase): - namespace = 'http://jabber.org/protocol/pubsub' - name = 'subscribe' - plugin_attrib = name - interfaces = set(('node', 'jid')) - plugin_attrib_map = {} - plugin_tag_map = {} - - def setJid(self, value): - self._setAttr('jid', str(value)) - - def getJid(self): - return JID(self._getAttr('jid')) - -registerStanzaPlugin(Pubsub, Subscribe) - -class Configure(ElementBase): - namespace = 'http://jabber.org/protocol/pubsub' - name = 'configure' - plugin_attrib = name - interfaces = set(('node', 'type')) - plugin_attrib_map = {} - plugin_tag_map = {} - - def getType(self): - t = self._getAttr('type') - if not t: t == 'leaf' - return t - -registerStanzaPlugin(Pubsub, Configure) -registerStanzaPlugin(Configure, xep_0004.Form) - -class DefaultConfig(ElementBase): - namespace = 'http://jabber.org/protocol/pubsub#owner' - name = 'default' - plugin_attrib = 'default' - interfaces = set(('node', 'type', 'config')) - plugin_attrib_map = {} - plugin_tag_map = {} - - def __init__(self, *args, **kwargs): - ElementBase.__init__(self, *args, **kwargs) - - def getType(self): - t = self._getAttr('type') - if not t: t = 'leaf' - return t - - def getConfig(self): - return self['form'] - - def setConfig(self, value): - self['form'].setStanzaValues(value.getStanzaValues()) - return self - -registerStanzaPlugin(PubsubOwner, DefaultConfig) -registerStanzaPlugin(DefaultConfig, xep_0004.Form) - -class Options(ElementBase): - namespace = 'http://jabber.org/protocol/pubsub' - name = 'options' - plugin_attrib = 'options' - interfaces = set(('jid', 'node', 'options')) - plugin_attrib_map = {} - plugin_tag_map = {} - - def __init__(self, *args, **kwargs): - ElementBase.__init__(self, *args, **kwargs) - - def getOptions(self): - config = self.xml.find('{jabber:x:data}x') - form = xep_0004.Form() - if config is not None: - form.fromXML(config) - return form - - def setOptions(self, value): - self.xml.append(value.getXML()) - return self - - def delOptions(self): - config = self.xml.find('{jabber:x:data}x') - self.xml.remove(config) - - def setJid(self, value): - self._setAttr('jid', str(value)) - - def getJid(self): - return JID(self._getAttr('jid')) - -registerStanzaPlugin(Pubsub, Options) -registerStanzaPlugin(Subscribe, Options) - -class OwnerAffiliations(Affiliations): - namespace = 'http://jabber.org/protocol/pubsub#owner' - interfaces = set(('node')) - plugin_attrib_map = {} - plugin_tag_map = {} - - def append(self, affiliation): - if not isinstance(affiliation, OwnerAffiliation): - raise TypeError - self.xml.append(affiliation.xml) - return self.affiliations.append(affiliation) - -registerStanzaPlugin(PubsubOwner, OwnerAffiliations) - -class OwnerAffiliation(Affiliation): - namespace = 'http://jabber.org/protocol/pubsub#owner' - interfaces = set(('affiliation', 'jid')) - plugin_attrib_map = {} - plugin_tag_map = {} - -class OwnerConfigure(Configure): - namespace = 'http://jabber.org/protocol/pubsub#owner' - interfaces = set(('node', 'config')) - plugin_attrib_map = {} - plugin_tag_map = {} - -registerStanzaPlugin(PubsubOwner, OwnerConfigure) - -class OwnerDefault(OwnerConfigure): - namespace = 'http://jabber.org/protocol/pubsub#owner' - interfaces = set(('node', 'config')) - plugin_attrib_map = {} - plugin_tag_map = {} - - def getConfig(self): - return self['form'] - - def setConfig(self, value): - self['form'].setStanzaValues(value.getStanzaValues()) - return self - -registerStanzaPlugin(PubsubOwner, OwnerDefault) -registerStanzaPlugin(OwnerDefault, xep_0004.Form) - -class OwnerDelete(ElementBase, OptionalSetting): - namespace = 'http://jabber.org/protocol/pubsub#owner' - name = 'delete' - plugin_attrib = 'delete' - plugin_attrib_map = {} - plugin_tag_map = {} - interfaces = set(('node',)) - -registerStanzaPlugin(PubsubOwner, OwnerDelete) - -class OwnerPurge(ElementBase, OptionalSetting): - namespace = 'http://jabber.org/protocol/pubsub#owner' - name = 'purge' - plugin_attrib = name - plugin_attrib_map = {} - plugin_tag_map = {} - -registerStanzaPlugin(PubsubOwner, OwnerPurge) - -class OwnerRedirect(ElementBase): - namespace = 'http://jabber.org/protocol/pubsub#owner' - name = 'redirect' - plugin_attrib = name - interfaces = set(('node', 'jid')) - plugin_attrib_map = {} - plugin_tag_map = {} - - def setJid(self, value): - self._setAttr('jid', str(value)) - - def getJid(self): - return JID(self._getAttr('jid')) - -registerStanzaPlugin(OwnerDelete, OwnerRedirect) - -class OwnerSubscriptions(Subscriptions): - namespace = 'http://jabber.org/protocol/pubsub#owner' - interfaces = set(('node',)) - plugin_attrib_map = {} - plugin_tag_map = {} - - def append(self, subscription): - if not isinstance(subscription, OwnerSubscription): - raise TypeError - self.xml.append(subscription.xml) - return self.subscriptions.append(subscription) - -registerStanzaPlugin(PubsubOwner, OwnerSubscriptions) - -class OwnerSubscription(ElementBase): - namespace = 'http://jabber.org/protocol/pubsub#owner' - name = 'subscription' - plugin_attrib = name - interfaces = set(('jid', 'subscription')) - plugin_attrib_map = {} - plugin_tag_map = {} - - def setJid(self, value): - self._setAttr('jid', str(value)) - - def getJid(self): - return JID(self._getAttr('from')) - -class Event(ElementBase): - namespace = 'http://jabber.org/protocol/pubsub#event' - name = 'event' - plugin_attrib = 'pubsub_event' - interfaces = set(('node',)) - plugin_attrib_map = {} - plugin_tag_map = {} - -registerStanzaPlugin(Message, Event) - -class EventItem(ElementBase): - namespace = 'http://jabber.org/protocol/pubsub#event' - name = 'item' - plugin_attrib = 'item' - interfaces = set(('id', 'payload')) - plugin_attrib_map = {} - plugin_tag_map = {} - - def setPayload(self, value): - self.xml.append(value) - - def getPayload(self): - childs = self.xml.getchildren() - if len(childs) > 0: - return childs[0] - - def delPayload(self): - for child in self.xml.getchildren(): - self.xml.remove(child) - - -class EventRetract(ElementBase): - namespace = 'http://jabber.org/protocol/pubsub#event' - name = 'retract' - plugin_attrib = 'retract' - interfaces = set(('id',)) - plugin_attrib_map = {} - plugin_tag_map = {} - -class EventItems(ElementBase): - namespace = 'http://jabber.org/protocol/pubsub#event' - name = 'items' - plugin_attrib = 'items' - interfaces = set(('node',)) - plugin_attrib_map = {} - plugin_tag_map = {} - subitem = (EventItem, EventRetract) - -registerStanzaPlugin(Event, EventItems) - -class EventCollection(ElementBase): - namespace = 'http://jabber.org/protocol/pubsub#event' - name = 'collection' - plugin_attrib = name - interfaces = set(('node',)) - plugin_attrib_map = {} - plugin_tag_map = {} - -registerStanzaPlugin(Event, EventCollection) - -class EventAssociate(ElementBase): - namespace = 'http://jabber.org/protocol/pubsub#event' - name = 'associate' - plugin_attrib = name - interfaces = set(('node',)) - plugin_attrib_map = {} - plugin_tag_map = {} - -registerStanzaPlugin(EventCollection, EventAssociate) - -class EventDisassociate(ElementBase): - namespace = 'http://jabber.org/protocol/pubsub#event' - name = 'disassociate' - plugin_attrib = name - interfaces = set(('node',)) - plugin_attrib_map = {} - plugin_tag_map = {} - -registerStanzaPlugin(EventCollection, EventDisassociate) - -class EventConfiguration(ElementBase): - namespace = 'http://jabber.org/protocol/pubsub#event' - name = 'configuration' - plugin_attrib = name - interfaces = set(('node', 'config')) - plugin_attrib_map = {} - plugin_tag_map = {} - -registerStanzaPlugin(Event, EventConfiguration) -registerStanzaPlugin(EventConfiguration, xep_0004.Form) - -class EventPurge(ElementBase): - namespace = 'http://jabber.org/protocol/pubsub#event' - name = 'purge' - plugin_attrib = name - interfaces = set(('node',)) - plugin_attrib_map = {} - plugin_tag_map = {} - -registerStanzaPlugin(Event, EventPurge) - -class EventSubscription(ElementBase): - namespace = 'http://jabber.org/protocol/pubsub#event' - name = 'subscription' - plugin_attrib = name - interfaces = set(('node','expiry', 'jid', 'subid', 'subscription')) - plugin_attrib_map = {} - plugin_tag_map = {} - - def setJid(self, value): - self._setAttr('jid', str(value)) - - def getJid(self): - return JID(self._getAttr('jid')) - -registerStanzaPlugin(Event, EventSubscription) diff --git a/sleekxmpp/plugins/xep_0060/__init__.py b/sleekxmpp/plugins/xep_0060/__init__.py new file mode 100644 index 00000000..026f7c2b --- /dev/null +++ b/sleekxmpp/plugins/xep_0060/__init__.py @@ -0,0 +1,2 @@ +from sleekxmpp.plugins.xep_0060.pubsub import xep_0060 +from sleekxmpp.plugins.xep_0060 import stanza diff --git a/sleekxmpp/plugins/xep_0060/pubsub.py b/sleekxmpp/plugins/xep_0060/pubsub.py new file mode 100644 index 00000000..e199be07 --- /dev/null +++ b/sleekxmpp/plugins/xep_0060/pubsub.py @@ -0,0 +1,313 @@ +from __future__ import with_statement +from sleekxmpp.plugins import base +import logging +#from xml.etree import cElementTree as ET +from sleekxmpp.xmlstream.stanzabase import registerStanzaPlugin, ElementBase, ET +from sleekxmpp.plugins.xep_0060 import stanza +from sleekxmpp.plugins.xep_0004 import Form + + +log = logging.getLogger(__name__) + + +class xep_0060(base.base_plugin): + """ + XEP-0060 Publish Subscribe + """ + + def plugin_init(self): + self.xep = '0060' + self.description = 'Publish-Subscribe' + + def create_node(self, jid, node, config=None, collection=False, ntype=None): + pubsub = ET.Element('{http://jabber.org/protocol/pubsub}pubsub') + create = ET.Element('create') + create.set('node', node) + pubsub.append(create) + configure = ET.Element('configure') + if collection: + ntype = 'collection' + #if config is None: + # submitform = self.xmpp.plugin['xep_0004'].makeForm('submit') + #else: + if config is not None: + submitform = config + if 'FORM_TYPE' in submitform.field: + submitform.field['FORM_TYPE'].setValue('http://jabber.org/protocol/pubsub#node_config') + else: + submitform.addField('FORM_TYPE', 'hidden', value='http://jabber.org/protocol/pubsub#node_config') + if ntype: + if 'pubsub#node_type' in submitform.field: + submitform.field['pubsub#node_type'].setValue(ntype) + else: + submitform.addField('pubsub#node_type', value=ntype) + else: + if 'pubsub#node_type' in submitform.field: + submitform.field['pubsub#node_type'].setValue('leaf') + else: + submitform.addField('pubsub#node_type', value='leaf') + submitform['type'] = 'submit' + configure.append(submitform.xml) + pubsub.append(configure) + iq = self.xmpp.makeIqSet(pubsub) + iq.attrib['to'] = jid + iq.attrib['from'] = self.xmpp.boundjid.full + id = iq['id'] + result = iq.send() + if result is False or result is None or result['type'] == 'error': return False + return True + + def subscribe(self, jid, node, bare=True, subscribee=None): + pubsub = ET.Element('{http://jabber.org/protocol/pubsub}pubsub') + subscribe = ET.Element('subscribe') + subscribe.attrib['node'] = node + if subscribee is None: + if bare: + subscribe.attrib['jid'] = self.xmpp.boundjid.bare + else: + subscribe.attrib['jid'] = self.xmpp.boundjid.full + else: + subscribe.attrib['jid'] = subscribee + pubsub.append(subscribe) + iq = self.xmpp.makeIqSet(pubsub) + iq.attrib['to'] = jid + iq.attrib['from'] = self.xmpp.boundjid.full + id = iq['id'] + result = iq.send() + if result is False or result is None or result['type'] == 'error': return False + return True + + def unsubscribe(self, jid, node, bare=True, subscribee=None): + pubsub = ET.Element('{http://jabber.org/protocol/pubsub}pubsub') + unsubscribe = ET.Element('unsubscribe') + unsubscribe.attrib['node'] = node + if subscribee is None: + if bare: + unsubscribe.attrib['jid'] = self.xmpp.boundjid.bare + else: + unsubscribe.attrib['jid'] = self.xmpp.boundjid.full + else: + unsubscribe.attrib['jid'] = subscribee + pubsub.append(unsubscribe) + iq = self.xmpp.makeIqSet(pubsub) + iq.attrib['to'] = jid + iq.attrib['from'] = self.xmpp.boundjid.full + id = iq['id'] + result = iq.send() + if result is False or result is None or result['type'] == 'error': return False + return True + + def getNodeConfig(self, jid, node=None): # if no node, then grab default + pubsub = ET.Element('{http://jabber.org/protocol/pubsub#owner}pubsub') + if node is not None: + configure = ET.Element('configure') + configure.attrib['node'] = node + else: + configure = ET.Element('default') + pubsub.append(configure) + #TODO: Add configure support. + iq = self.xmpp.makeIqGet() + iq.append(pubsub) + iq.attrib['to'] = jid + iq.attrib['from'] = self.xmpp.boundjid.full + id = iq['id'] + #self.xmpp.add_handler("<iq id='%s'/>" % id, self.handlerCreateNodeResponse) + result = iq.send() + if result is None or result == False or result['type'] == 'error': + log.warning("got error instead of config") + return False + if node is not None: + form = result.find('{http://jabber.org/protocol/pubsub#owner}pubsub/{http://jabber.org/protocol/pubsub#owner}configure/{jabber:x:data}x') + else: + form = result.find('{http://jabber.org/protocol/pubsub#owner}pubsub/{http://jabber.org/protocol/pubsub#owner}default/{jabber:x:data}x') + if not form or form is None: + log.error("No form found.") + return False + return Form(xml=form) + + def getNodeSubscriptions(self, jid, node): + pubsub = ET.Element('{http://jabber.org/protocol/pubsub#owner}pubsub') + subscriptions = ET.Element('subscriptions') + subscriptions.attrib['node'] = node + pubsub.append(subscriptions) + iq = self.xmpp.makeIqGet() + iq.append(pubsub) + iq.attrib['to'] = jid + iq.attrib['from'] = self.xmpp.boundjid.full + id = iq['id'] + result = iq.send() + if result is None or result == False or result['type'] == 'error': + log.warning("got error instead of config") + return False + else: + results = result.findall('{http://jabber.org/protocol/pubsub#owner}pubsub/{http://jabber.org/protocol/pubsub#owner}subscriptions/{http://jabber.org/protocol/pubsub#owner}subscription') + if results is None: + return False + subs = {} + for sub in results: + subs[sub.get('jid')] = sub.get('subscription') + return subs + + def getNodeAffiliations(self, jid, node): + pubsub = ET.Element('{http://jabber.org/protocol/pubsub#owner}pubsub') + affiliations = ET.Element('affiliations') + affiliations.attrib['node'] = node + pubsub.append(affiliations) + iq = self.xmpp.makeIqGet() + iq.append(pubsub) + iq.attrib['to'] = jid + iq.attrib['from'] = self.xmpp.boundjid.full + id = iq['id'] + result = iq.send() + if result is None or result == False or result['type'] == 'error': + log.warning("got error instead of config") + return False + else: + results = result.findall('{http://jabber.org/protocol/pubsub#owner}pubsub/{http://jabber.org/protocol/pubsub#owner}affiliations/{http://jabber.org/protocol/pubsub#owner}affiliation') + if results is None: + return False + subs = {} + for sub in results: + subs[sub.get('jid')] = sub.get('affiliation') + return subs + + def deleteNode(self, jid, node): + pubsub = ET.Element('{http://jabber.org/protocol/pubsub#owner}pubsub') + iq = self.xmpp.makeIqSet() + delete = ET.Element('delete') + delete.attrib['node'] = node + pubsub.append(delete) + iq.append(pubsub) + iq.attrib['to'] = jid + iq.attrib['from'] = self.xmpp.boundjid.full + result = iq.send() + if result is not None and result is not False and result['type'] != 'error': + return True + else: + return False + + + def setNodeConfig(self, jid, node, config): + pubsub = ET.Element('{http://jabber.org/protocol/pubsub#owner}pubsub') + configure = ET.Element('configure') + configure.attrib['node'] = node + config = config.getXML('submit') + configure.append(config) + pubsub.append(configure) + iq = self.xmpp.makeIqSet(pubsub) + iq.attrib['to'] = jid + iq.attrib['from'] = self.xmpp.boundjid.full + id = iq['id'] + result = iq.send() + if result is None or result['type'] == 'error': + return False + return True + + def setItem(self, jid, node, items=[]): + pubsub = ET.Element('{http://jabber.org/protocol/pubsub}pubsub') + publish = ET.Element('publish') + publish.attrib['node'] = node + for pub_item in items: + id, payload = pub_item + item = ET.Element('item') + if id is not None: + item.attrib['id'] = id + item.append(payload) + publish.append(item) + pubsub.append(publish) + iq = self.xmpp.makeIqSet(pubsub) + iq.attrib['to'] = jid + iq.attrib['from'] = self.xmpp.boundjid.full + id = iq['id'] + result = iq.send() + if result is None or result is False or result['type'] == 'error': return False + return True + + def addItem(self, jid, node, items=[]): + return self.setItem(jid, node, items) + + def deleteItem(self, jid, node, item): + pubsub = ET.Element('{http://jabber.org/protocol/pubsub}pubsub') + retract = ET.Element('retract') + retract.attrib['node'] = node + itemn = ET.Element('item') + itemn.attrib['id'] = item + retract.append(itemn) + pubsub.append(retract) + iq = self.xmpp.makeIqSet(pubsub) + iq.attrib['to'] = jid + iq.attrib['from'] = self.xmpp.boundjid.full + id = iq['id'] + result = iq.send() + if result is None or result is False or result['type'] == 'error': return False + return True + + def getNodes(self, jid): + response = self.xmpp.plugin['xep_0030'].getItems(jid) + items = response.findall('{http://jabber.org/protocol/disco#items}query/{http://jabber.org/protocol/disco#items}item') + nodes = {} + if items is not None and items is not False: + for item in items: + nodes[item.get('node')] = item.get('name') + return nodes + + def getItems(self, jid, node): + response = self.xmpp.plugin['xep_0030'].getItems(jid, node) + items = response.findall('{http://jabber.org/protocol/disco#items}query/{http://jabber.org/protocol/disco#items}item') + nodeitems = [] + if items is not None and items is not False: + for item in items: + nodeitems.append(item.get('node')) + return nodeitems + + def addNodeToCollection(self, jid, child, parent=''): + config = self.getNodeConfig(jid, child) + if not config or config is None: + self.lasterror = "Config Error" + return False + try: + config.field['pubsub#collection'].setValue(parent) + except KeyError: + log.warning("pubsub#collection doesn't exist in config, trying to add it") + config.addField('pubsub#collection', value=parent) + if not self.setNodeConfig(jid, child, config): + return False + return True + + def modifyAffiliation(self, ps_jid, node, user_jid, affiliation): + if affiliation not in ('owner', 'publisher', 'member', 'none', 'outcast'): + raise TypeError + pubsub = ET.Element('{http://jabber.org/protocol/pubsub#owner}pubsub') + affs = ET.Element('affiliations') + affs.attrib['node'] = node + aff = ET.Element('affiliation') + aff.attrib['jid'] = user_jid + aff.attrib['affiliation'] = affiliation + affs.append(aff) + pubsub.append(affs) + iq = self.xmpp.makeIqSet(pubsub) + iq.attrib['to'] = ps_jid + iq.attrib['from'] = self.xmpp.boundjid.full + id = iq['id'] + result = iq.send() + if result is None or result is False or result['type'] == 'error': + return False + return True + + def addNodeToCollection(self, jid, child, parent=''): + config = self.getNodeConfig(jid, child) + if not config or config is None: + self.lasterror = "Config Error" + return False + try: + config.field['pubsub#collection'].setValue(parent) + except KeyError: + log.warning("pubsub#collection doesn't exist in config, trying to add it") + config.addField('pubsub#collection', value=parent) + if not self.setNodeConfig(jid, child, config): + return False + return True + + def removeNodeFromCollection(self, jid, child): + self.addNodeToCollection(jid, child, '') + diff --git a/sleekxmpp/plugins/xep_0060/stanza/__init__.py b/sleekxmpp/plugins/xep_0060/stanza/__init__.py new file mode 100644 index 00000000..d7cd91a8 --- /dev/null +++ b/sleekxmpp/plugins/xep_0060/stanza/__init__.py @@ -0,0 +1,3 @@ +from sleekxmpp.plugins.xep_0060.stanza.pubsub import Pubsub, Affiliation, Affiliations, Subscription, Subscriptions, SubscribeOptions, Item, Items, Create, Publish, Retract, Unsubscribe, Subscribe, Configure, Options, PubsubState, PubsubStateEvent +from sleekxmpp.plugins.xep_0060.stanza.pubsub_owner import PubsubOwner, DefaultConfig, OwnerAffiliations, OwnerAffiliation, OwnerConfigure, OwnerDefault, OwnerDelete, OwnerPurge, OwnerRedirect, OwnerSubscriptions, OwnerSubscription +from sleekxmpp.plugins.xep_0060.stanza.pubsub_event import Event, EventItem, EventRetract, EventItems, EventCollection, EventAssociate, EventDisassociate, EventConfiguration, EventPurge, EventSubscription diff --git a/sleekxmpp/plugins/xep_0060/stanza/base.py b/sleekxmpp/plugins/xep_0060/stanza/base.py new file mode 100644 index 00000000..9b1efe1b --- /dev/null +++ b/sleekxmpp/plugins/xep_0060/stanza/base.py @@ -0,0 +1,24 @@ +from xml.etree import cElementTree as ET + +class OptionalSetting(object): + interfaces = set(('required',)) + + def setRequired(self, value): + value = bool(value) + if value and not self['required']: + self.xml.append(ET.Element("{%s}required" % self.namespace)) + elif not value and self['required']: + self.delRequired() + + def getRequired(self): + required = self.xml.find("{%s}required" % self.namespace) + if required is not None: + return True + else: + return False + + def delRequired(self): + required = self.xml.find("{%s}required" % self.namespace) + if required is not None: + self.xml.remove(required) + diff --git a/sleekxmpp/plugins/xep_0060/stanza/pubsub.py b/sleekxmpp/plugins/xep_0060/stanza/pubsub.py new file mode 100644 index 00000000..96655942 --- /dev/null +++ b/sleekxmpp/plugins/xep_0060/stanza/pubsub.py @@ -0,0 +1,277 @@ +from sleekxmpp.xmlstream.stanzabase import registerStanzaPlugin, ElementBase, ET, JID +from sleekxmpp.stanza.iq import Iq +from sleekxmpp.stanza.message import Message +from sleekxmpp.basexmpp import basexmpp +from sleekxmpp.xmlstream.xmlstream import XMLStream +import logging +from sleekxmpp.plugins import xep_0004 +from sleekxmpp.plugins.xep_0060.stanza.base import OptionalSetting + + +class Pubsub(ElementBase): + namespace = 'http://jabber.org/protocol/pubsub' + name = 'pubsub' + plugin_attrib = 'pubsub' + interfaces = set(tuple()) + plugin_attrib_map = {} + plugin_tag_map = {} + +registerStanzaPlugin(Iq, Pubsub) + + +class Affiliation(ElementBase): + namespace = 'http://jabber.org/protocol/pubsub' + name = 'affiliation' + plugin_attrib = name + interfaces = set(('node', 'affiliation')) + plugin_attrib_map = {} + plugin_tag_map = {} + +class Affiliations(ElementBase): + namespace = 'http://jabber.org/protocol/pubsub' + name = 'affiliations' + plugin_attrib = 'affiliations' + interfaces = set(tuple()) + plugin_attrib_map = {} + plugin_tag_map = {} + subitem = (Affiliation,) + + def append(self, affiliation): + if not isinstance(affiliation, Affiliation): + raise TypeError + self.xml.append(affiliation.xml) + return self.iterables.append(affiliation) + +registerStanzaPlugin(Pubsub, Affiliations) + + +class Subscription(ElementBase): + namespace = 'http://jabber.org/protocol/pubsub' + name = 'subscription' + plugin_attrib = name + interfaces = set(('jid', 'node', 'subscription', 'subid')) + plugin_attrib_map = {} + plugin_tag_map = {} + + def setjid(self, value): + self._setattr('jid', str(value)) + + def getjid(self): + return jid(self._getattr('jid')) + +registerStanzaPlugin(Pubsub, Subscription) + +class Subscriptions(ElementBase): + namespace = 'http://jabber.org/protocol/pubsub' + name = 'subscriptions' + plugin_attrib = 'subscriptions' + interfaces = set(tuple()) + plugin_attrib_map = {} + plugin_tag_map = {} + subitem = (Subscription,) + +registerStanzaPlugin(Pubsub, Subscriptions) + + +class SubscribeOptions(ElementBase, OptionalSetting): + namespace = 'http://jabber.org/protocol/pubsub' + name = 'subscribe-options' + plugin_attrib = 'suboptions' + plugin_attrib_map = {} + plugin_tag_map = {} + interfaces = set(('required',)) + +registerStanzaPlugin(Subscription, SubscribeOptions) + +class Item(ElementBase): + namespace = 'http://jabber.org/protocol/pubsub' + name = 'item' + plugin_attrib = name + interfaces = set(('id', 'payload')) + plugin_attrib_map = {} + plugin_tag_map = {} + + def setPayload(self, value): + self.xml.append(value) + + def getPayload(self): + childs = self.xml.getchildren() + if len(childs) > 0: + return childs[0] + + def delPayload(self): + for child in self.xml.getchildren(): + self.xml.remove(child) + +class Items(ElementBase): + namespace = 'http://jabber.org/protocol/pubsub' + name = 'items' + plugin_attrib = 'items' + interfaces = set(('node',)) + plugin_attrib_map = {} + plugin_tag_map = {} + subitem = (Item,) + +registerStanzaPlugin(Pubsub, Items) + +class Create(ElementBase): + namespace = 'http://jabber.org/protocol/pubsub' + name = 'create' + plugin_attrib = name + interfaces = set(('node',)) + plugin_attrib_map = {} + plugin_tag_map = {} + +registerStanzaPlugin(Pubsub, Create) + +#class Default(ElementBase): +# namespace = 'http://jabber.org/protocol/pubsub' +# name = 'default' +# plugin_attrib = name +# interfaces = set(('node', 'type')) +# plugin_attrib_map = {} +# plugin_tag_map = {} +# +# def getType(self): +# t = self._getAttr('type') +# if not t: t == 'leaf' +# return t +# +#registerStanzaPlugin(Pubsub, Default) + +class Publish(Items): + namespace = 'http://jabber.org/protocol/pubsub' + name = 'publish' + plugin_attrib = name + interfaces = set(('node',)) + plugin_attrib_map = {} + plugin_tag_map = {} + subitem = (Item,) + +registerStanzaPlugin(Pubsub, Publish) + +class Retract(Items): + namespace = 'http://jabber.org/protocol/pubsub' + name = 'retract' + plugin_attrib = name + interfaces = set(('node', 'notify')) + plugin_attrib_map = {} + plugin_tag_map = {} + +registerStanzaPlugin(Pubsub, Retract) + +class Unsubscribe(ElementBase): + namespace = 'http://jabber.org/protocol/pubsub' + name = 'unsubscribe' + plugin_attrib = name + interfaces = set(('node', 'jid')) + plugin_attrib_map = {} + plugin_tag_map = {} + + def setJid(self, value): + self._setAttr('jid', str(value)) + + def getJid(self): + return JID(self._getAttr('jid')) + +registerStanzaPlugin(Pubsub, Unsubscribe) + +class Subscribe(ElementBase): + namespace = 'http://jabber.org/protocol/pubsub' + name = 'subscribe' + plugin_attrib = name + interfaces = set(('node', 'jid')) + plugin_attrib_map = {} + plugin_tag_map = {} + + def setJid(self, value): + self._setAttr('jid', str(value)) + + def getJid(self): + return JID(self._getAttr('jid')) + +registerStanzaPlugin(Pubsub, Subscribe) + +class Configure(ElementBase): + namespace = 'http://jabber.org/protocol/pubsub' + name = 'configure' + plugin_attrib = name + interfaces = set(('node', 'type')) + plugin_attrib_map = {} + plugin_tag_map = {} + + def getType(self): + t = self._getAttr('type') + if not t: t == 'leaf' + return t + +registerStanzaPlugin(Pubsub, Configure) +registerStanzaPlugin(Configure, xep_0004.Form) + +class Options(ElementBase): + namespace = 'http://jabber.org/protocol/pubsub' + name = 'options' + plugin_attrib = 'options' + interfaces = set(('jid', 'node', 'options')) + plugin_attrib_map = {} + plugin_tag_map = {} + + def __init__(self, *args, **kwargs): + ElementBase.__init__(self, *args, **kwargs) + + def getOptions(self): + config = self.xml.find('{jabber:x:data}x') + form = xep_0004.Form() + if config is not None: + form.fromXML(config) + return form + + def setOptions(self, value): + self.xml.append(value.getXML()) + return self + + def delOptions(self): + config = self.xml.find('{jabber:x:data}x') + self.xml.remove(config) + + def setJid(self, value): + self._setAttr('jid', str(value)) + + def getJid(self): + return JID(self._getAttr('jid')) + +registerStanzaPlugin(Pubsub, Options) +registerStanzaPlugin(Subscribe, Options) + +class PubsubState(ElementBase): + namespace = 'http://jabber.org/protocol/psstate' + name = 'state' + plugin_attrib = 'psstate' + interfaces = set(('node', 'item', 'payload')) + plugin_attrib_map = {} + plugin_tag_map = {} + + def setPayload(self, value): + self.xml.append(value) + + def getPayload(self): + childs = self.xml.getchildren() + if len(childs) > 0: + return childs[0] + + def delPayload(self): + for child in self.xml.getchildren(): + self.xml.remove(child) + +registerStanzaPlugin(Iq, PubsubState) + +class PubsubStateEvent(ElementBase): + namespace = 'http://jabber.org/protocol/psstate#event' + name = 'event' + plugin_attrib = 'psstate_event' + intefaces = set(tuple()) + plugin_attrib_map = {} + plugin_tag_map = {} + +registerStanzaPlugin(Message, PubsubStateEvent) +registerStanzaPlugin(PubsubStateEvent, PubsubState) diff --git a/sleekxmpp/plugins/xep_0060/stanza/pubsub_event.py b/sleekxmpp/plugins/xep_0060/stanza/pubsub_event.py new file mode 100644 index 00000000..2dfe6c4a --- /dev/null +++ b/sleekxmpp/plugins/xep_0060/stanza/pubsub_event.py @@ -0,0 +1,124 @@ +from sleekxmpp.xmlstream.stanzabase import registerStanzaPlugin, ElementBase, ET, JID +from sleekxmpp.stanza.iq import Iq +from sleekxmpp.stanza.message import Message +from sleekxmpp.basexmpp import basexmpp +from sleekxmpp.xmlstream.xmlstream import XMLStream +import logging +from sleekxmpp.plugins import xep_0004 + +class Event(ElementBase): + namespace = 'http://jabber.org/protocol/pubsub#event' + name = 'event' + plugin_attrib = 'pubsub_event' + interfaces = set(('node',)) + plugin_attrib_map = {} + plugin_tag_map = {} + +registerStanzaPlugin(Message, Event) + +class EventItem(ElementBase): + namespace = 'http://jabber.org/protocol/pubsub#event' + name = 'item' + plugin_attrib = 'item' + interfaces = set(('id', 'payload')) + plugin_attrib_map = {} + plugin_tag_map = {} + + def setPayload(self, value): + self.xml.append(value) + + def getPayload(self): + childs = self.xml.getchildren() + if len(childs) > 0: + return childs[0] + + def delPayload(self): + for child in self.xml.getchildren(): + self.xml.remove(child) + + +class EventRetract(ElementBase): + namespace = 'http://jabber.org/protocol/pubsub#event' + name = 'retract' + plugin_attrib = 'retract' + interfaces = set(('id',)) + plugin_attrib_map = {} + plugin_tag_map = {} + +class EventItems(ElementBase): + namespace = 'http://jabber.org/protocol/pubsub#event' + name = 'items' + plugin_attrib = 'items' + interfaces = set(('node',)) + plugin_attrib_map = {} + plugin_tag_map = {} + subitem = (EventItem, EventRetract) + +registerStanzaPlugin(Event, EventItems) + +class EventCollection(ElementBase): + namespace = 'http://jabber.org/protocol/pubsub#event' + name = 'collection' + plugin_attrib = name + interfaces = set(('node',)) + plugin_attrib_map = {} + plugin_tag_map = {} + +registerStanzaPlugin(Event, EventCollection) + +class EventAssociate(ElementBase): + namespace = 'http://jabber.org/protocol/pubsub#event' + name = 'associate' + plugin_attrib = name + interfaces = set(('node',)) + plugin_attrib_map = {} + plugin_tag_map = {} + +registerStanzaPlugin(EventCollection, EventAssociate) + +class EventDisassociate(ElementBase): + namespace = 'http://jabber.org/protocol/pubsub#event' + name = 'disassociate' + plugin_attrib = name + interfaces = set(('node',)) + plugin_attrib_map = {} + plugin_tag_map = {} + +registerStanzaPlugin(EventCollection, EventDisassociate) + +class EventConfiguration(ElementBase): + namespace = 'http://jabber.org/protocol/pubsub#event' + name = 'configuration' + plugin_attrib = name + interfaces = set(('node', 'config')) + plugin_attrib_map = {} + plugin_tag_map = {} + +registerStanzaPlugin(Event, EventConfiguration) +registerStanzaPlugin(EventConfiguration, xep_0004.Form) + +class EventPurge(ElementBase): + namespace = 'http://jabber.org/protocol/pubsub#event' + name = 'purge' + plugin_attrib = name + interfaces = set(('node',)) + plugin_attrib_map = {} + plugin_tag_map = {} + +registerStanzaPlugin(Event, EventPurge) + +class EventSubscription(ElementBase): + namespace = 'http://jabber.org/protocol/pubsub#event' + name = 'subscription' + plugin_attrib = name + interfaces = set(('node','expiry', 'jid', 'subid', 'subscription')) + plugin_attrib_map = {} + plugin_tag_map = {} + + def setJid(self, value): + self._setAttr('jid', str(value)) + + def getJid(self): + return JID(self._getAttr('jid')) + +registerStanzaPlugin(Event, EventSubscription) diff --git a/sleekxmpp/plugins/xep_0060/stanza/pubsub_owner.py b/sleekxmpp/plugins/xep_0060/stanza/pubsub_owner.py new file mode 100644 index 00000000..a90780cc --- /dev/null +++ b/sleekxmpp/plugins/xep_0060/stanza/pubsub_owner.py @@ -0,0 +1,152 @@ +from sleekxmpp.xmlstream.stanzabase import registerStanzaPlugin, ElementBase, ET, JID +from sleekxmpp.stanza.iq import Iq +from sleekxmpp.stanza.message import Message +from sleekxmpp.basexmpp import basexmpp +from sleekxmpp.xmlstream.xmlstream import XMLStream +import logging +from sleekxmpp.plugins import xep_0004 +from sleekxmpp.plugins.xep_0060.stanza.base import OptionalSetting +from sleekxmpp.plugins.xep_0060.stanza.pubsub import Affiliations, Affiliation, Configure, Subscriptions + +class PubsubOwner(ElementBase): + namespace = 'http://jabber.org/protocol/pubsub#owner' + name = 'pubsub' + plugin_attrib = 'pubsub_owner' + interfaces = set(tuple()) + plugin_attrib_map = {} + plugin_tag_map = {} + +registerStanzaPlugin(Iq, PubsubOwner) + +class DefaultConfig(ElementBase): + namespace = 'http://jabber.org/protocol/pubsub#owner' + name = 'default' + plugin_attrib = 'default' + interfaces = set(('node', 'type', 'config')) + plugin_attrib_map = {} + plugin_tag_map = {} + + def __init__(self, *args, **kwargs): + ElementBase.__init__(self, *args, **kwargs) + + def getType(self): + t = self._getAttr('type') + if not t: t = 'leaf' + return t + + def getConfig(self): + return self['form'] + + def setConfig(self, value): + self['form'].setStanzaValues(value.getStanzaValues()) + return self + +registerStanzaPlugin(PubsubOwner, DefaultConfig) +registerStanzaPlugin(DefaultConfig, xep_0004.Form) + +class OwnerAffiliations(Affiliations): + namespace = 'http://jabber.org/protocol/pubsub#owner' + interfaces = set(('node')) + plugin_attrib_map = {} + plugin_tag_map = {} + + def append(self, affiliation): + if not isinstance(affiliation, OwnerAffiliation): + raise TypeError + self.xml.append(affiliation.xml) + return self.affiliations.append(affiliation) + +registerStanzaPlugin(PubsubOwner, OwnerAffiliations) + +class OwnerAffiliation(Affiliation): + namespace = 'http://jabber.org/protocol/pubsub#owner' + interfaces = set(('affiliation', 'jid')) + plugin_attrib_map = {} + plugin_tag_map = {} + +class OwnerConfigure(Configure): + namespace = 'http://jabber.org/protocol/pubsub#owner' + interfaces = set(('node', 'config')) + plugin_attrib_map = {} + plugin_tag_map = {} + +registerStanzaPlugin(PubsubOwner, OwnerConfigure) + +class OwnerDefault(OwnerConfigure): + namespace = 'http://jabber.org/protocol/pubsub#owner' + interfaces = set(('node', 'config')) + plugin_attrib_map = {} + plugin_tag_map = {} + + def getConfig(self): + return self['form'] + + def setConfig(self, value): + self['form'].setStanzaValues(value.getStanzaValues()) + return self + +registerStanzaPlugin(PubsubOwner, OwnerDefault) +registerStanzaPlugin(OwnerDefault, xep_0004.Form) + +class OwnerDelete(ElementBase, OptionalSetting): + namespace = 'http://jabber.org/protocol/pubsub#owner' + name = 'delete' + plugin_attrib = 'delete' + plugin_attrib_map = {} + plugin_tag_map = {} + interfaces = set(('node',)) + +registerStanzaPlugin(PubsubOwner, OwnerDelete) + +class OwnerPurge(ElementBase, OptionalSetting): + namespace = 'http://jabber.org/protocol/pubsub#owner' + name = 'purge' + plugin_attrib = name + plugin_attrib_map = {} + plugin_tag_map = {} + +registerStanzaPlugin(PubsubOwner, OwnerPurge) + +class OwnerRedirect(ElementBase): + namespace = 'http://jabber.org/protocol/pubsub#owner' + name = 'redirect' + plugin_attrib = name + interfaces = set(('node', 'jid')) + plugin_attrib_map = {} + plugin_tag_map = {} + + def setJid(self, value): + self._setAttr('jid', str(value)) + + def getJid(self): + return JID(self._getAttr('jid')) + +registerStanzaPlugin(OwnerDelete, OwnerRedirect) + +class OwnerSubscriptions(Subscriptions): + namespace = 'http://jabber.org/protocol/pubsub#owner' + interfaces = set(('node',)) + plugin_attrib_map = {} + plugin_tag_map = {} + + def append(self, subscription): + if not isinstance(subscription, OwnerSubscription): + raise TypeError + self.xml.append(subscription.xml) + return self.subscriptions.append(subscription) + +registerStanzaPlugin(PubsubOwner, OwnerSubscriptions) + +class OwnerSubscription(ElementBase): + namespace = 'http://jabber.org/protocol/pubsub#owner' + name = 'subscription' + plugin_attrib = name + interfaces = set(('jid', 'subscription')) + plugin_attrib_map = {} + plugin_tag_map = {} + + def setJid(self, value): + self._setAttr('jid', str(value)) + + def getJid(self): + return JID(self._getAttr('from')) diff --git a/sleekxmpp/stanza/__init__.py b/sleekxmpp/stanza/__init__.py index dbf7b86f..4bd37dc5 100644 --- a/sleekxmpp/stanza/__init__.py +++ b/sleekxmpp/stanza/__init__.py @@ -8,7 +8,8 @@ from sleekxmpp.stanza.error import Error -from sleekxmpp.stanza.stream_error import StreamError from sleekxmpp.stanza.iq import Iq from sleekxmpp.stanza.message import Message from sleekxmpp.stanza.presence import Presence +from sleekxmpp.stanza.stream_features import StreamFeatures +from sleekxmpp.stanza.stream_error import StreamError diff --git a/sleekxmpp/stanza/error.py b/sleekxmpp/stanza/error.py index 5d1ce50d..93231a48 100644 --- a/sleekxmpp/stanza/error.py +++ b/sleekxmpp/stanza/error.py @@ -88,7 +88,9 @@ class Error(ElementBase): """Return the condition element's name.""" for child in self.xml.getchildren(): if "{%s}" % self.condition_ns in child.tag: - return child.tag.split('}', 1)[-1] + cond = child.tag.split('}', 1)[-1] + if cond in self.conditions: + return cond return '' def set_condition(self, value): diff --git a/sleekxmpp/stanza/stream_features.py b/sleekxmpp/stanza/stream_features.py new file mode 100644 index 00000000..5be2e55f --- /dev/null +++ b/sleekxmpp/stanza/stream_features.py @@ -0,0 +1,52 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2010 Nathanael C. Fritz + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from sleekxmpp.xmlstream import ElementBase, StanzaBase, ET +from sleekxmpp.xmlstream import register_stanza_plugin + + +class StreamFeatures(StanzaBase): + + """ + """ + + name = 'features' + namespace = 'http://etherx.jabber.org/streams' + interfaces = set(('features', 'required', 'optional')) + sub_interfaces = interfaces + + def setup(self, xml): + StanzaBase.setup(self, xml) + self.values = self.values + + def get_features(self): + """ + """ + return self.plugins + + def set_features(self, value): + """ + """ + pass + + def del_features(self): + """ + """ + pass + + def get_required(self): + """ + """ + features = self['features'] + return [f for n, f in features.items() if f['required']] + + def get_optional(self): + """ + """ + features = self['features'] + return [f for n, f in features.items() if not f['required']] diff --git a/sleekxmpp/thirdparty/__init__.py b/sleekxmpp/thirdparty/__init__.py index 276ac3cc..3eb6ad73 100644 --- a/sleekxmpp/thirdparty/__init__.py +++ b/sleekxmpp/thirdparty/__init__.py @@ -2,3 +2,5 @@ try: from collections import OrderedDict except: from sleekxmpp.thirdparty.ordereddict import OrderedDict + +from sleekxmpp.thirdparty import suelta diff --git a/sleekxmpp/thirdparty/suelta/LICENSE b/sleekxmpp/thirdparty/suelta/LICENSE new file mode 100644 index 00000000..6eee4f33 --- /dev/null +++ b/sleekxmpp/thirdparty/suelta/LICENSE @@ -0,0 +1,21 @@ +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 new file mode 100644 index 00000000..393b8078 --- /dev/null +++ b/sleekxmpp/thirdparty/suelta/PLAYING-NICELY @@ -0,0 +1,27 @@ +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 new file mode 100644 index 00000000..c32463a4 --- /dev/null +++ b/sleekxmpp/thirdparty/suelta/README @@ -0,0 +1,8 @@ +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 new file mode 100644 index 00000000..04f0cbad --- /dev/null +++ b/sleekxmpp/thirdparty/suelta/__init__.py @@ -0,0 +1,26 @@ +# 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 new file mode 100644 index 00000000..625cca0e --- /dev/null +++ b/sleekxmpp/thirdparty/suelta/exceptions.py @@ -0,0 +1,31 @@ +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) diff --git a/sleekxmpp/thirdparty/suelta/mechanisms/__init__.py b/sleekxmpp/thirdparty/suelta/mechanisms/__init__.py new file mode 100644 index 00000000..5cb2ee3d --- /dev/null +++ b/sleekxmpp/thirdparty/suelta/mechanisms/__init__.py @@ -0,0 +1,5 @@ +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 diff --git a/sleekxmpp/thirdparty/suelta/mechanisms/anonymous.py b/sleekxmpp/thirdparty/suelta/mechanisms/anonymous.py new file mode 100644 index 00000000..de89eef2 --- /dev/null +++ b/sleekxmpp/thirdparty/suelta/mechanisms/anonymous.py @@ -0,0 +1,36 @@ +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__(self, 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 new file mode 100644 index 00000000..ba44befe --- /dev/null +++ b/sleekxmpp/thirdparty/suelta/mechanisms/cram_md5.py @@ -0,0 +1,63 @@ +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): + """ + """ + 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 new file mode 100644 index 00000000..5492c553 --- /dev/null +++ b/sleekxmpp/thirdparty/suelta/mechanisms/digest_md5.py @@ -0,0 +1,273 @@ +import sys + +import random + +from sleekxmpp.thirdparty.suelta.util import hash, bytes, quote +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/plain.py b/sleekxmpp/thirdparty/suelta/mechanisms/plain.py new file mode 100644 index 00000000..ab17095e --- /dev/null +++ b/sleekxmpp/thirdparty/suelta/mechanisms/plain.py @@ -0,0 +1,61 @@ +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', 1, PLAIN, use_hashes=False) diff --git a/sleekxmpp/thirdparty/suelta/mechanisms/scram_hmac.py b/sleekxmpp/thirdparty/suelta/mechanisms/scram_hmac.py new file mode 100644 index 00000000..e0020329 --- /dev/null +++ b/sleekxmpp/thirdparty/suelta/mechanisms/scram_hmac.py @@ -0,0 +1,176 @@ +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(self.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 new file mode 100644 index 00000000..ec7afe9d --- /dev/null +++ b/sleekxmpp/thirdparty/suelta/sasl.py @@ -0,0 +1,402 @@ +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 == 'anonymous': + 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 != 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 new file mode 100644 index 00000000..fe58d58b --- /dev/null +++ b/sleekxmpp/thirdparty/suelta/saslprep.py @@ -0,0 +1,78 @@ +from __future__ import unicode_literals + +import sys +import stringprep +import unicodedata + + +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('us-ascii') + + # 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 UnicodeError('Section 6.3 [end]') + + # Check for prohibited characters + for x in range(len(text)): + if strict and stringprep.in_table_a1(text[x]): + raise UnicodeError('Unassigned Codepoint') + if stringprep.in_table_c12(text[x]): + raise UnicodeError('In table C.1.2') + if stringprep.in_table_c21(text[x]): + raise UnicodeError('In table C.2.1') + if stringprep.in_table_c22(text[x]): + raise UnicodeError('In table C.2.2') + if stringprep.in_table_c3(text[x]): + raise UnicodeError('In table C.3') + if stringprep.in_table_c4(text[x]): + raise UnicodeError('In table C.4') + if stringprep.in_table_c5(text[x]): + raise UnicodeError('In table C.5') + if stringprep.in_table_c6(text[x]): + raise UnicodeError('In table C.6') + if stringprep.in_table_c7(text[x]): + raise UnicodeError('In table C.7') + if stringprep.in_table_c8(text[x]): + raise UnicodeError('In table C.8') + if stringprep.in_table_c9(text[x]): + raise UnicodeError('In table C.9') + if x: + if first_is_randal and stringprep.in_table_d2(text[x]): + raise UnicodeError('Section 6.2') + if not first_is_randal and \ + x != len(text) - 1 and \ + stringprep.in_table_d1(text[x]): + raise UnicodeError('Section 6.3') + + return text diff --git a/sleekxmpp/thirdparty/suelta/util.py b/sleekxmpp/thirdparty/suelta/util.py new file mode 100644 index 00000000..7d822a81 --- /dev/null +++ b/sleekxmpp/thirdparty/suelta/util.py @@ -0,0 +1,118 @@ +""" +""" + +import sys +import hashlib + + +def bytes(text): + """ + Convert Unicode text to UTF-8 encoded bytes. + + Since Python 2.6+ and Python 3+ have similar but incompatible + signatures, this function unifies the two to keep code sane. + + :param text: Unicode text to convert to bytes + :rtype: bytes (Python3), str (Python2.6+) + """ + if sys.version_info < (3, 0): + import __builtin__ + return __builtin__.bytes(text) + else: + import builtins + if isinstance(text, builtins.bytes): + # We already have bytes, so do nothing + return text + if isinstance(text, list): + # Convert a list of integers to bytes + return builtins.bytes(text) + else: + # Convert UTF-8 text to bytes + return builtins.bytes(text, encoding='utf-8') + + +def quote(text): + """ + Enclose in quotes and escape internal slashes and double quotes. + + :param text: A Unicode or byte string. + """ + text = bytes(text) + return b'"' + text.replace(b'\\', b'\\\\').replace(b'"', b'\\"') + b'"' + + +def num_to_bytes(num): + """ + Convert an integer into a four byte sequence. + + :param integer num: An integer to convert to its byte representation. + """ + bval = b'' + bval += bytes(chr(0xFF & (num >> 24))) + bval += bytes(chr(0xFF & (num >> 16))) + bval += bytes(chr(0xFF & (num >> 8))) + bval += bytes(chr(0xFF & (num >> 0))) + return bval + + +def bytes_to_num(bval): + """ + Convert a four byte sequence to an integer. + + :param bytes bval: A four byte sequence to turn into an integer. + """ + num = 0 + num += ord(bval[0] << 24) + num += ord(bval[1] << 16) + num += ord(bval[2] << 8) + num += ord(bval[3]) + return num + + +def XOR(x, y): + """ + Return the results of an XOR operation on two equal length byte strings. + + :param bytes x: A byte string + :param bytes y: A byte string + :rtype: bytes + """ + result = b'' + for a, b in zip(x, y): + if sys.version_info < (3, 0): + result += chr((ord(a) ^ ord(b))) + else: + result += bytes([a ^ b]) + return result + + +def hash(name): + """ + Return a hash function implementing the given algorithm. + + :param name: The name of the hashing algorithm to use. + :type name: string + + :rtype: function + """ + name = name.lower() + if name.startswith('sha-'): + name = 'sha' + name[4:] + if name in dir(hashlib): + return getattr(hashlib, name) + return None + + +def hashes(): + """ + Return a list of available hashing algorithms. + + :rtype: list of strings + """ + t = [] + if 'md5' in dir(hashlib): + t = ['MD5'] + if 'md2' in dir(hashlib): + t += ['MD2'] + hashes = ['SHA-' + h[3:] for h in dir(hashlib) if h.startswith('sha')] + return t + hashes diff --git a/sleekxmpp/xmlstream/stanzabase.py b/sleekxmpp/xmlstream/stanzabase.py index d9a4636a..f1a9e1f5 100644 --- a/sleekxmpp/xmlstream/stanzabase.py +++ b/sleekxmpp/xmlstream/stanzabase.py @@ -1064,7 +1064,9 @@ class ElementBase(object): Defaults to True. """ stanza_ns = '' if top_level_ns else self.namespace - return tostring(self.xml, xmlns='', stanza_ns=stanza_ns) + return tostring(self.xml, xmlns='', + stanza_ns=stanza_ns, + top_level = not top_level_ns) def __repr__(self): """ @@ -1282,7 +1284,8 @@ class StanzaBase(ElementBase): stanza_ns = '' if top_level_ns else self.namespace return tostring(self.xml, xmlns='', stanza_ns=stanza_ns, - stream=self.stream) + stream=self.stream, + top_level = not top_level_ns) # To comply with PEP8, method names now use underscores. diff --git a/sleekxmpp/xmlstream/tostring/tostring.py b/sleekxmpp/xmlstream/tostring.py index 38b08d82..f9674b15 100644 --- a/sleekxmpp/xmlstream/tostring/tostring.py +++ b/sleekxmpp/xmlstream/tostring.py @@ -6,8 +6,14 @@ See the file LICENSE for copying permission. """ +import sys -def tostring(xml=None, xmlns='', stanza_ns='', stream=None, outbuffer=''): +if sys.version_info < (3, 0): + import types + + +def tostring(xml=None, xmlns='', stanza_ns='', stream=None, + outbuffer='', top_level=False): """ Serialize an XML object to a Unicode string. @@ -26,6 +32,8 @@ def tostring(xml=None, xmlns='', stanza_ns='', stream=None, outbuffer=''): stream -- The XML stream that generated the XML object. outbuffer -- Optional buffer for storing serializations during recursive calls. + top_level -- Indicates that the element is the outermost + element. """ # Add previous results to the start of the output. output = [outbuffer] @@ -39,14 +47,21 @@ def tostring(xml=None, xmlns='', stanza_ns='', stream=None, outbuffer=''): else: tag_xmlns = '' + default_ns = '' + stream_ns = '' + if stream: + default_ns = stream.default_ns + stream_ns = stream.stream_ns + # Output the tag name and derived namespace of the element. namespace = '' - if tag_xmlns not in ['', xmlns, stanza_ns]: + if top_level and tag_xmlns not in ['', default_ns, stream_ns] or \ + tag_xmlns not in ['', xmlns, stanza_ns, stream_ns]: namespace = ' xmlns="%s"' % tag_xmlns - if stream and tag_xmlns in stream.namespace_map: - mapped_namespace = stream.namespace_map[tag_xmlns] - if mapped_namespace: - tag_name = "%s:%s" % (mapped_namespace, tag_name) + if stream and tag_xmlns in stream.namespace_map: + mapped_namespace = stream.namespace_map[tag_xmlns] + if mapped_namespace: + tag_name = "%s:%s" % (mapped_namespace, tag_name) output.append("<%s" % tag_name) output.append(namespace) @@ -93,6 +108,10 @@ def xml_escape(text): Arguments: text -- The XML text to convert. """ + if sys.version_info < (3, 0): + if type(text) != types.UnicodeType: + text = unicode(text, 'utf-8', 'ignore') + text = list(text) escapes = {'&': '&', '<': '<', diff --git a/sleekxmpp/xmlstream/tostring/__init__.py b/sleekxmpp/xmlstream/tostring/__init__.py deleted file mode 100644 index 5852cba2..00000000 --- a/sleekxmpp/xmlstream/tostring/__init__.py +++ /dev/null @@ -1,19 +0,0 @@ -""" - SleekXMPP: The Sleek XMPP Library - Copyright (C) 2010 Nathanael C. Fritz - This file is part of SleekXMPP. - - See the file LICENSE for copying permission. -""" - -import sys - -# Import the correct tostring and xml_escape functions based on the Python -# version in order to properly handle Unicode. - -if sys.version_info < (3, 0): - from sleekxmpp.xmlstream.tostring.tostring26 import tostring, xml_escape -else: - from sleekxmpp.xmlstream.tostring.tostring import tostring, xml_escape - -__all__ = ['tostring', 'xml_escape'] diff --git a/sleekxmpp/xmlstream/tostring/tostring26.py b/sleekxmpp/xmlstream/tostring/tostring26.py deleted file mode 100644 index 11501780..00000000 --- a/sleekxmpp/xmlstream/tostring/tostring26.py +++ /dev/null @@ -1,110 +0,0 @@ -""" - SleekXMPP: The Sleek XMPP Library - Copyright (C) 2010 Nathanael C. Fritz - This file is part of SleekXMPP. - - See the file LICENSE for copying permission. -""" - -from __future__ import unicode_literals -import types - - -def tostring(xml=None, xmlns='', stanza_ns='', stream=None, outbuffer=''): - """ - Serialize an XML object to a Unicode string. - - If namespaces are provided using xmlns or stanza_ns, then elements - that use those namespaces will not include the xmlns attribute in - the output. - - Arguments: - xml -- The XML object to serialize. If the value is None, - then the XML object contained in this stanza - object will be used. - xmlns -- Optional namespace of an element wrapping the XML - object. - stanza_ns -- The namespace of the stanza object that contains - the XML object. - stream -- The XML stream that generated the XML object. - outbuffer -- Optional buffer for storing serializations during - recursive calls. - """ - # Add previous results to the start of the output. - output = [outbuffer] - - # Extract the element's tag name. - tag_name = xml.tag.split('}', 1)[-1] - - # Extract the element's namespace if it is defined. - if '}' in xml.tag: - tag_xmlns = xml.tag.split('}', 1)[0][1:] - else: - tag_xmlns = u'' - - # Output the tag name and derived namespace of the element. - namespace = u'' - if tag_xmlns not in ['', xmlns, stanza_ns]: - namespace = u' xmlns="%s"' % tag_xmlns - if stream and tag_xmlns in stream.namespace_map: - mapped_namespace = stream.namespace_map[tag_xmlns] - if mapped_namespace: - tag_name = u"%s:%s" % (mapped_namespace, tag_name) - output.append(u"<%s" % tag_name) - output.append(namespace) - - # Output escaped attribute values. - for attrib, value in xml.attrib.items(): - value = xml_escape(value) - if '}' not in attrib: - output.append(' %s="%s"' % (attrib, value)) - else: - attrib_ns = attrib.split('}')[0][1:] - attrib = attrib.split('}')[1] - if stream and attrib_ns in stream.namespace_map: - mapped_ns = stream.namespace_map[attrib_ns] - if mapped_ns: - output.append(' %s:%s="%s"' % (mapped_ns, - attrib, - value)) - - if len(xml) or xml.text: - # If there are additional child elements to serialize. - output.append(u">") - if xml.text: - output.append(xml_escape(xml.text)) - if len(xml): - for child in xml.getchildren(): - output.append(tostring(child, tag_xmlns, stanza_ns, stream)) - output.append(u"</%s>" % tag_name) - elif xml.text: - # If we only have text content. - output.append(u">%s</%s>" % (xml_escape(xml.text), tag_name)) - else: - # Empty element. - output.append(u" />") - if xml.tail: - # If there is additional text after the element. - output.append(xml_escape(xml.tail)) - return u''.join(output) - - -def xml_escape(text): - """ - Convert special characters in XML to escape sequences. - - Arguments: - text -- The XML text to convert. - """ - if type(text) != types.UnicodeType: - text = list(unicode(text, 'utf-8', 'ignore')) - else: - text = list(text) - escapes = {u'&': u'&', - u'<': u'<', - u'>': u'>', - u"'": u''', - u'"': u'"'} - for i, c in enumerate(text): - text[i] = escapes.get(c, c) - return u''.join(text) diff --git a/tests/test_stanza_xep_0060.py b/tests/test_stanza_xep_0060.py index 8e6e820d..d42c11bd 100644 --- a/tests/test_stanza_xep_0060.py +++ b/tests/test_stanza_xep_0060.py @@ -1,6 +1,6 @@ from sleekxmpp.test import * import sleekxmpp.plugins.xep_0004 as xep_0004 -import sleekxmpp.plugins.stanza_pubsub as pubsub +import sleekxmpp.plugins.xep_0060.stanza as pubsub class TestPubsubStanzas(SleekTest): diff --git a/tests/test_tostring.py b/tests/test_tostring.py index 638e613a..e456d28e 100644 --- a/tests/test_tostring.py +++ b/tests/test_tostring.py @@ -102,11 +102,13 @@ class TestToString(SleekTest): """ Test that stanza objects are serialized properly. """ + self.stream_start() + utf8_message = '\xe0\xb2\xa0_\xe0\xb2\xa0' if not hasattr(utf8_message, 'decode'): # Python 3 utf8_message = bytes(utf8_message, encoding='utf-8') - msg = Message() + msg = self.Message() msg['body'] = utf8_message.decode('utf-8') expected = '<message><body>\xe0\xb2\xa0_\xe0\xb2\xa0</body></message>' result = msg.__str__() |