From bd9bf3f1c7c17606f455ce0cf9c4d0b6b237a7fe Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Thu, 27 Jan 2011 18:05:05 -0500 Subject: Update tostring methods. Will now always show top-level namespace, unless it is the same as the stream's default namespace. Also added the XMPP stream namespace to the namespace map as 'stream'. --- sleekxmpp/basexmpp.py | 3 +++ sleekxmpp/xmlstream/stanzabase.py | 7 +++++-- sleekxmpp/xmlstream/tostring/tostring.py | 22 ++++++++++++++++------ sleekxmpp/xmlstream/tostring/tostring26.py | 22 ++++++++++++++++------ tests/test_tostring.py | 4 +++- 5 files changed, 43 insertions(+), 15 deletions(-) diff --git a/sleekxmpp/basexmpp.py b/sleekxmpp/basexmpp.py index 3cf949a7..a490510a 100644 --- a/sleekxmpp/basexmpp.py +++ b/sleekxmpp/basexmpp.py @@ -106,6 +106,7 @@ class BaseXMPP(XMLStream): self.default_ns = default_ns self.stream_ns = 'http://etherx.jabber.org/streams' + self.namespace_map[self.stream_ns] = 'stream' self.boundjid = JID("") @@ -119,6 +120,8 @@ class BaseXMPP(XMLStream): self.sentpresence = False + self.stanza = sleekxmpp.stanza + self.register_handler( Callback('IM', MatchXPath('{%s}message/{%s}body' % (self.default_ns, diff --git a/sleekxmpp/xmlstream/stanzabase.py b/sleekxmpp/xmlstream/stanzabase.py index 3937a7a9..83d86999 100644 --- a/sleekxmpp/xmlstream/stanzabase.py +++ b/sleekxmpp/xmlstream/stanzabase.py @@ -1007,7 +1007,9 @@ class ElementBase(object): """ Return a string serialization of the underlying XML object. """ - return tostring(self.xml, xmlns='', stanza_ns=self.namespace) + return tostring(self.xml, xmlns='', + stanza_ns=self.namespace, + top_level=True) def __repr__(self): """ @@ -1217,4 +1219,5 @@ class StanzaBase(ElementBase): """Serialize the stanza's XML to a string.""" return tostring(self.xml, xmlns='', stanza_ns=self.namespace, - stream=self.stream) + stream=self.stream, + top_level = True) diff --git a/sleekxmpp/xmlstream/tostring/tostring.py b/sleekxmpp/xmlstream/tostring/tostring.py index 38b08d82..a6bb6ebc 100644 --- a/sleekxmpp/xmlstream/tostring/tostring.py +++ b/sleekxmpp/xmlstream/tostring/tostring.py @@ -7,7 +7,8 @@ """ -def tostring(xml=None, xmlns='', stanza_ns='', stream=None, outbuffer=''): +def tostring(xml=None, xmlns='', stanza_ns='', stream=None, + outbuffer='', top_level=False): """ Serialize an XML object to a Unicode string. @@ -26,6 +27,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 +42,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) diff --git a/sleekxmpp/xmlstream/tostring/tostring26.py b/sleekxmpp/xmlstream/tostring/tostring26.py index 11501780..3d1ca3d7 100644 --- a/sleekxmpp/xmlstream/tostring/tostring26.py +++ b/sleekxmpp/xmlstream/tostring/tostring26.py @@ -10,7 +10,8 @@ from __future__ import unicode_literals import types -def tostring(xml=None, xmlns='', stanza_ns='', stream=None, outbuffer=''): +def tostring(xml=None, xmlns='', stanza_ns='', stream=None, + outbuffer='', top_level=False): """ Serialize an XML object to a Unicode string. @@ -29,6 +30,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] @@ -42,14 +45,21 @@ def tostring(xml=None, xmlns='', stanza_ns='', stream=None, outbuffer=''): else: tag_xmlns = u'' + 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 = u'' - 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 = 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) + 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) 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 = '\xe0\xb2\xa0_\xe0\xb2\xa0' result = msg.__str__() -- cgit v1.2.3 From 1a270dc05cc368000f3545975befa0589031b684 Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Fri, 28 Jan 2011 00:49:37 -0500 Subject: First pass at re-worked stream features. Stream features now use stanza objects! Features are given a ranking that expresses the dependency relationships (since only one feature is negotiated at a time, the dependency graph can be replaced by a line). >>> xmpp.register_feature('my_feature', _my_handler, >>> restart=True, # Requires stream restart >>> order=600) # Ranking (out of ~ 10,000, >>> # lower #'s executed first) SASL mechanisms may now be added or disabled as needed. Each mechanism is given a priority value indicating the order in which the client wishes for mechanisms to be tried. Higher priority numbers are executed first. >>> xmpp.register_sasl_mechanism('SASL-MECH', _mech_handler, >>> priority=0) Disabling a SASL mechanism: >>> xmpp.remove_sasl_mechanism('ANONYMOUS') --- sleekxmpp/clientxmpp.py | 361 +++++++++++++++++++++--------------- sleekxmpp/stanza/__init__.py | 3 + sleekxmpp/stanza/bind.py | 26 +++ sleekxmpp/stanza/sasl.py | 104 +++++++++++ sleekxmpp/stanza/session.py | 25 +++ sleekxmpp/stanza/stream_features.py | 52 ++++++ sleekxmpp/stanza/tls.py | 50 +++++ 7 files changed, 473 insertions(+), 148 deletions(-) create mode 100644 sleekxmpp/stanza/bind.py create mode 100644 sleekxmpp/stanza/sasl.py create mode 100644 sleekxmpp/stanza/session.py create mode 100644 sleekxmpp/stanza/stream_features.py create mode 100644 sleekxmpp/stanza/tls.py diff --git a/sleekxmpp/clientxmpp.py b/sleekxmpp/clientxmpp.py index a1813985..e05f8e7b 100644 --- a/sleekxmpp/clientxmpp.py +++ b/sleekxmpp/clientxmpp.py @@ -18,9 +18,11 @@ import threading from sleekxmpp import plugins from sleekxmpp import stanza from sleekxmpp.basexmpp import BaseXMPP -from sleekxmpp.stanza import Message, Presence, Iq +from sleekxmpp.stanza import * +from sleekxmpp.stanza import tls +from sleekxmpp.stanza import sasl 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 * @@ -92,14 +94,24 @@ class ClientXMPP(BaseXMPP): self.stream_footer = "" self.features = [] - self.registered_features = [] + self._stream_feature_handlers = {} + self._stream_feature_order = [] + self._sasl_mechanism_handlers = {} + self._sasl_mechanism_priorities = [] #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_stanza(tls.Proceed) + self.register_stanza(sasl.Success) + self.register_stanza(sasl.Failure) + self.register_stanza(sasl.Auth) self.register_handler( Callback('Stream Features', @@ -112,32 +124,25 @@ class ClientXMPP(BaseXMPP): 'jabber:iq:roster')), self._handle_roster)) - self.register_feature( - "", - self._handle_starttls, True) - self.register_feature( - "", - self._handle_sasl_auth, True) - self.register_feature( - "", - self._handle_bind_resource) - self.register_feature( - "", - 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) + self.register_feature('starttls', self._handle_starttls, + restart=True, + order=0) + self.register_feature('mechanisms', self._handle_sasl_auth, + restart=True, + order=100) + self.register_feature('bind', self._handle_bind_resource, + restart=False, + order=10000) + self.register_feature('session', self._handle_start_session, + restart=False, + order=10001) + + self.register_sasl_mechanism('PLAIN', + self._handle_sasl_plain, + priority=1) + self.register_sasl_mechanism('ANONYMOUS', + self._handle_sasl_plain, + priority=0) def connect(self, address=tuple(), reattempt=True): """ @@ -197,19 +202,54 @@ class ClientXMPP(BaseXMPP): return XMLStream.connect(self, address[0], address[1], use_tls=True, 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._stream_feature_handlers[name] = (handler, restart) + self._stream_feature_order.append((order, name)) + self._stream_feature_order.sort() + + def register_sasl_mechanism(self, name, handler, priority=0): + """ + Register a handler for a SASL authentication mechanism. + + Arguments: + name -- The name of the mechanism (all caps) + handler -- The function that will perform the + authentication. The function must + return True if it is able to carry + out the authentication, False if + a required condition is not met. + priority -- An integer value indicating the + preferred ordering for the mechanism. + High values will be attempted first. + """ + self._sasl_mechanism_handlers[name] = handler + self._sasl_mechanism_priorities.append((priority, name)) + self._sasl_mechanism_priorities.sort(reverse=True) + + def remove_sasl_mechanism(self, name): + """ + Remove support for a given SASL authentication mechanism. + + Arguments: + name -- The name of the mechanism to remove (all caps) """ - self.registered_features.append((MatchXMLMask(mask), - pointer, - breaker)) + if name in self._sasl_mechanism_handlers: + del self._sasl_mechanism_handlers[name] + + p = self._sasl_mechanism_priorities + self._sasl_mechanism_priorities = [i for i in p if i[1] != name] def update_roster(self, jid, name=None, subscription=None, groups=[]): """ @@ -223,7 +263,8 @@ class ClientXMPP(BaseXMPP): to 'remove', the entry will be deleted. groups -- The roster groups that contain this item. """ - iq = self.Iq()._set_stanza_values({'type': 'set'}) + iq = self.Iq() + iq['type'] = 'set' iq['roster']['items'] = {jid: {'name': name, 'subscription': subscription, 'groups': groups}} @@ -242,10 +283,27 @@ class ClientXMPP(BaseXMPP): def get_roster(self): """Request the roster from the server.""" - iq = self.Iq()._set_stanza_values({'type': 'get'}).enable('roster') + iq = self.Iq() + iq['type'] = 'get' + iq.enable('roster') response = iq.send() self._handle_roster(response, request=True) + 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 = [] + + 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) + + self.schedule("session timeout checker", 15, session_timeout) + def _handle_stream_features(self, features): """ Process the received stream features. @@ -253,167 +311,174 @@ class ClientXMPP(BaseXMPP): 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): + 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_starttls(self, features): """ Handle notification that the server supports TLS. Arguments: - xml -- The STARTLS proceed element. + features -- The stream:features element. """ - if not self.authenticated and self.ssl_support: - tls_ns = 'urn:ietf:params:xml:ns:xmpp-tls' - self.add_handler("" % tls_ns, - self._handle_tls_start, - name='TLS Proceed', - instream=True) - self.send_xml(xml) + + def tls_proceed(proceed): + """Restart the XML stream when TLS is accepted.""" + log.debug("Starting TLS") + if self.start_tls(): + self.features.append('starttls') + raise RestartStream() + + if self.ssl_support: + self.register_handler( + Callback('STARTTLS Proceed', + MatchXPath(tls.Proceed.tag_name()), + tls_proceed, + instream=True)) + self.send(features['starttls']) 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): + def _handle_sasl_auth(self, features): """ - Handle encrypting the stream using TLS. + Handle authenticating using SASL. - Restarts the stream. + Arguments: + features -- The stream features stanza. """ - log.debug("Starting TLS") - if self.start_tls(): + + def sasl_success(stanza): + """SASL authentication succeeded. Restart the stream.""" + self.authenticated = True + self.features.append('mechanisms') raise RestartStream() - def _handle_sasl_auth(self, xml): + def sasl_fail(stanza): + """SASL authentication failed. Disconnect and shutdown.""" + log.info("Authentication failed.") + self.event("failed_auth", direct=True) + self.disconnect() + log.debug("Starting SASL Auth") + return True + + self.register_handler( + Callback('SASL Success', + MatchXPath(sasl.Success.tag_name()), + sasl_success, + instream=True, + once=True)) + + self.register_handler( + Callback('SASL Failure', + MatchXPath(sasl.Failure.tag_name()), + sasl_fail, + instream=True, + once=True)) + + for priority, mech in self._sasl_mechanism_priorities: + if mech in self._sasl_mechanism_handlers: + handler = self._sasl_mechanism_handlers[mech] + if handler(self): + break + else: + log.error("No appropriate login method.") + self.disconnect() + + return True + + def _handle_sasl_plain(self, xmpp): """ - Handle authenticating using SASL. + Attempt to authenticate using SASL PLAIN. Arguments: - xml -- The SASL mechanisms stanza. + xmpp -- The SleekXMPP connection instance. """ - if '{urn:ietf:params:xml:ns:xmpp-tls}starttls' in self.features: + if not xmpp.boundjid.user: return False - log.debug("Starting SASL Auth") - sasl_ns = 'urn:ietf:params:xml:ns:xmpp-sasl' - self.add_handler("" % sasl_ns, - self._handle_auth_success, - name='SASL Sucess', - instream=True) - self.add_handler("" % 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("%s" % ( - sasl_ns, - auth)) - elif 'sasl:ANONYMOUS' in self.features and not self.boundjid.user: - self.send("" % ( - sasl_ns, - 'ANONYMOUS')) - else: - log.error("No appropriate login method.") - self.disconnect() + 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') + + resp = sasl.Auth(xmpp) + resp['mechanism'] = 'PLAIN' + resp['value'] = auth + resp.send() + return True - def _handle_auth_success(self, xml): + def _handle_sasl_anonymous(self, xmpp): """ - SASL authentication succeeded. Restart the stream. + Attempt to authenticate using SASL ANONYMOUS. Arguments: - xml -- The SASL authentication success element. + xmpp -- The SleekXMPP connection instance. """ - self.authenticated = True - self.features = [] - raise RestartStream() + if xmpp.boundjid.user: + return False - def _handle_auth_fail(self, xml): - """ - SASL authentication failed. Disconnect and shutdown. + resp = sasl.Auth(xmpp) + resp['mechanism'] = 'ANONYMOUS' + resp.send() - Arguments: - xml -- The SASL authentication failure element. - """ - log.info("Authentication failed.") - self.event("failed_auth", direct=True) - self.disconnect() + return True - def _handle_bind_resource(self, xml): + def _handle_bind_resource(self, features): """ Handle requesting a specific resource. Arguments: - xml -- The bind feature element. + features -- The stream features stanza. """ log.debug("Requesting resource: %s" % self.boundjid.resource) - xml.clear() - iq = self.Iq(stype='set') + iq = self.Iq() + iq['type'] = 'set' + iq.enable('bind') if self.boundjid.resource: - res = ET.Element('resource') - res.text = self.boundjid.resource - xml.append(res) - iq.append(xml) + iq['bind']['resource'] = self.boundjid.resource response = iq.send() - 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.set_jid(response['bind']['jid']) 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: + + if 'session' not in features['features']: log.debug("Established Session") self.sessionstarted = True self.session_started_event.set() self.event("session_start") - def _handle_start_session(self, xml): + def _handle_start_session(self, features): """ Handle the start of the session. Arguments: - xml -- The session feature element. + feature -- The stream features element. """ - if self.authenticated and self.bound: - iq = self.makeIqSet(xml) - response = iq.send() - 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 + iq = self.Iq() + iq['type'] = 'set' + iq.enable('session') + response = iq.send() + + log.debug("Established Session") + self.sessionstarted = True + self.session_started_event.set() + self.event("session_start") def _handle_roster(self, iq, request=False): """ diff --git a/sleekxmpp/stanza/__init__.py b/sleekxmpp/stanza/__init__.py index dbf7b86f..ef44dfb2 100644 --- a/sleekxmpp/stanza/__init__.py +++ b/sleekxmpp/stanza/__init__.py @@ -12,3 +12,6 @@ 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.bind import Bind + diff --git a/sleekxmpp/stanza/bind.py b/sleekxmpp/stanza/bind.py new file mode 100644 index 00000000..ae1f96f0 --- /dev/null +++ b/sleekxmpp/stanza/bind.py @@ -0,0 +1,26 @@ +""" + 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.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' + + +register_stanza_plugin(Iq, Bind) +register_stanza_plugin(StreamFeatures, Bind) diff --git a/sleekxmpp/stanza/sasl.py b/sleekxmpp/stanza/sasl.py new file mode 100644 index 00000000..e55a72ad --- /dev/null +++ b/sleekxmpp/stanza/sasl.py @@ -0,0 +1,104 @@ +""" + 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.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) + + +class Success(StanzaBase): + + """ + """ + + name = 'success' + namespace = 'urn:ietf:params:xml:ns:xmpp-sasl' + interfaces = set() + plugin_attrib = name + + +class Failure(StanzaBase): + + """ + """ + + name = 'failure' + namespace = 'urn:ietf:params:xml:ns:xmpp-sasl' + interfaces = set() + plugin_attrib = name + + +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 set_value(self, value): + self.xml.text = value + + def get_value(self): + return self.xml.text + + def del_value(self): + self.xml.text = '' + + +register_stanza_plugin(StreamFeatures, Mechanisms) diff --git a/sleekxmpp/stanza/session.py b/sleekxmpp/stanza/session.py new file mode 100644 index 00000000..b7b175d2 --- /dev/null +++ b/sleekxmpp/stanza/session.py @@ -0,0 +1,25 @@ +""" + 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.stanza import Iq, StreamFeatures +from sleekxmpp.xmlstream import ElementBase, ET, register_stanza_plugin + + +class Session(ElementBase): + + """ + """ + + name = 'bind' + namespace = 'urn:ietf:params:xml:ns:xmpp-session' + interfaces = set() + plugin_attrib = 'session' + + +register_stanza_plugin(Iq, Session) +register_stanza_plugin(StreamFeatures, Session) 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/stanza/tls.py b/sleekxmpp/stanza/tls.py new file mode 100644 index 00000000..d85f9b49 --- /dev/null +++ b/sleekxmpp/stanza/tls.py @@ -0,0 +1,50 @@ +""" + 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.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() + + +register_stanza_plugin(StreamFeatures, STARTTLS) -- cgit v1.2.3 From af45b51f4fe32b37e80e51dea66b9fd0ca3d8ad2 Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Fri, 18 Mar 2011 17:57:49 -0400 Subject: Fix error with session feature. --- sleekxmpp/stanza/__init__.py | 1 + sleekxmpp/stanza/session.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/sleekxmpp/stanza/__init__.py b/sleekxmpp/stanza/__init__.py index ef44dfb2..4481fa42 100644 --- a/sleekxmpp/stanza/__init__.py +++ b/sleekxmpp/stanza/__init__.py @@ -14,4 +14,5 @@ from sleekxmpp.stanza.message import Message from sleekxmpp.stanza.presence import Presence from sleekxmpp.stanza.stream_features import StreamFeatures from sleekxmpp.stanza.bind import Bind +from sleekxmpp.stanza.session import Session diff --git a/sleekxmpp/stanza/session.py b/sleekxmpp/stanza/session.py index b7b175d2..c9d97157 100644 --- a/sleekxmpp/stanza/session.py +++ b/sleekxmpp/stanza/session.py @@ -15,7 +15,7 @@ class Session(ElementBase): """ """ - name = 'bind' + name = 'session' namespace = 'urn:ietf:params:xml:ns:xmpp-session' interfaces = set() plugin_attrib = 'session' -- cgit v1.2.3 From 58aa944a5e07032fc8d5770347f82e7c2ce6c948 Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Wed, 15 Jun 2011 10:55:36 -0700 Subject: Fix another roster issue. Caused by same issue of a JID being in the roster, but with an incomplete entry. --- sleekxmpp/basexmpp.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sleekxmpp/basexmpp.py b/sleekxmpp/basexmpp.py index 3992a4f9..a7d4931a 100644 --- a/sleekxmpp/basexmpp.py +++ b/sleekxmpp/basexmpp.py @@ -640,7 +640,8 @@ class BaseXMPP(XMLStream): log.debug("%s %s got offline" % (jid, resource)) del connections[resource] - if not connections and not self.roster[jid]['in_roster']: + if not connections and \ + not self.roster[jid].get('in_roster', False): del self.roster[jid] if not was_offline: self.event("got_offline", presence) -- cgit v1.2.3 From d8d9e8df16c07bd13bbac72e4445a2930407b244 Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Mon, 20 Jun 2011 16:25:56 -0700 Subject: Fix stanza clobbering when replying to errors. If a stanza handler raised an exception, the exception was processed and replied by the modified stanza, not a stanza with the original content. A copy is now made before handler processing, and if an exception occurs it is the copy that processes the exception using the original content. --- sleekxmpp/stanza/message.py | 2 +- sleekxmpp/xmlstream/xmlstream.py | 13 ++++++++----- tests/test_stream_exceptions.py | 29 +++++++++++++++++++++++++++++ 3 files changed, 38 insertions(+), 6 deletions(-) diff --git a/sleekxmpp/stanza/message.py b/sleekxmpp/stanza/message.py index cb3d344c..3518fc7a 100644 --- a/sleekxmpp/stanza/message.py +++ b/sleekxmpp/stanza/message.py @@ -97,7 +97,7 @@ class Message(RootStanza): clear -- Indicates if existing content should be removed before replying. Defaults to True. """ - StanzaBase.reply(self) + StanzaBase.reply(self, clear) if self['type'] == 'groupchat': self['to'] = self['to'].bare diff --git a/sleekxmpp/xmlstream/xmlstream.py b/sleekxmpp/xmlstream/xmlstream.py index 5bc71f04..6282c8d0 100644 --- a/sleekxmpp/xmlstream/xmlstream.py +++ b/sleekxmpp/xmlstream/xmlstream.py @@ -944,13 +944,14 @@ class XMLStream(object): func -- The event handler to execute. args -- Arguments to the event handler. """ + orig = copy.copy(args[0]) try: func(*args) except Exception as e: error_msg = 'Error processing event handler: %s' log.exception(error_msg % str(func)) - if hasattr(args[0], 'exception'): - args[0].exception(e) + if hasattr(orig, 'exception'): + orig.exception(e) def _event_runner(self): """ @@ -973,6 +974,7 @@ class XMLStream(object): etype, handler = event[0:2] args = event[2:] + orig = copy.copy(args[0]) if etype == 'stanza': try: @@ -980,7 +982,7 @@ class XMLStream(object): except Exception as e: error_msg = 'Error processing stream handler: %s' log.exception(error_msg % handler.name) - args[0].exception(e) + orig.exception(e) elif etype == 'schedule': try: log.debug('Scheduled event: %s' % args) @@ -989,6 +991,7 @@ class XMLStream(object): log.exception('Error processing scheduled task') elif etype == 'event': func, threaded, disposable = handler + orig = copy.copy(args[0]) try: if threaded: x = threading.Thread( @@ -1001,8 +1004,8 @@ class XMLStream(object): except Exception as e: error_msg = 'Error processing event handler: %s' log.exception(error_msg % str(func)) - if hasattr(args[0], 'exception'): - args[0].exception(e) + if hasattr(orig, 'exception'): + orig.exception(e) elif etype == 'quit': log.debug("Quitting event runner thread") return False diff --git a/tests/test_stream_exceptions.py b/tests/test_stream_exceptions.py index bc01c2a7..1143ce28 100644 --- a/tests/test_stream_exceptions.py +++ b/tests/test_stream_exceptions.py @@ -15,6 +15,35 @@ class TestStreamExceptions(SleekTest): sys.excepthook = sys.__excepthook__ self.stream_close() + def testExceptionReply(self): + """Test that raising an exception replies with the original stanza.""" + + def message(msg): + msg.reply() + msg['body'] = 'Body changed' + raise XMPPError(clear=False) + + + sys.excepthook = lambda *args, **kwargs: None + self.stream_start() + self.xmpp.add_event_handler('message', message) + + self.recv(""" + + This is going to cause an error. + + """) + + self.send(""" + + This is going to cause an error. + + + + + """) + def testXMPPErrorException(self): """Test raising an XMPPError exception.""" -- cgit v1.2.3 From 3b1f3fddf093f9bad80522287b8425a713ea8c5e Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Tue, 28 Jun 2011 11:06:44 -0700 Subject: Reorganized stream level stanzas. --- sleekxmpp/clientxmpp.py | 3 +- sleekxmpp/stanza/__init__.py | 9 ++-- sleekxmpp/stanza/bind.py | 26 --------- sleekxmpp/stanza/sasl.py | 104 ------------------------------------ sleekxmpp/stanza/session.py | 25 --------- sleekxmpp/stanza/stream/__init__.py | 13 +++++ sleekxmpp/stanza/stream/bind.py | 27 ++++++++++ sleekxmpp/stanza/stream/error.py | 69 ++++++++++++++++++++++++ sleekxmpp/stanza/stream/features.py | 52 ++++++++++++++++++ sleekxmpp/stanza/stream/sasl.py | 104 ++++++++++++++++++++++++++++++++++++ sleekxmpp/stanza/stream/session.py | 26 +++++++++ sleekxmpp/stanza/stream/tls.py | 50 +++++++++++++++++ sleekxmpp/stanza/stream_error.py | 69 ------------------------ sleekxmpp/stanza/stream_features.py | 52 ------------------ sleekxmpp/stanza/tls.py | 50 ----------------- 15 files changed, 346 insertions(+), 333 deletions(-) delete mode 100644 sleekxmpp/stanza/bind.py delete mode 100644 sleekxmpp/stanza/sasl.py delete mode 100644 sleekxmpp/stanza/session.py create mode 100644 sleekxmpp/stanza/stream/__init__.py create mode 100644 sleekxmpp/stanza/stream/bind.py create mode 100644 sleekxmpp/stanza/stream/error.py create mode 100644 sleekxmpp/stanza/stream/features.py create mode 100644 sleekxmpp/stanza/stream/sasl.py create mode 100644 sleekxmpp/stanza/stream/session.py create mode 100644 sleekxmpp/stanza/stream/tls.py delete mode 100644 sleekxmpp/stanza/stream_error.py delete mode 100644 sleekxmpp/stanza/stream_features.py delete mode 100644 sleekxmpp/stanza/tls.py diff --git a/sleekxmpp/clientxmpp.py b/sleekxmpp/clientxmpp.py index 3a5f41bd..ea9654a6 100644 --- a/sleekxmpp/clientxmpp.py +++ b/sleekxmpp/clientxmpp.py @@ -19,8 +19,7 @@ from sleekxmpp import plugins from sleekxmpp import stanza from sleekxmpp.basexmpp import BaseXMPP from sleekxmpp.stanza import * -from sleekxmpp.stanza import tls -from sleekxmpp.stanza import sasl +from sleekxmpp.stanza.stream import tls, sasl from sleekxmpp.xmlstream import XMLStream, RestartStream from sleekxmpp.xmlstream import StanzaBase, ET, register_stanza_plugin from sleekxmpp.xmlstream.matcher import * diff --git a/sleekxmpp/stanza/__init__.py b/sleekxmpp/stanza/__init__.py index 4481fa42..05df8837 100644 --- a/sleekxmpp/stanza/__init__.py +++ b/sleekxmpp/stanza/__init__.py @@ -8,11 +8,10 @@ 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.bind import Bind -from sleekxmpp.stanza.session import Session - +from sleekxmpp.stanza.stream import StreamFeatures +from sleekxmpp.stanza.stream import Bind +from sleekxmpp.stanza.stream import Session +from sleekxmpp.stanza.stream import StreamError diff --git a/sleekxmpp/stanza/bind.py b/sleekxmpp/stanza/bind.py deleted file mode 100644 index ae1f96f0..00000000 --- a/sleekxmpp/stanza/bind.py +++ /dev/null @@ -1,26 +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 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' - - -register_stanza_plugin(Iq, Bind) -register_stanza_plugin(StreamFeatures, Bind) diff --git a/sleekxmpp/stanza/sasl.py b/sleekxmpp/stanza/sasl.py deleted file mode 100644 index e55a72ad..00000000 --- a/sleekxmpp/stanza/sasl.py +++ /dev/null @@ -1,104 +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 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) - - -class Success(StanzaBase): - - """ - """ - - name = 'success' - namespace = 'urn:ietf:params:xml:ns:xmpp-sasl' - interfaces = set() - plugin_attrib = name - - -class Failure(StanzaBase): - - """ - """ - - name = 'failure' - namespace = 'urn:ietf:params:xml:ns:xmpp-sasl' - interfaces = set() - plugin_attrib = name - - -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 set_value(self, value): - self.xml.text = value - - def get_value(self): - return self.xml.text - - def del_value(self): - self.xml.text = '' - - -register_stanza_plugin(StreamFeatures, Mechanisms) diff --git a/sleekxmpp/stanza/session.py b/sleekxmpp/stanza/session.py deleted file mode 100644 index c9d97157..00000000 --- a/sleekxmpp/stanza/session.py +++ /dev/null @@ -1,25 +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 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' - - -register_stanza_plugin(Iq, Session) -register_stanza_plugin(StreamFeatures, Session) diff --git a/sleekxmpp/stanza/stream/__init__.py b/sleekxmpp/stanza/stream/__init__.py new file mode 100644 index 00000000..a386bbac --- /dev/null +++ b/sleekxmpp/stanza/stream/__init__.py @@ -0,0 +1,13 @@ +""" + 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.stanza.stream.error import StreamError +from sleekxmpp.stanza.stream.features import StreamFeatures +from sleekxmpp.stanza.stream.bind import Bind +from sleekxmpp.stanza.stream.session import Session diff --git a/sleekxmpp/stanza/stream/bind.py b/sleekxmpp/stanza/stream/bind.py new file mode 100644 index 00000000..165afcb4 --- /dev/null +++ b/sleekxmpp/stanza/stream/bind.py @@ -0,0 +1,27 @@ +""" + 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.stanza import Iq +from sleekxmpp.stanza.stream import 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' + + +register_stanza_plugin(Iq, Bind) +register_stanza_plugin(StreamFeatures, Bind) diff --git a/sleekxmpp/stanza/stream/error.py b/sleekxmpp/stanza/stream/error.py new file mode 100644 index 00000000..cf59a7fa --- /dev/null +++ b/sleekxmpp/stanza/stream/error.py @@ -0,0 +1,69 @@ +""" + 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.stanza.error import Error +from sleekxmpp.xmlstream import StanzaBase, ElementBase, ET +from sleekxmpp.xmlstream import register_stanza_plugin + + +class StreamError(Error, StanzaBase): + + """ + XMPP stanzas of type 'error' should include an stanza that + describes the nature of the error and how it should be handled. + + Use the 'XEP-0086: Error Condition Mappings' plugin to include error + codes used in older XMPP versions. + + The stream:error stanza is used to provide more information for + error that occur with the underlying XML stream itself, and not + a particular stanza. + + Note: The StreamError stanza is mostly the same as the normal + Error stanza, but with different namespaces and + condition names. + + Example error stanza: + + + + XML was not well-formed. + + + + Stanza Interface: + condition -- The name of the condition element. + text -- Human readable description of the error. + + Attributes: + conditions -- The set of allowable error condition elements. + condition_ns -- The namespace for the condition element. + + Methods: + setup -- Overrides ElementBase.setup. + get_condition -- Retrieve the name of the condition element. + set_condition -- Add a condition element. + del_condition -- Remove the condition element. + get_text -- Retrieve the contents of the element. + set_text -- Set the contents of the element. + del_text -- Remove the element. + """ + + namespace = 'http://etherx.jabber.org/streams' + interfaces = set(('condition', 'text')) + conditions = set(( + 'bad-format', 'bad-namespace-prefix', 'conflict', + 'connection-timeout', 'host-gone', 'host-unknown', + 'improper-addressing', 'internal-server-error', 'invalid-from', + 'invalid-namespace', 'invalid-xml', 'not-authorized', + 'not-well-formed', 'policy-violation', 'remote-connection-failed', + 'reset', 'resource-constraint', 'restricted-xml', 'see-other-host', + 'system-shutdown', 'undefined-condition', 'unsupported-encoding', + 'unsupported-feature', 'unsupported-stanza-type', + 'unsupported-version')) + condition_ns = 'urn:ietf:params:xml:ns:xmpp-streams' 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/stanza/stream/sasl.py b/sleekxmpp/stanza/stream/sasl.py new file mode 100644 index 00000000..e55a72ad --- /dev/null +++ b/sleekxmpp/stanza/stream/sasl.py @@ -0,0 +1,104 @@ +""" + 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.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) + + +class Success(StanzaBase): + + """ + """ + + name = 'success' + namespace = 'urn:ietf:params:xml:ns:xmpp-sasl' + interfaces = set() + plugin_attrib = name + + +class Failure(StanzaBase): + + """ + """ + + name = 'failure' + namespace = 'urn:ietf:params:xml:ns:xmpp-sasl' + interfaces = set() + plugin_attrib = name + + +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 set_value(self, value): + self.xml.text = value + + def get_value(self): + return self.xml.text + + def del_value(self): + self.xml.text = '' + + +register_stanza_plugin(StreamFeatures, Mechanisms) diff --git a/sleekxmpp/stanza/stream/session.py b/sleekxmpp/stanza/stream/session.py new file mode 100644 index 00000000..87f21857 --- /dev/null +++ b/sleekxmpp/stanza/stream/session.py @@ -0,0 +1,26 @@ +""" + 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.stanza import Iq +from sleekxmpp.stanza.stream import 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' + + +register_stanza_plugin(Iq, Session) +register_stanza_plugin(StreamFeatures, Session) diff --git a/sleekxmpp/stanza/stream/tls.py b/sleekxmpp/stanza/stream/tls.py new file mode 100644 index 00000000..d85f9b49 --- /dev/null +++ b/sleekxmpp/stanza/stream/tls.py @@ -0,0 +1,50 @@ +""" + 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.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() + + +register_stanza_plugin(StreamFeatures, STARTTLS) diff --git a/sleekxmpp/stanza/stream_error.py b/sleekxmpp/stanza/stream_error.py deleted file mode 100644 index cf59a7fa..00000000 --- a/sleekxmpp/stanza/stream_error.py +++ /dev/null @@ -1,69 +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 sleekxmpp.stanza.error import Error -from sleekxmpp.xmlstream import StanzaBase, ElementBase, ET -from sleekxmpp.xmlstream import register_stanza_plugin - - -class StreamError(Error, StanzaBase): - - """ - XMPP stanzas of type 'error' should include an stanza that - describes the nature of the error and how it should be handled. - - Use the 'XEP-0086: Error Condition Mappings' plugin to include error - codes used in older XMPP versions. - - The stream:error stanza is used to provide more information for - error that occur with the underlying XML stream itself, and not - a particular stanza. - - Note: The StreamError stanza is mostly the same as the normal - Error stanza, but with different namespaces and - condition names. - - Example error stanza: - - - - XML was not well-formed. - - - - Stanza Interface: - condition -- The name of the condition element. - text -- Human readable description of the error. - - Attributes: - conditions -- The set of allowable error condition elements. - condition_ns -- The namespace for the condition element. - - Methods: - setup -- Overrides ElementBase.setup. - get_condition -- Retrieve the name of the condition element. - set_condition -- Add a condition element. - del_condition -- Remove the condition element. - get_text -- Retrieve the contents of the element. - set_text -- Set the contents of the element. - del_text -- Remove the element. - """ - - namespace = 'http://etherx.jabber.org/streams' - interfaces = set(('condition', 'text')) - conditions = set(( - 'bad-format', 'bad-namespace-prefix', 'conflict', - 'connection-timeout', 'host-gone', 'host-unknown', - 'improper-addressing', 'internal-server-error', 'invalid-from', - 'invalid-namespace', 'invalid-xml', 'not-authorized', - 'not-well-formed', 'policy-violation', 'remote-connection-failed', - 'reset', 'resource-constraint', 'restricted-xml', 'see-other-host', - 'system-shutdown', 'undefined-condition', 'unsupported-encoding', - 'unsupported-feature', 'unsupported-stanza-type', - 'unsupported-version')) - condition_ns = 'urn:ietf:params:xml:ns:xmpp-streams' diff --git a/sleekxmpp/stanza/stream_features.py b/sleekxmpp/stanza/stream_features.py deleted file mode 100644 index 5be2e55f..00000000 --- a/sleekxmpp/stanza/stream_features.py +++ /dev/null @@ -1,52 +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 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/stanza/tls.py b/sleekxmpp/stanza/tls.py deleted file mode 100644 index d85f9b49..00000000 --- a/sleekxmpp/stanza/tls.py +++ /dev/null @@ -1,50 +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 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() - - -register_stanza_plugin(StreamFeatures, STARTTLS) -- cgit v1.2.3 From 9ed972ffeba8f5071d5cae8497322764207fec04 Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Wed, 29 Jun 2011 14:05:27 -0700 Subject: Fix SASL mechanism selection bug. ANONYMOUS was being treated as PLAIN, mechanism was being chosen purely from supported mechanisms, not those provided by the server. Broke nested handler methods into top-level methods. --- sleekxmpp/clientxmpp.py | 85 ++++++++++++++++++++++++------------------------- 1 file changed, 41 insertions(+), 44 deletions(-) diff --git a/sleekxmpp/clientxmpp.py b/sleekxmpp/clientxmpp.py index ea9654a6..5d7ca125 100644 --- a/sleekxmpp/clientxmpp.py +++ b/sleekxmpp/clientxmpp.py @@ -112,6 +112,23 @@ class ClientXMPP(BaseXMPP): self.default_ns, 'jabber:iq:roster')), self._handle_roster)) + self.register_handler( + Callback('SASL Success', + MatchXPath(sasl.Success.tag_name()), + self._handle_sasl_success, + instream=True, + once=True)) + self.register_handler( + Callback('SASL Failure', + MatchXPath(sasl.Failure.tag_name()), + self._handle_sasl_fail, + instream=True, + once=True)) + self.register_handler( + Callback('STARTTLS Proceed', + MatchXPath(tls.Proceed.tag_name()), + self._handle_starttls_proceed, + instream=True)) self.register_feature('starttls', self._handle_starttls, restart=True, @@ -130,7 +147,7 @@ class ClientXMPP(BaseXMPP): self._handle_sasl_plain, priority=1) self.register_sasl_mechanism('ANONYMOUS', - self._handle_sasl_plain, + self._handle_sasl_anonymous, priority=0) def connect(self, address=tuple(), reattempt=True, use_tls=True): @@ -349,22 +366,9 @@ class ClientXMPP(BaseXMPP): Arguments: features -- The stream:features element. """ - - def tls_proceed(proceed): - """Restart the XML stream when TLS is accepted.""" - log.debug("Starting TLS") - if self.start_tls(): - self.features.append('starttls') - raise RestartStream() - if not self.use_tls: return False elif self.ssl_support: - self.register_handler( - Callback('STARTTLS Proceed', - MatchXPath(tls.Proceed.tag_name()), - tls_proceed, - instream=True)) self.send(features['starttls'], now=True) return True else: @@ -372,6 +376,13 @@ class ClientXMPP(BaseXMPP): " 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.start_tls(): + self.features.append('starttls') + raise RestartStream() + def _handle_sasl_auth(self, features): """ Handle authenticating using SASL. @@ -379,46 +390,32 @@ class ClientXMPP(BaseXMPP): Arguments: features -- The stream features stanza. """ - - def sasl_success(stanza): - """SASL authentication succeeded. Restart the stream.""" - self.authenticated = True - self.features.append('mechanisms') - raise RestartStream() - - def sasl_fail(stanza): - """SASL authentication failed. Disconnect and shutdown.""" - log.info("Authentication failed.") - self.event("failed_auth", direct=True) - self.disconnect() - log.debug("Starting SASL Auth") - return True - - self.register_handler( - Callback('SASL Success', - MatchXPath(sasl.Success.tag_name()), - sasl_success, - instream=True, - once=True)) - - self.register_handler( - Callback('SASL Failure', - MatchXPath(sasl.Failure.tag_name()), - sasl_fail, - instream=True, - once=True)) - for priority, mech in self._sasl_mechanism_priorities: - if mech in self._sasl_mechanism_handlers: + if mech in features['mechanisms']: handler = self._sasl_mechanism_handlers[mech] if handler(self): break else: log.error("No appropriate login method.") + self.event("no_auth", direct=True) self.disconnect() return True + def _handle_sasl_success(self, stanza): + """SASL authentication succeeded. Restart the stream.""" + self.authenticated = True + self.features.append('mechanisms') + raise RestartStream() + + def _handle_sasl_fail(self, stanza): + """SASL authentication failed. Disconnect and shutdown.""" + log.info("Authentication failed.") + self.event("failed_auth", direct=True) + self.disconnect() + log.debug("Starting SASL Auth") + return True + def _handle_sasl_plain(self, xmpp): """ Attempt to authenticate using SASL PLAIN. -- cgit v1.2.3 From 754ac5092a3a37819a71f6565a1e54b3f2547940 Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Thu, 30 Jun 2011 15:40:22 -0700 Subject: Reorganize features into plugins. --- sleekxmpp/basexmpp.py | 23 ++-- sleekxmpp/clientxmpp.py | 207 +++---------------------------- sleekxmpp/features/__init__.py | 10 ++ sleekxmpp/features/feature_bind.py | 55 ++++++++ sleekxmpp/features/feature_mechanisms.py | 116 +++++++++++++++++ sleekxmpp/features/feature_session.py | 46 +++++++ sleekxmpp/features/feature_starttls.py | 61 +++++++++ sleekxmpp/features/sasl_anonymous.py | 31 +++++ sleekxmpp/features/sasl_plain.py | 41 ++++++ sleekxmpp/plugins/base.py | 3 +- 10 files changed, 393 insertions(+), 200 deletions(-) create mode 100644 sleekxmpp/features/__init__.py create mode 100644 sleekxmpp/features/feature_bind.py create mode 100644 sleekxmpp/features/feature_mechanisms.py create mode 100644 sleekxmpp/features/feature_session.py create mode 100644 sleekxmpp/features/feature_starttls.py create mode 100644 sleekxmpp/features/sasl_anonymous.py create mode 100644 sleekxmpp/features/sasl_plain.py diff --git a/sleekxmpp/basexmpp.py b/sleekxmpp/basexmpp.py index 8e5c762a..43ad420f 100644 --- a/sleekxmpp/basexmpp.py +++ b/sleekxmpp/basexmpp.py @@ -165,9 +165,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__("%s.%s" % (module.__name__, plugin), + globals(), locals(), [plugin]) + except ImportError: + module = sleekxmpp.features + module = __import__("%s.%s" % (module.__name__, plugin), + globals(), locals(), [plugin]) if isinstance(module, str): # We probably want to load a module from outside # the sleekxmpp package, so leave out the globals(). @@ -176,12 +181,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 5d7ca125..9c2696da 100644 --- a/sleekxmpp/clientxmpp.py +++ b/sleekxmpp/clientxmpp.py @@ -15,8 +15,10 @@ 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 * from sleekxmpp.stanza.stream import tls, sasl @@ -97,10 +99,6 @@ class ClientXMPP(BaseXMPP): self.add_event_handler('connected', self._handle_connected) self.register_stanza(StreamFeatures) - self.register_stanza(tls.Proceed) - self.register_stanza(sasl.Success) - self.register_stanza(sasl.Failure) - self.register_stanza(sasl.Auth) self.register_handler( Callback('Stream Features', @@ -112,43 +110,18 @@ class ClientXMPP(BaseXMPP): self.default_ns, 'jabber:iq:roster')), self._handle_roster)) - self.register_handler( - Callback('SASL Success', - MatchXPath(sasl.Success.tag_name()), - self._handle_sasl_success, - instream=True, - once=True)) - self.register_handler( - Callback('SASL Failure', - MatchXPath(sasl.Failure.tag_name()), - self._handle_sasl_fail, - instream=True, - once=True)) - self.register_handler( - Callback('STARTTLS Proceed', - MatchXPath(tls.Proceed.tag_name()), - self._handle_starttls_proceed, - instream=True)) - - self.register_feature('starttls', self._handle_starttls, - restart=True, - order=0) - self.register_feature('mechanisms', self._handle_sasl_auth, - restart=True, - order=100) - self.register_feature('bind', self._handle_bind_resource, - restart=False, - order=10000) - self.register_feature('session', self._handle_start_session, - restart=False, - order=10001) - - self.register_sasl_mechanism('PLAIN', - self._handle_sasl_plain, - priority=1) - self.register_sasl_mechanism('ANONYMOUS', - self._handle_sasl_anonymous, - priority=0) + + # Setup default stream features + self.register_plugin('feature_starttls') + self.register_plugin('feature_mechanisms') + self.register_plugin('feature_bind') + self.register_plugin('feature_session') + + # Setup default SASL mechanisms + self.register_plugin('sasl_plain', + {'priority': 1}) + self.register_plugin('sasl_anonymous', + {'priority': 0}) def connect(self, address=tuple(), reattempt=True, use_tls=True): """ @@ -242,9 +215,7 @@ class ClientXMPP(BaseXMPP): preferred ordering for the mechanism. High values will be attempted first. """ - self._sasl_mechanism_handlers[name] = handler - self._sasl_mechanism_priorities.append((priority, name)) - self._sasl_mechanism_priorities.sort(reverse=True) + self['feature_mechanisms'].register_mechanism(name, handler, priority) def remove_sasl_mechanism(self, name): """ @@ -253,11 +224,7 @@ class ClientXMPP(BaseXMPP): Arguments: name -- The name of the mechanism to remove (all caps) """ - if name in self._sasl_mechanism_handlers: - del self._sasl_mechanism_handlers[name] - - p = self._sasl_mechanism_priorities - self._sasl_mechanism_priorities = [i for i in p if i[1] != name] + self['feature_mechanisms'].remove_mechanism(name) def update_roster(self, jid, name=None, subscription=None, groups=[], block=True, timeout=None, callback=None): @@ -359,148 +326,6 @@ class ClientXMPP(BaseXMPP): # restarting the XML stream. return True - def _handle_starttls(self, features): - """ - Handle notification that the server supports TLS. - - Arguments: - features -- The stream:features element. - """ - if not self.use_tls: - return False - elif self.ssl_support: - self.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.start_tls(): - self.features.append('starttls') - raise RestartStream() - - def _handle_sasl_auth(self, features): - """ - Handle authenticating using SASL. - - Arguments: - features -- The stream features stanza. - """ - for priority, mech in self._sasl_mechanism_priorities: - if mech in features['mechanisms']: - handler = self._sasl_mechanism_handlers[mech] - if handler(self): - break - else: - log.error("No appropriate login method.") - self.event("no_auth", direct=True) - self.disconnect() - - return True - - def _handle_sasl_success(self, stanza): - """SASL authentication succeeded. Restart the stream.""" - self.authenticated = True - self.features.append('mechanisms') - raise RestartStream() - - def _handle_sasl_fail(self, stanza): - """SASL authentication failed. Disconnect and shutdown.""" - log.info("Authentication failed.") - self.event("failed_auth", direct=True) - self.disconnect() - log.debug("Starting SASL Auth") - return True - - def _handle_sasl_plain(self, xmpp): - """ - Attempt to authenticate using SASL PLAIN. - - Arguments: - xmpp -- The SleekXMPP connection instance. - """ - if not xmpp.boundjid.user: - return False - - 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') - - resp = sasl.Auth(xmpp) - resp['mechanism'] = 'PLAIN' - resp['value'] = auth - resp.send(now=True) - return True - - def _handle_sasl_anonymous(self, xmpp): - """ - Attempt to authenticate using SASL ANONYMOUS. - - Arguments: - xmpp -- The SleekXMPP connection instance. - """ - if xmpp.boundjid.user: - return False - - resp = sasl.Auth(xmpp) - resp['mechanism'] = 'ANONYMOUS' - resp.send() - - return True - - def _handle_bind_resource(self, features): - """ - Handle requesting a specific resource. - - Arguments: - features -- The stream features stanza. - """ - log.debug("Requesting resource: %s" % self.boundjid.resource) - iq = self.Iq() - iq['type'] = 'set' - iq.enable('bind') - if self.boundjid.resource: - iq['bind']['resource'] = self.boundjid.resource - response = iq.send(now=True) - - self.set_jid(response['bind']['jid']) - self.bound = True - - log.info("Node set to: %s" % self.boundjid.full) - - if 'session' not in features['features']: - log.debug("Established Session") - self.sessionstarted = True - self.session_started_event.set() - self.event("session_start") - - def _handle_start_session(self, features): - """ - Handle the start of the session. - - Arguments: - feature -- The stream features element. - """ - iq = self.Iq() - iq['type'] = 'set' - iq.enable('session') - response = iq.send(now=True) - - log.debug("Established Session") - self.sessionstarted = True - self.session_started_event.set() - self.event("session_start") - def _handle_roster(self, iq, request=False): """ Update the roster after receiving a roster stanza. diff --git a/sleekxmpp/features/__init__.py b/sleekxmpp/features/__init__.py new file mode 100644 index 00000000..940a37f1 --- /dev/null +++ b/sleekxmpp/features/__init__.py @@ -0,0 +1,10 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2010 Nathanael C. Fritz + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +__all__ = ['feature_starttls', 'feature_mechanisms', + 'sasl_plain', 'sasl_anonymous'] diff --git a/sleekxmpp/features/feature_bind.py b/sleekxmpp/features/feature_bind.py new file mode 100644 index 00000000..caa3844b --- /dev/null +++ b/sleekxmpp/features/feature_bind.py @@ -0,0 +1,55 @@ +""" + 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 logging + +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.xmpp.register_feature('bind', + self._handle_bind_resource, + restart=False, + order=10000) + + 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 + + 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_mechanisms.py b/sleekxmpp/features/feature_mechanisms.py new file mode 100644 index 00000000..994c9bed --- /dev/null +++ b/sleekxmpp/features/feature_mechanisms.py @@ -0,0 +1,116 @@ +""" + 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 logging + +from sleekxmpp.stanza import stream +from sleekxmpp.xmlstream import RestartStream +from sleekxmpp.xmlstream.matcher import * +from sleekxmpp.xmlstream.handler import * +from sleekxmpp.plugins.base import base_plugin + + +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.xmpp.register_stanza(stream.sasl.Success) + self.xmpp.register_stanza(stream.sasl.Failure) + self.xmpp.register_stanza(stream.sasl.Auth) + + self._mechanism_handlers = {} + self._mechanism_priorities = [] + + self.xmpp.register_handler( + Callback('SASL Success', + MatchXPath(stream.sasl.Success.tag_name()), + self._handle_success, + instream=True, + once=True)) + self.xmpp.register_handler( + Callback('SASL Failure', + MatchXPath(stream.sasl.Failure.tag_name()), + self._handle_fail, + instream=True, + once=True)) + + self.xmpp.register_feature('mechanisms', + self._handle_sasl_auth, + restart=True, + order=self.config.get('order', 100)) + + def register_mechanism(self, name, handler, priority=0): + """ + Register a handler for a SASL authentication mechanism. + + Arguments: + name -- The name of the mechanism (all caps) + handler -- The function that will perform the + authentication. The function must + return True if it is able to carry + out the authentication, False if + a required condition is not met. + priority -- An integer value indicating the + preferred ordering for the mechanism. + High values will be attempted first. + """ + self._mechanism_handlers[name] = handler + self._mechanism_priorities.append((priority, name)) + self._mechanism_priorities.sort(reverse=True) + + def remove_mechanism(self, name): + """ + Remove support for a given SASL authentication mechanism. + + Arguments: + name -- The name of the mechanism to remove (all caps) + """ + if name in self._mechanism_handlers: + del self._mechanism_handlers[name] + + p = self._mechanism_priorities + self._mechanism_priorities = [i for i in p if i[1] != name] + + def _handle_sasl_auth(self, features): + """ + Handle authenticating using SASL. + + Arguments: + features -- The stream features stanza. + """ + for priority, mech in self._mechanism_priorities: + if mech in features['mechanisms']: + log.debug('Attempt to use SASL %s' % mech) + if self._mechanism_handlers[mech](): + break + else: + log.error("No appropriate login method.") + self.xmpp.event("no_auth", direct=True) + self.xmpp.disconnect() + + return True + + def _handle_success(self, stanza): + """SASL authentication succeeded. Restart the stream.""" + self.xmpp.authenticated = True + self.xmpp.features.append('mechanisms') + raise RestartStream() + + def _handle_fail(self, stanza): + """SASL authentication failed. Disconnect and shutdown.""" + log.info("Authentication failed.") + self.xmpp.event("failed_auth", direct=True) + self.xmpp.disconnect() + log.debug("Starting SASL Auth") + return True diff --git a/sleekxmpp/features/feature_session.py b/sleekxmpp/features/feature_session.py new file mode 100644 index 00000000..5bae358c --- /dev/null +++ b/sleekxmpp/features/feature_session.py @@ -0,0 +1,46 @@ +""" + 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 logging + +from sleekxmpp.xmlstream.matcher import * +from sleekxmpp.xmlstream.handler import * +from sleekxmpp.plugins.base import base_plugin + + +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.xmpp.register_feature('session', + self._handle_start_session, + restart=False, + order=10001) + + 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) + + 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_starttls.py b/sleekxmpp/features/feature_starttls.py new file mode 100644 index 00000000..5367fa49 --- /dev/null +++ b/sleekxmpp/features/feature_starttls.py @@ -0,0 +1,61 @@ +""" + 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 logging + +from sleekxmpp.stanza.stream import tls +from sleekxmpp.xmlstream import RestartStream +from sleekxmpp.xmlstream.matcher import * +from sleekxmpp.xmlstream.handler import * +from sleekxmpp.plugins.base import base_plugin + + +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.xmpp.register_stanza(tls.Proceed) + self.xmpp.register_handler( + Callback('STARTTLS Proceed', + MatchXPath(tls.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)) + + def _handle_starttls(self, features): + """ + Handle notification that the server supports TLS. + + Arguments: + features -- The stream:features element. + """ + if 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.append('starttls') + raise RestartStream() diff --git a/sleekxmpp/features/sasl_anonymous.py b/sleekxmpp/features/sasl_anonymous.py new file mode 100644 index 00000000..469d9d19 --- /dev/null +++ b/sleekxmpp/features/sasl_anonymous.py @@ -0,0 +1,31 @@ +import base64 +import sys +import logging + +from sleekxmpp.stanza.stream import sasl +from sleekxmpp.plugins.base import base_plugin + + +log = logging.getLogger(__name__) + + +class sasl_anonymous(base_plugin): + + def plugin_init(self): + self.name = 'SASL ANONYMOUS' + self.rfc = '6120' + self.description = 'SASL ANONYMOUS Mechanism' + + self.xmpp.register_sasl_mechanism('ANONYMOUS', + self._handle_anonymous, + priority=self.config.get('priority', 0)) + + def _handle_anonymous(self): + if self.xmpp.boundjid.user: + return False + + resp = sasl.Auth(self.xmpp) + resp['mechanism'] = 'ANONYMOUS' + resp.send(now=True) + + return True diff --git a/sleekxmpp/features/sasl_plain.py b/sleekxmpp/features/sasl_plain.py new file mode 100644 index 00000000..36c7d9df --- /dev/null +++ b/sleekxmpp/features/sasl_plain.py @@ -0,0 +1,41 @@ +import base64 +import sys +import logging + +from sleekxmpp.stanza.stream import sasl +from sleekxmpp.plugins.base import base_plugin + + +log = logging.getLogger(__name__) + + +class sasl_plain(base_plugin): + + def plugin_init(self): + self.name = 'SASL PLAIN' + self.rfc = '6120' + self.description = 'SASL PLAIN Mechanism' + + self.xmpp.register_sasl_mechanism('PLAIN', + self._handle_plain, + priority=self.config.get('priority', 1)) + + def _handle_plain(self): + if not self.xmpp.boundjid.user: + return False + + if sys.version_info < (3, 0): + user = bytes(self.xmpp.boundjid.user) + password = bytes(self.xmpp.password) + else: + user = bytes(self.xmpp.boundjid.user, 'utf-8') + password = bytes(self.xmpp.password, 'utf-8') + + auth = base64.b64encode(b'\x00' + user + \ + b'\x00' + password).decode('utf-8') + + resp = sasl.Auth(self.xmpp) + resp['mechanism'] = 'PLAIN' + resp['value'] = auth + resp.send(now=True) + return True 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 -- cgit v1.2.3 From 634f5d691bab9855deddc4c201389bb60470d76e Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Fri, 1 Jul 2011 14:45:55 -0700 Subject: Continued reorganization and streamlining. --- sleekxmpp/basexmpp.py | 8 +- sleekxmpp/clientxmpp.py | 2 - sleekxmpp/features/__init__.py | 1 + sleekxmpp/features/feature_bind.py | 55 ---------- sleekxmpp/features/feature_bind/__init__.py | 10 ++ sleekxmpp/features/feature_bind/bind.py | 62 +++++++++++ sleekxmpp/features/feature_bind/stanza.py | 22 ++++ sleekxmpp/features/feature_mechanisms.py | 116 --------------------- sleekxmpp/features/feature_mechanisms/__init__.py | 10 ++ .../features/feature_mechanisms/mechanisms.py | 116 +++++++++++++++++++++ sleekxmpp/features/feature_mechanisms/stanza.py | 104 ++++++++++++++++++ sleekxmpp/features/feature_session.py | 46 -------- sleekxmpp/features/feature_session/__init__.py | 10 ++ sleekxmpp/features/feature_session/session.py | 54 ++++++++++ sleekxmpp/features/feature_session/stanza.py | 21 ++++ sleekxmpp/features/feature_starttls.py | 61 ----------- sleekxmpp/features/feature_starttls/__init__.py | 10 ++ sleekxmpp/features/feature_starttls/stanza.py | 47 +++++++++ sleekxmpp/features/feature_starttls/starttls.py | 66 ++++++++++++ sleekxmpp/stanza/__init__.py | 6 +- sleekxmpp/stanza/stream/__init__.py | 5 - sleekxmpp/stanza/stream/bind.py | 27 ----- sleekxmpp/stanza/stream/error.py | 69 ------------ sleekxmpp/stanza/stream/features.py | 52 --------- sleekxmpp/stanza/stream/sasl.py | 104 ------------------ sleekxmpp/stanza/stream/session.py | 26 ----- sleekxmpp/stanza/stream/tls.py | 50 --------- sleekxmpp/stanza/stream_error.py | 69 ++++++++++++ sleekxmpp/stanza/stream_features.py | 52 +++++++++ 29 files changed, 660 insertions(+), 621 deletions(-) delete mode 100644 sleekxmpp/features/feature_bind.py create mode 100644 sleekxmpp/features/feature_bind/__init__.py create mode 100644 sleekxmpp/features/feature_bind/bind.py create mode 100644 sleekxmpp/features/feature_bind/stanza.py delete mode 100644 sleekxmpp/features/feature_mechanisms.py create mode 100644 sleekxmpp/features/feature_mechanisms/__init__.py create mode 100644 sleekxmpp/features/feature_mechanisms/mechanisms.py create mode 100644 sleekxmpp/features/feature_mechanisms/stanza.py delete mode 100644 sleekxmpp/features/feature_session.py create mode 100644 sleekxmpp/features/feature_session/__init__.py create mode 100644 sleekxmpp/features/feature_session/session.py create mode 100644 sleekxmpp/features/feature_session/stanza.py delete mode 100644 sleekxmpp/features/feature_starttls.py create mode 100644 sleekxmpp/features/feature_starttls/__init__.py create mode 100644 sleekxmpp/features/feature_starttls/stanza.py create mode 100644 sleekxmpp/features/feature_starttls/starttls.py delete mode 100644 sleekxmpp/stanza/stream/bind.py delete mode 100644 sleekxmpp/stanza/stream/error.py delete mode 100644 sleekxmpp/stanza/stream/features.py delete mode 100644 sleekxmpp/stanza/stream/sasl.py delete mode 100644 sleekxmpp/stanza/stream/session.py delete mode 100644 sleekxmpp/stanza/stream/tls.py create mode 100644 sleekxmpp/stanza/stream_error.py create mode 100644 sleekxmpp/stanza/stream_features.py diff --git a/sleekxmpp/basexmpp.py b/sleekxmpp/basexmpp.py index 43ad420f..b188e767 100644 --- a/sleekxmpp/basexmpp.py +++ b/sleekxmpp/basexmpp.py @@ -167,12 +167,12 @@ class BaseXMPP(XMLStream): if not module: try: module = sleekxmpp.plugins - module = __import__("%s.%s" % (module.__name__, plugin), - globals(), locals(), [plugin]) + module = __import__(str("%s.%s" % (module.__name__, plugin)), + globals(), locals(), [str(plugin)]) except ImportError: module = sleekxmpp.features - module = __import__("%s.%s" % (module.__name__, plugin), - globals(), locals(), [plugin]) + 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(). diff --git a/sleekxmpp/clientxmpp.py b/sleekxmpp/clientxmpp.py index 9c2696da..7245053f 100644 --- a/sleekxmpp/clientxmpp.py +++ b/sleekxmpp/clientxmpp.py @@ -87,8 +87,6 @@ class ClientXMPP(BaseXMPP): self.features = [] self._stream_feature_handlers = {} self._stream_feature_order = [] - self._sasl_mechanism_handlers = {} - self._sasl_mechanism_priorities = [] #TODO: Use stream state here self.authenticated = False diff --git a/sleekxmpp/features/__init__.py b/sleekxmpp/features/__init__.py index 940a37f1..65d2bdbf 100644 --- a/sleekxmpp/features/__init__.py +++ b/sleekxmpp/features/__init__.py @@ -7,4 +7,5 @@ """ __all__ = ['feature_starttls', 'feature_mechanisms', + 'feature_bind', 'feature_session', 'sasl_plain', 'sasl_anonymous'] diff --git a/sleekxmpp/features/feature_bind.py b/sleekxmpp/features/feature_bind.py deleted file mode 100644 index caa3844b..00000000 --- a/sleekxmpp/features/feature_bind.py +++ /dev/null @@ -1,55 +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 logging - -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.xmpp.register_feature('bind', - self._handle_bind_resource, - restart=False, - order=10000) - - 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 - - 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/__init__.py b/sleekxmpp/features/feature_bind/__init__.py new file mode 100644 index 00000000..fce94dd6 --- /dev/null +++ b/sleekxmpp/features/feature_bind/__init__.py @@ -0,0 +1,10 @@ +""" + 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.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..e177d7b2 --- /dev/null +++ b/sleekxmpp/features/feature_bind/bind.py @@ -0,0 +1,62 @@ +""" + 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 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 + + 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..f3e025fa --- /dev/null +++ b/sleekxmpp/features/feature_bind/stanza.py @@ -0,0 +1,22 @@ +""" + 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.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.py b/sleekxmpp/features/feature_mechanisms.py deleted file mode 100644 index 994c9bed..00000000 --- a/sleekxmpp/features/feature_mechanisms.py +++ /dev/null @@ -1,116 +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 logging - -from sleekxmpp.stanza import stream -from sleekxmpp.xmlstream import RestartStream -from sleekxmpp.xmlstream.matcher import * -from sleekxmpp.xmlstream.handler import * -from sleekxmpp.plugins.base import base_plugin - - -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.xmpp.register_stanza(stream.sasl.Success) - self.xmpp.register_stanza(stream.sasl.Failure) - self.xmpp.register_stanza(stream.sasl.Auth) - - self._mechanism_handlers = {} - self._mechanism_priorities = [] - - self.xmpp.register_handler( - Callback('SASL Success', - MatchXPath(stream.sasl.Success.tag_name()), - self._handle_success, - instream=True, - once=True)) - self.xmpp.register_handler( - Callback('SASL Failure', - MatchXPath(stream.sasl.Failure.tag_name()), - self._handle_fail, - instream=True, - once=True)) - - self.xmpp.register_feature('mechanisms', - self._handle_sasl_auth, - restart=True, - order=self.config.get('order', 100)) - - def register_mechanism(self, name, handler, priority=0): - """ - Register a handler for a SASL authentication mechanism. - - Arguments: - name -- The name of the mechanism (all caps) - handler -- The function that will perform the - authentication. The function must - return True if it is able to carry - out the authentication, False if - a required condition is not met. - priority -- An integer value indicating the - preferred ordering for the mechanism. - High values will be attempted first. - """ - self._mechanism_handlers[name] = handler - self._mechanism_priorities.append((priority, name)) - self._mechanism_priorities.sort(reverse=True) - - def remove_mechanism(self, name): - """ - Remove support for a given SASL authentication mechanism. - - Arguments: - name -- The name of the mechanism to remove (all caps) - """ - if name in self._mechanism_handlers: - del self._mechanism_handlers[name] - - p = self._mechanism_priorities - self._mechanism_priorities = [i for i in p if i[1] != name] - - def _handle_sasl_auth(self, features): - """ - Handle authenticating using SASL. - - Arguments: - features -- The stream features stanza. - """ - for priority, mech in self._mechanism_priorities: - if mech in features['mechanisms']: - log.debug('Attempt to use SASL %s' % mech) - if self._mechanism_handlers[mech](): - break - else: - log.error("No appropriate login method.") - self.xmpp.event("no_auth", direct=True) - self.xmpp.disconnect() - - return True - - def _handle_success(self, stanza): - """SASL authentication succeeded. Restart the stream.""" - self.xmpp.authenticated = True - self.xmpp.features.append('mechanisms') - raise RestartStream() - - def _handle_fail(self, stanza): - """SASL authentication failed. Disconnect and shutdown.""" - log.info("Authentication failed.") - self.xmpp.event("failed_auth", direct=True) - self.xmpp.disconnect() - log.debug("Starting SASL Auth") - return True diff --git a/sleekxmpp/features/feature_mechanisms/__init__.py b/sleekxmpp/features/feature_mechanisms/__init__.py new file mode 100644 index 00000000..a93b2b6f --- /dev/null +++ b/sleekxmpp/features/feature_mechanisms/__init__.py @@ -0,0 +1,10 @@ +""" + 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.features.feature_mechanisms.mechanisms import feature_mechanisms +from sleekxmpp.features.feature_mechanisms.stanza import * diff --git a/sleekxmpp/features/feature_mechanisms/mechanisms.py b/sleekxmpp/features/feature_mechanisms/mechanisms.py new file mode 100644 index 00000000..994c9bed --- /dev/null +++ b/sleekxmpp/features/feature_mechanisms/mechanisms.py @@ -0,0 +1,116 @@ +""" + 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 logging + +from sleekxmpp.stanza import stream +from sleekxmpp.xmlstream import RestartStream +from sleekxmpp.xmlstream.matcher import * +from sleekxmpp.xmlstream.handler import * +from sleekxmpp.plugins.base import base_plugin + + +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.xmpp.register_stanza(stream.sasl.Success) + self.xmpp.register_stanza(stream.sasl.Failure) + self.xmpp.register_stanza(stream.sasl.Auth) + + self._mechanism_handlers = {} + self._mechanism_priorities = [] + + self.xmpp.register_handler( + Callback('SASL Success', + MatchXPath(stream.sasl.Success.tag_name()), + self._handle_success, + instream=True, + once=True)) + self.xmpp.register_handler( + Callback('SASL Failure', + MatchXPath(stream.sasl.Failure.tag_name()), + self._handle_fail, + instream=True, + once=True)) + + self.xmpp.register_feature('mechanisms', + self._handle_sasl_auth, + restart=True, + order=self.config.get('order', 100)) + + def register_mechanism(self, name, handler, priority=0): + """ + Register a handler for a SASL authentication mechanism. + + Arguments: + name -- The name of the mechanism (all caps) + handler -- The function that will perform the + authentication. The function must + return True if it is able to carry + out the authentication, False if + a required condition is not met. + priority -- An integer value indicating the + preferred ordering for the mechanism. + High values will be attempted first. + """ + self._mechanism_handlers[name] = handler + self._mechanism_priorities.append((priority, name)) + self._mechanism_priorities.sort(reverse=True) + + def remove_mechanism(self, name): + """ + Remove support for a given SASL authentication mechanism. + + Arguments: + name -- The name of the mechanism to remove (all caps) + """ + if name in self._mechanism_handlers: + del self._mechanism_handlers[name] + + p = self._mechanism_priorities + self._mechanism_priorities = [i for i in p if i[1] != name] + + def _handle_sasl_auth(self, features): + """ + Handle authenticating using SASL. + + Arguments: + features -- The stream features stanza. + """ + for priority, mech in self._mechanism_priorities: + if mech in features['mechanisms']: + log.debug('Attempt to use SASL %s' % mech) + if self._mechanism_handlers[mech](): + break + else: + log.error("No appropriate login method.") + self.xmpp.event("no_auth", direct=True) + self.xmpp.disconnect() + + return True + + def _handle_success(self, stanza): + """SASL authentication succeeded. Restart the stream.""" + self.xmpp.authenticated = True + self.xmpp.features.append('mechanisms') + raise RestartStream() + + def _handle_fail(self, stanza): + """SASL authentication failed. Disconnect and shutdown.""" + log.info("Authentication failed.") + self.xmpp.event("failed_auth", direct=True) + self.xmpp.disconnect() + log.debug("Starting SASL Auth") + return True diff --git a/sleekxmpp/features/feature_mechanisms/stanza.py b/sleekxmpp/features/feature_mechanisms/stanza.py new file mode 100644 index 00000000..e55a72ad --- /dev/null +++ b/sleekxmpp/features/feature_mechanisms/stanza.py @@ -0,0 +1,104 @@ +""" + 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.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) + + +class Success(StanzaBase): + + """ + """ + + name = 'success' + namespace = 'urn:ietf:params:xml:ns:xmpp-sasl' + interfaces = set() + plugin_attrib = name + + +class Failure(StanzaBase): + + """ + """ + + name = 'failure' + namespace = 'urn:ietf:params:xml:ns:xmpp-sasl' + interfaces = set() + plugin_attrib = name + + +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 set_value(self, value): + self.xml.text = value + + def get_value(self): + return self.xml.text + + def del_value(self): + self.xml.text = '' + + +register_stanza_plugin(StreamFeatures, Mechanisms) diff --git a/sleekxmpp/features/feature_session.py b/sleekxmpp/features/feature_session.py deleted file mode 100644 index 5bae358c..00000000 --- a/sleekxmpp/features/feature_session.py +++ /dev/null @@ -1,46 +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 logging - -from sleekxmpp.xmlstream.matcher import * -from sleekxmpp.xmlstream.handler import * -from sleekxmpp.plugins.base import base_plugin - - -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.xmpp.register_feature('session', - self._handle_start_session, - restart=False, - order=10001) - - 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) - - 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/__init__.py b/sleekxmpp/features/feature_session/__init__.py new file mode 100644 index 00000000..1399f73b --- /dev/null +++ b/sleekxmpp/features/feature_session/__init__.py @@ -0,0 +1,10 @@ +""" + 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.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..4d17b2d2 --- /dev/null +++ b/sleekxmpp/features/feature_session/session.py @@ -0,0 +1,54 @@ +""" + 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 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) + + 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..2047a4f0 --- /dev/null +++ b/sleekxmpp/features/feature_session/stanza.py @@ -0,0 +1,21 @@ +""" + 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.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.py b/sleekxmpp/features/feature_starttls.py deleted file mode 100644 index 5367fa49..00000000 --- a/sleekxmpp/features/feature_starttls.py +++ /dev/null @@ -1,61 +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 logging - -from sleekxmpp.stanza.stream import tls -from sleekxmpp.xmlstream import RestartStream -from sleekxmpp.xmlstream.matcher import * -from sleekxmpp.xmlstream.handler import * -from sleekxmpp.plugins.base import base_plugin - - -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.xmpp.register_stanza(tls.Proceed) - self.xmpp.register_handler( - Callback('STARTTLS Proceed', - MatchXPath(tls.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)) - - def _handle_starttls(self, features): - """ - Handle notification that the server supports TLS. - - Arguments: - features -- The stream:features element. - """ - if 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.append('starttls') - raise RestartStream() diff --git a/sleekxmpp/features/feature_starttls/__init__.py b/sleekxmpp/features/feature_starttls/__init__.py new file mode 100644 index 00000000..042e37fa --- /dev/null +++ b/sleekxmpp/features/feature_starttls/__init__.py @@ -0,0 +1,10 @@ +""" + 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.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..5fdafabd --- /dev/null +++ b/sleekxmpp/features/feature_starttls/stanza.py @@ -0,0 +1,47 @@ +""" + 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.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..cbb94be0 --- /dev/null +++ b/sleekxmpp/features/feature_starttls/starttls.py @@ -0,0 +1,66 @@ +""" + 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 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 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.append('starttls') + raise RestartStream() diff --git a/sleekxmpp/stanza/__init__.py b/sleekxmpp/stanza/__init__.py index 05df8837..4bd37dc5 100644 --- a/sleekxmpp/stanza/__init__.py +++ b/sleekxmpp/stanza/__init__.py @@ -11,7 +11,5 @@ from sleekxmpp.stanza.error import Error from sleekxmpp.stanza.iq import Iq from sleekxmpp.stanza.message import Message from sleekxmpp.stanza.presence import Presence -from sleekxmpp.stanza.stream import StreamFeatures -from sleekxmpp.stanza.stream import Bind -from sleekxmpp.stanza.stream import Session -from sleekxmpp.stanza.stream import StreamError +from sleekxmpp.stanza.stream_features import StreamFeatures +from sleekxmpp.stanza.stream_error import StreamError diff --git a/sleekxmpp/stanza/stream/__init__.py b/sleekxmpp/stanza/stream/__init__.py index a386bbac..2cb79673 100644 --- a/sleekxmpp/stanza/stream/__init__.py +++ b/sleekxmpp/stanza/stream/__init__.py @@ -6,8 +6,3 @@ See the file LICENSE for copying permission. """ - -from sleekxmpp.stanza.stream.error import StreamError -from sleekxmpp.stanza.stream.features import StreamFeatures -from sleekxmpp.stanza.stream.bind import Bind -from sleekxmpp.stanza.stream.session import Session diff --git a/sleekxmpp/stanza/stream/bind.py b/sleekxmpp/stanza/stream/bind.py deleted file mode 100644 index 165afcb4..00000000 --- a/sleekxmpp/stanza/stream/bind.py +++ /dev/null @@ -1,27 +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 sleekxmpp.stanza import Iq -from sleekxmpp.stanza.stream import 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' - - -register_stanza_plugin(Iq, Bind) -register_stanza_plugin(StreamFeatures, Bind) diff --git a/sleekxmpp/stanza/stream/error.py b/sleekxmpp/stanza/stream/error.py deleted file mode 100644 index cf59a7fa..00000000 --- a/sleekxmpp/stanza/stream/error.py +++ /dev/null @@ -1,69 +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 sleekxmpp.stanza.error import Error -from sleekxmpp.xmlstream import StanzaBase, ElementBase, ET -from sleekxmpp.xmlstream import register_stanza_plugin - - -class StreamError(Error, StanzaBase): - - """ - XMPP stanzas of type 'error' should include an stanza that - describes the nature of the error and how it should be handled. - - Use the 'XEP-0086: Error Condition Mappings' plugin to include error - codes used in older XMPP versions. - - The stream:error stanza is used to provide more information for - error that occur with the underlying XML stream itself, and not - a particular stanza. - - Note: The StreamError stanza is mostly the same as the normal - Error stanza, but with different namespaces and - condition names. - - Example error stanza: - - - - XML was not well-formed. - - - - Stanza Interface: - condition -- The name of the condition element. - text -- Human readable description of the error. - - Attributes: - conditions -- The set of allowable error condition elements. - condition_ns -- The namespace for the condition element. - - Methods: - setup -- Overrides ElementBase.setup. - get_condition -- Retrieve the name of the condition element. - set_condition -- Add a condition element. - del_condition -- Remove the condition element. - get_text -- Retrieve the contents of the element. - set_text -- Set the contents of the element. - del_text -- Remove the element. - """ - - namespace = 'http://etherx.jabber.org/streams' - interfaces = set(('condition', 'text')) - conditions = set(( - 'bad-format', 'bad-namespace-prefix', 'conflict', - 'connection-timeout', 'host-gone', 'host-unknown', - 'improper-addressing', 'internal-server-error', 'invalid-from', - 'invalid-namespace', 'invalid-xml', 'not-authorized', - 'not-well-formed', 'policy-violation', 'remote-connection-failed', - 'reset', 'resource-constraint', 'restricted-xml', 'see-other-host', - 'system-shutdown', 'undefined-condition', 'unsupported-encoding', - 'unsupported-feature', 'unsupported-stanza-type', - 'unsupported-version')) - condition_ns = 'urn:ietf:params:xml:ns:xmpp-streams' diff --git a/sleekxmpp/stanza/stream/features.py b/sleekxmpp/stanza/stream/features.py deleted file mode 100644 index 5be2e55f..00000000 --- a/sleekxmpp/stanza/stream/features.py +++ /dev/null @@ -1,52 +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 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/stanza/stream/sasl.py b/sleekxmpp/stanza/stream/sasl.py deleted file mode 100644 index e55a72ad..00000000 --- a/sleekxmpp/stanza/stream/sasl.py +++ /dev/null @@ -1,104 +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 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) - - -class Success(StanzaBase): - - """ - """ - - name = 'success' - namespace = 'urn:ietf:params:xml:ns:xmpp-sasl' - interfaces = set() - plugin_attrib = name - - -class Failure(StanzaBase): - - """ - """ - - name = 'failure' - namespace = 'urn:ietf:params:xml:ns:xmpp-sasl' - interfaces = set() - plugin_attrib = name - - -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 set_value(self, value): - self.xml.text = value - - def get_value(self): - return self.xml.text - - def del_value(self): - self.xml.text = '' - - -register_stanza_plugin(StreamFeatures, Mechanisms) diff --git a/sleekxmpp/stanza/stream/session.py b/sleekxmpp/stanza/stream/session.py deleted file mode 100644 index 87f21857..00000000 --- a/sleekxmpp/stanza/stream/session.py +++ /dev/null @@ -1,26 +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 sleekxmpp.stanza import Iq -from sleekxmpp.stanza.stream import 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' - - -register_stanza_plugin(Iq, Session) -register_stanza_plugin(StreamFeatures, Session) diff --git a/sleekxmpp/stanza/stream/tls.py b/sleekxmpp/stanza/stream/tls.py deleted file mode 100644 index d85f9b49..00000000 --- a/sleekxmpp/stanza/stream/tls.py +++ /dev/null @@ -1,50 +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 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() - - -register_stanza_plugin(StreamFeatures, STARTTLS) diff --git a/sleekxmpp/stanza/stream_error.py b/sleekxmpp/stanza/stream_error.py new file mode 100644 index 00000000..cf59a7fa --- /dev/null +++ b/sleekxmpp/stanza/stream_error.py @@ -0,0 +1,69 @@ +""" + 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.stanza.error import Error +from sleekxmpp.xmlstream import StanzaBase, ElementBase, ET +from sleekxmpp.xmlstream import register_stanza_plugin + + +class StreamError(Error, StanzaBase): + + """ + XMPP stanzas of type 'error' should include an stanza that + describes the nature of the error and how it should be handled. + + Use the 'XEP-0086: Error Condition Mappings' plugin to include error + codes used in older XMPP versions. + + The stream:error stanza is used to provide more information for + error that occur with the underlying XML stream itself, and not + a particular stanza. + + Note: The StreamError stanza is mostly the same as the normal + Error stanza, but with different namespaces and + condition names. + + Example error stanza: + + + + XML was not well-formed. + + + + Stanza Interface: + condition -- The name of the condition element. + text -- Human readable description of the error. + + Attributes: + conditions -- The set of allowable error condition elements. + condition_ns -- The namespace for the condition element. + + Methods: + setup -- Overrides ElementBase.setup. + get_condition -- Retrieve the name of the condition element. + set_condition -- Add a condition element. + del_condition -- Remove the condition element. + get_text -- Retrieve the contents of the element. + set_text -- Set the contents of the element. + del_text -- Remove the element. + """ + + namespace = 'http://etherx.jabber.org/streams' + interfaces = set(('condition', 'text')) + conditions = set(( + 'bad-format', 'bad-namespace-prefix', 'conflict', + 'connection-timeout', 'host-gone', 'host-unknown', + 'improper-addressing', 'internal-server-error', 'invalid-from', + 'invalid-namespace', 'invalid-xml', 'not-authorized', + 'not-well-formed', 'policy-violation', 'remote-connection-failed', + 'reset', 'resource-constraint', 'restricted-xml', 'see-other-host', + 'system-shutdown', 'undefined-condition', 'unsupported-encoding', + 'unsupported-feature', 'unsupported-stanza-type', + 'unsupported-version')) + condition_ns = 'urn:ietf:params:xml:ns:xmpp-streams' 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']] -- cgit v1.2.3 From 2a2ac73845ffc8695e2bc55746f45e1a18d55e6c Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Fri, 1 Jul 2011 15:15:13 -0700 Subject: So using sys.excepthook to catch errors only works once. The error bubbles through the event processing loop, breaking it and hanging the application. Instead, there is now a .exception(e) method on XMLStream which may be overridden or reassigned that will receive all unhandled exceptions (read: not XMPPError) from event and stream handlers. --- sleekxmpp/stanza/rootstanza.py | 5 +- sleekxmpp/xmlstream/xmlstream.py | 29 +++--------- tests/test_stream_exceptions.py | 99 ++++++++++++++++++++++++++++++++++++++-- tests/test_stream_xep_0030.py | 14 +++--- tests/test_stream_xep_0128.py | 1 - tests/test_stream_xep_0249.py | 1 - 6 files changed, 111 insertions(+), 38 deletions(-) diff --git a/sleekxmpp/stanza/rootstanza.py b/sleekxmpp/stanza/rootstanza.py index bc11476e..9e1d1cfa 100644 --- a/sleekxmpp/stanza/rootstanza.py +++ b/sleekxmpp/stanza/rootstanza.py @@ -64,8 +64,7 @@ class RootStanza(StanzaBase): # log the error log.exception('Error handling {%s}%s stanza' % (self.namespace, self.name)) - # Finally raise the exception, so it can be handled (or not) - # at a higher level by using sys.excepthook. - raise e + # Finally raise the exception to a global exception handler + self.stream.exception(e) register_stanza_plugin(RootStanza, Error) diff --git a/sleekxmpp/xmlstream/xmlstream.py b/sleekxmpp/xmlstream/xmlstream.py index 6282c8d0..c7d0d3a8 100644 --- a/sleekxmpp/xmlstream/xmlstream.py +++ b/sleekxmpp/xmlstream/xmlstream.py @@ -764,7 +764,6 @@ class XMLStream(object): Event handlers and the send queue will be threaded regardless of this parameter's value. """ - self._thread_excepthook() self.scheduler.process(threaded=True) def start_thread(name, target): @@ -1052,30 +1051,16 @@ class XMLStream(object): self.event_queue.put(('quit', None, None)) return - def _thread_excepthook(self): + def exception(self, exception): """ - If a threaded event handler raises an exception, there is no way to - catch it except with an excepthook. Currently, each thread has its own - excepthook, but ideally we could use the main sys.excepthook. + Process an unknown exception. - Modifies threading.Thread to use sys.excepthook when an exception - is not caught. - """ - init_old = threading.Thread.__init__ - - def init(self, *args, **kwargs): - init_old(self, *args, **kwargs) - run_old = self.run + Meant to be overridden. - def run_with_except_hook(*args, **kw): - try: - run_old(*args, **kw) - except (KeyboardInterrupt, SystemExit): - raise - except: - sys.excepthook(*sys.exc_info()) - self.run = run_with_except_hook - threading.Thread.__init__ = init + Arguments: + exception -- An unhandled exception object. + """ + pass # To comply with PEP8, method names now use underscores. diff --git a/tests/test_stream_exceptions.py b/tests/test_stream_exceptions.py index 1143ce28..c41edbb2 100644 --- a/tests/test_stream_exceptions.py +++ b/tests/test_stream_exceptions.py @@ -12,7 +12,6 @@ class TestStreamExceptions(SleekTest): """ def tearDown(self): - sys.excepthook = sys.__excepthook__ self.stream_close() def testExceptionReply(self): @@ -23,8 +22,33 @@ class TestStreamExceptions(SleekTest): msg['body'] = 'Body changed' raise XMPPError(clear=False) + self.stream_start() + self.xmpp.add_event_handler('message', message) + + self.recv(""" + + This is going to cause an error. + + """) + + self.send(""" + + This is going to cause an error. + + + + + """) + + def testExceptionContinueWorking(self): + """Test that Sleek continues to respond after an XMPPError is raised.""" + + def message(msg): + msg.reply() + msg['body'] = 'Body changed' + raise XMPPError(clear=False) - sys.excepthook = lambda *args, **kwargs: None self.stream_start() self.xmpp.add_event_handler('message', message) @@ -44,6 +68,22 @@ class TestStreamExceptions(SleekTest): """) + self.recv(""" + + This is going to cause an error. + + """) + + self.send(""" + + This is going to cause an error. + + + + + """) + def testXMPPErrorException(self): """Test raising an XMPPError exception.""" @@ -153,9 +193,8 @@ class TestStreamExceptions(SleekTest): def catch_error(*args, **kwargs): raised_errors.append(True) - sys.excepthook = catch_error - self.stream_start() + self.xmpp.exception = catch_error self.xmpp.add_event_handler('message', message) self.recv(""" @@ -178,6 +217,58 @@ class TestStreamExceptions(SleekTest): self.assertEqual(raised_errors, [True], "Exception was not raised: %s" % raised_errors) + def testUnknownException(self): + """Test Sleek continues to respond after an unknown exception.""" + + raised_errors = [] + + def message(msg): + raise ValueError("Did something wrong") + + def catch_error(*args, **kwargs): + raised_errors.append(True) + + self.stream_start() + self.xmpp.exception = catch_error + self.xmpp.add_event_handler('message', message) + + self.recv(""" + + This is going to cause an error. + + """) + + self.send(""" + + + + + SleekXMPP got into trouble. + + + + """) + + self.recv(""" + + This is going to cause an error. + + """) + + self.send(""" + + + + + SleekXMPP got into trouble. + + + + """) + + self.assertEqual(raised_errors, [True, True], "Exceptions were not raised: %s" % raised_errors) suite = unittest.TestLoader().loadTestsFromTestCase(TestStreamExceptions) diff --git a/tests/test_stream_xep_0030.py b/tests/test_stream_xep_0030.py index c960fc7a..1666d3a1 100644 --- a/tests/test_stream_xep_0030.py +++ b/tests/test_stream_xep_0030.py @@ -12,7 +12,6 @@ class TestStreamDisco(SleekTest): """ def tearDown(self): - sys.excepthook = sys.__excepthook__ self.stream_close() def testInfoEmptyDefaultNode(self): @@ -531,11 +530,6 @@ class TestStreamDisco(SleekTest): raised_exceptions = [] - def catch_exception(*args, **kwargs): - raised_exceptions.append(True) - - sys.excepthook = catch_exception - self.stream_start(mode='client', plugins=['xep_0030', 'xep_0059']) @@ -544,8 +538,14 @@ class TestStreamDisco(SleekTest): iterator=True) results.amount = 10 + def run_test(): + try: + results.next() + except StopIteration: + raised_exceptions.append(True) + t = threading.Thread(name="get_items_iterator", - target=results.next) + target=run_test) t.start() self.send(""" diff --git a/tests/test_stream_xep_0128.py b/tests/test_stream_xep_0128.py index 6fee6556..42fc9143 100644 --- a/tests/test_stream_xep_0128.py +++ b/tests/test_stream_xep_0128.py @@ -13,7 +13,6 @@ class TestStreamExtendedDisco(SleekTest): """ def tearDown(self): - sys.excepthook = sys.__excepthook__ self.stream_close() def testUsingExtendedInfo(self): diff --git a/tests/test_stream_xep_0249.py b/tests/test_stream_xep_0249.py index f49d1f7e..9a25253f 100644 --- a/tests/test_stream_xep_0249.py +++ b/tests/test_stream_xep_0249.py @@ -13,7 +13,6 @@ class TestStreamDirectInvite(SleekTest): """ def tearDown(self): - sys.excepthook = sys.__excepthook__ self.stream_close() def testReceiveInvite(self): -- cgit v1.2.3 From b0297af38d6dcd9ebfdaa0131ea798c9fe2b8c63 Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Sat, 2 Jul 2011 21:43:02 -0700 Subject: Finish cleaning up stream feature organization. Fixed missing references that weren't caught due to leftover pyc file allowing tests to keep working when they shouldn't have. --- sleekxmpp/clientxmpp.py | 1 - sleekxmpp/features/feature_mechanisms/mechanisms.py | 13 +++++++------ sleekxmpp/features/sasl_anonymous.py | 3 +-- sleekxmpp/features/sasl_plain.py | 3 +-- 4 files changed, 9 insertions(+), 11 deletions(-) diff --git a/sleekxmpp/clientxmpp.py b/sleekxmpp/clientxmpp.py index 7245053f..17a7582f 100644 --- a/sleekxmpp/clientxmpp.py +++ b/sleekxmpp/clientxmpp.py @@ -21,7 +21,6 @@ from sleekxmpp import stanza from sleekxmpp import features from sleekxmpp.basexmpp import BaseXMPP from sleekxmpp.stanza import * -from sleekxmpp.stanza.stream import tls, sasl from sleekxmpp.xmlstream import XMLStream, RestartStream from sleekxmpp.xmlstream import StanzaBase, ET, register_stanza_plugin from sleekxmpp.xmlstream.matcher import * diff --git a/sleekxmpp/features/feature_mechanisms/mechanisms.py b/sleekxmpp/features/feature_mechanisms/mechanisms.py index 994c9bed..3cdb1b0a 100644 --- a/sleekxmpp/features/feature_mechanisms/mechanisms.py +++ b/sleekxmpp/features/feature_mechanisms/mechanisms.py @@ -8,11 +8,11 @@ import logging -from sleekxmpp.stanza import stream from sleekxmpp.xmlstream import RestartStream 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__) @@ -24,23 +24,24 @@ class feature_mechanisms(base_plugin): self.name = 'SASL Mechanisms' self.rfc = '6120' self.description = "SASL Stream Feature" + self.stanza = stanza - self.xmpp.register_stanza(stream.sasl.Success) - self.xmpp.register_stanza(stream.sasl.Failure) - self.xmpp.register_stanza(stream.sasl.Auth) + self.xmpp.register_stanza(stanza.Success) + self.xmpp.register_stanza(stanza.Failure) + self.xmpp.register_stanza(stanza.Auth) self._mechanism_handlers = {} self._mechanism_priorities = [] self.xmpp.register_handler( Callback('SASL Success', - MatchXPath(stream.sasl.Success.tag_name()), + MatchXPath(stanza.Success.tag_name()), self._handle_success, instream=True, once=True)) self.xmpp.register_handler( Callback('SASL Failure', - MatchXPath(stream.sasl.Failure.tag_name()), + MatchXPath(stanza.Failure.tag_name()), self._handle_fail, instream=True, once=True)) diff --git a/sleekxmpp/features/sasl_anonymous.py b/sleekxmpp/features/sasl_anonymous.py index 469d9d19..71a4b2e5 100644 --- a/sleekxmpp/features/sasl_anonymous.py +++ b/sleekxmpp/features/sasl_anonymous.py @@ -2,7 +2,6 @@ import base64 import sys import logging -from sleekxmpp.stanza.stream import sasl from sleekxmpp.plugins.base import base_plugin @@ -24,7 +23,7 @@ class sasl_anonymous(base_plugin): if self.xmpp.boundjid.user: return False - resp = sasl.Auth(self.xmpp) + resp = self.xmpp['feature_sasl'].stanza.Auth(self.xmpp) resp['mechanism'] = 'ANONYMOUS' resp.send(now=True) diff --git a/sleekxmpp/features/sasl_plain.py b/sleekxmpp/features/sasl_plain.py index 36c7d9df..270d28fe 100644 --- a/sleekxmpp/features/sasl_plain.py +++ b/sleekxmpp/features/sasl_plain.py @@ -2,7 +2,6 @@ import base64 import sys import logging -from sleekxmpp.stanza.stream import sasl from sleekxmpp.plugins.base import base_plugin @@ -34,7 +33,7 @@ class sasl_plain(base_plugin): auth = base64.b64encode(b'\x00' + user + \ b'\x00' + password).decode('utf-8') - resp = sasl.Auth(self.xmpp) + resp = self.xmpp['feature_mechanisms'].stanza.Auth(self.xmpp) resp['mechanism'] = 'PLAIN' resp['value'] = auth resp.send(now=True) -- cgit v1.2.3 From fba235a801a3a1c06d1769cdc944b72dce33f88a Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Sat, 2 Jul 2011 21:57:50 -0700 Subject: Simplify SASL mech registration. Moved SASL registration completely to the feature plugin, instead of keeping a portion of it in ClientXMPP. --- sleekxmpp/clientxmpp.py | 26 ---------------------- .../features/feature_mechanisms/mechanisms.py | 4 ++-- sleekxmpp/features/sasl_anonymous.py | 5 +++-- sleekxmpp/features/sasl_plain.py | 5 +++-- sleekxmpp/stanza/stream/__init__.py | 8 ------- 5 files changed, 8 insertions(+), 40 deletions(-) delete mode 100644 sleekxmpp/stanza/stream/__init__.py diff --git a/sleekxmpp/clientxmpp.py b/sleekxmpp/clientxmpp.py index 17a7582f..5b36e845 100644 --- a/sleekxmpp/clientxmpp.py +++ b/sleekxmpp/clientxmpp.py @@ -197,32 +197,6 @@ class ClientXMPP(BaseXMPP): self._stream_feature_order.append((order, name)) self._stream_feature_order.sort() - def register_sasl_mechanism(self, name, handler, priority=0): - """ - Register a handler for a SASL authentication mechanism. - - Arguments: - name -- The name of the mechanism (all caps) - handler -- The function that will perform the - authentication. The function must - return True if it is able to carry - out the authentication, False if - a required condition is not met. - priority -- An integer value indicating the - preferred ordering for the mechanism. - High values will be attempted first. - """ - self['feature_mechanisms'].register_mechanism(name, handler, priority) - - def remove_sasl_mechanism(self, name): - """ - Remove support for a given SASL authentication mechanism. - - Arguments: - name -- The name of the mechanism to remove (all caps) - """ - self['feature_mechanisms'].remove_mechanism(name) - def update_roster(self, jid, name=None, subscription=None, groups=[], block=True, timeout=None, callback=None): """ diff --git a/sleekxmpp/features/feature_mechanisms/mechanisms.py b/sleekxmpp/features/feature_mechanisms/mechanisms.py index 3cdb1b0a..a8a046e4 100644 --- a/sleekxmpp/features/feature_mechanisms/mechanisms.py +++ b/sleekxmpp/features/feature_mechanisms/mechanisms.py @@ -51,7 +51,7 @@ class feature_mechanisms(base_plugin): restart=True, order=self.config.get('order', 100)) - def register_mechanism(self, name, handler, priority=0): + def register(self, name, handler, priority=0): """ Register a handler for a SASL authentication mechanism. @@ -70,7 +70,7 @@ class feature_mechanisms(base_plugin): self._mechanism_priorities.append((priority, name)) self._mechanism_priorities.sort(reverse=True) - def remove_mechanism(self, name): + def remove(self, name): """ Remove support for a given SASL authentication mechanism. diff --git a/sleekxmpp/features/sasl_anonymous.py b/sleekxmpp/features/sasl_anonymous.py index 71a4b2e5..98a0d36e 100644 --- a/sleekxmpp/features/sasl_anonymous.py +++ b/sleekxmpp/features/sasl_anonymous.py @@ -14,8 +14,9 @@ class sasl_anonymous(base_plugin): self.name = 'SASL ANONYMOUS' self.rfc = '6120' self.description = 'SASL ANONYMOUS Mechanism' + self.stanza = self.xmpp['feature_mechanisms'].stanza - self.xmpp.register_sasl_mechanism('ANONYMOUS', + self.xmpp['feature_mechanisms'].register('ANONYMOUS', self._handle_anonymous, priority=self.config.get('priority', 0)) @@ -23,7 +24,7 @@ class sasl_anonymous(base_plugin): if self.xmpp.boundjid.user: return False - resp = self.xmpp['feature_sasl'].stanza.Auth(self.xmpp) + resp = self.stanza.Auth(self.xmpp) resp['mechanism'] = 'ANONYMOUS' resp.send(now=True) diff --git a/sleekxmpp/features/sasl_plain.py b/sleekxmpp/features/sasl_plain.py index 270d28fe..427660ab 100644 --- a/sleekxmpp/features/sasl_plain.py +++ b/sleekxmpp/features/sasl_plain.py @@ -14,8 +14,9 @@ class sasl_plain(base_plugin): self.name = 'SASL PLAIN' self.rfc = '6120' self.description = 'SASL PLAIN Mechanism' + self.stanza = self.xmpp['feature_mechanisms'].stanza - self.xmpp.register_sasl_mechanism('PLAIN', + self.xmpp['feature_mechanisms'].register('PLAIN', self._handle_plain, priority=self.config.get('priority', 1)) @@ -33,7 +34,7 @@ class sasl_plain(base_plugin): auth = base64.b64encode(b'\x00' + user + \ b'\x00' + password).decode('utf-8') - resp = self.xmpp['feature_mechanisms'].stanza.Auth(self.xmpp) + resp = self.stanza.Auth(self.xmpp) resp['mechanism'] = 'PLAIN' resp['value'] = auth resp.send(now=True) diff --git a/sleekxmpp/stanza/stream/__init__.py b/sleekxmpp/stanza/stream/__init__.py deleted file mode 100644 index 2cb79673..00000000 --- a/sleekxmpp/stanza/stream/__init__.py +++ /dev/null @@ -1,8 +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. -""" - -- cgit v1.2.3 From b898b14b77d739cb1c118c9e3648aa268348d293 Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Sat, 2 Jul 2011 22:30:34 -0700 Subject: Use a set to track negotiated features. Added guards to prevent renegotiating STARTTLS or SASL in cases where servers don't behave properly. --- sleekxmpp/clientxmpp.py | 4 ++-- sleekxmpp/features/feature_bind/bind.py | 2 ++ sleekxmpp/features/feature_mechanisms/mechanisms.py | 7 ++++++- sleekxmpp/features/feature_session/session.py | 2 ++ sleekxmpp/features/feature_starttls/starttls.py | 8 ++++++-- 5 files changed, 18 insertions(+), 5 deletions(-) diff --git a/sleekxmpp/clientxmpp.py b/sleekxmpp/clientxmpp.py index 5b36e845..5eb9c90a 100644 --- a/sleekxmpp/clientxmpp.py +++ b/sleekxmpp/clientxmpp.py @@ -83,7 +83,7 @@ class ClientXMPP(BaseXMPP): "xmlns='%s'" % self.default_ns) self.stream_footer = "" - self.features = [] + self.features = set() self._stream_feature_handlers = {} self._stream_feature_order = [] @@ -273,7 +273,7 @@ class ClientXMPP(BaseXMPP): self.sessionstarted = False self.bound = False self.bindfail = False - self.features = [] + self.features = set() def session_timeout(): if not self.session_started_event.isSet(): diff --git a/sleekxmpp/features/feature_bind/bind.py b/sleekxmpp/features/feature_bind/bind.py index e177d7b2..c5d9395f 100644 --- a/sleekxmpp/features/feature_bind/bind.py +++ b/sleekxmpp/features/feature_bind/bind.py @@ -53,6 +53,8 @@ class feature_bind(base_plugin): self.xmpp.set_jid(response['bind']['jid']) self.xmpp.bound = True + self.features.add('bind') + log.info("Node set to: %s" % self.xmpp.boundjid.full) if 'session' not in features['features']: diff --git a/sleekxmpp/features/feature_mechanisms/mechanisms.py b/sleekxmpp/features/feature_mechanisms/mechanisms.py index a8a046e4..011010fb 100644 --- a/sleekxmpp/features/feature_mechanisms/mechanisms.py +++ b/sleekxmpp/features/feature_mechanisms/mechanisms.py @@ -90,6 +90,11 @@ class feature_mechanisms(base_plugin): 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 + for priority, mech in self._mechanism_priorities: if mech in features['mechanisms']: log.debug('Attempt to use SASL %s' % mech) @@ -105,7 +110,7 @@ class feature_mechanisms(base_plugin): def _handle_success(self, stanza): """SASL authentication succeeded. Restart the stream.""" self.xmpp.authenticated = True - self.xmpp.features.append('mechanisms') + self.xmpp.features.add('mechanisms') raise RestartStream() def _handle_fail(self, stanza): diff --git a/sleekxmpp/features/feature_session/session.py b/sleekxmpp/features/feature_session/session.py index 4d17b2d2..9c5e0448 100644 --- a/sleekxmpp/features/feature_session/session.py +++ b/sleekxmpp/features/feature_session/session.py @@ -48,6 +48,8 @@ class feature_session(base_plugin): 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() diff --git a/sleekxmpp/features/feature_starttls/starttls.py b/sleekxmpp/features/feature_starttls/starttls.py index cbb94be0..841e7a8d 100644 --- a/sleekxmpp/features/feature_starttls/starttls.py +++ b/sleekxmpp/features/feature_starttls/starttls.py @@ -48,7 +48,11 @@ class feature_starttls(base_plugin): Arguments: features -- The stream:features element. """ - if not self.xmpp.use_tls: + 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) @@ -62,5 +66,5 @@ class feature_starttls(base_plugin): """Restart the XML stream when TLS is accepted.""" log.debug("Starting TLS") if self.xmpp.start_tls(): - self.xmpp.features.append('starttls') + self.xmpp.features.add('starttls') raise RestartStream() -- cgit v1.2.3 From 219df582dab2a5dd3c9e2bbfef27d3cfa814841d Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Sat, 2 Jul 2011 22:49:34 -0700 Subject: It isn't 2010 anymore. I keep forgetting to update the copyright on new code. --- sleekxmpp/features/__init__.py | 2 +- sleekxmpp/features/feature_bind/__init__.py | 2 +- sleekxmpp/features/feature_bind/bind.py | 2 +- sleekxmpp/features/feature_bind/stanza.py | 2 +- sleekxmpp/features/feature_mechanisms/__init__.py | 2 +- sleekxmpp/features/feature_mechanisms/mechanisms.py | 2 +- sleekxmpp/features/feature_session/__init__.py | 2 +- sleekxmpp/features/feature_session/session.py | 2 +- sleekxmpp/features/feature_session/stanza.py | 2 +- sleekxmpp/features/feature_starttls/__init__.py | 2 +- sleekxmpp/features/feature_starttls/stanza.py | 2 +- sleekxmpp/features/feature_starttls/starttls.py | 2 +- 12 files changed, 12 insertions(+), 12 deletions(-) diff --git a/sleekxmpp/features/__init__.py b/sleekxmpp/features/__init__.py index 65d2bdbf..5c86cfea 100644 --- a/sleekxmpp/features/__init__.py +++ b/sleekxmpp/features/__init__.py @@ -1,6 +1,6 @@ """ SleekXMPP: The Sleek XMPP Library - Copyright (C) 2010 Nathanael C. Fritz + Copyright (C) 2011 Nathanael C. Fritz This file is part of SleekXMPP. See the file LICENSE for copying permission. diff --git a/sleekxmpp/features/feature_bind/__init__.py b/sleekxmpp/features/feature_bind/__init__.py index fce94dd6..aa854f87 100644 --- a/sleekxmpp/features/feature_bind/__init__.py +++ b/sleekxmpp/features/feature_bind/__init__.py @@ -1,6 +1,6 @@ """ SleekXMPP: The Sleek XMPP Library - Copyright (C) 2010 Nathanael C. Fritz + Copyright (C) 2011 Nathanael C. Fritz This file is part of SleekXMPP. See the file LICENSE for copying permission. diff --git a/sleekxmpp/features/feature_bind/bind.py b/sleekxmpp/features/feature_bind/bind.py index c5d9395f..0b0f2033 100644 --- a/sleekxmpp/features/feature_bind/bind.py +++ b/sleekxmpp/features/feature_bind/bind.py @@ -1,6 +1,6 @@ """ SleekXMPP: The Sleek XMPP Library - Copyright (C) 2010 Nathanael C. Fritz + Copyright (C) 2011 Nathanael C. Fritz This file is part of SleekXMPP. See the file LICENSE for copying permission. diff --git a/sleekxmpp/features/feature_bind/stanza.py b/sleekxmpp/features/feature_bind/stanza.py index f3e025fa..2c1484e0 100644 --- a/sleekxmpp/features/feature_bind/stanza.py +++ b/sleekxmpp/features/feature_bind/stanza.py @@ -1,6 +1,6 @@ """ SleekXMPP: The Sleek XMPP Library - Copyright (C) 2010 Nathanael C. Fritz + Copyright (C) 2011 Nathanael C. Fritz This file is part of SleekXMPP. See the file LICENSE for copying permission. diff --git a/sleekxmpp/features/feature_mechanisms/__init__.py b/sleekxmpp/features/feature_mechanisms/__init__.py index a93b2b6f..b0b9dcc1 100644 --- a/sleekxmpp/features/feature_mechanisms/__init__.py +++ b/sleekxmpp/features/feature_mechanisms/__init__.py @@ -1,6 +1,6 @@ """ SleekXMPP: The Sleek XMPP Library - Copyright (C) 2010 Nathanael C. Fritz + Copyright (C) 2011 Nathanael C. Fritz This file is part of SleekXMPP. See the file LICENSE for copying permission. diff --git a/sleekxmpp/features/feature_mechanisms/mechanisms.py b/sleekxmpp/features/feature_mechanisms/mechanisms.py index 011010fb..210267fa 100644 --- a/sleekxmpp/features/feature_mechanisms/mechanisms.py +++ b/sleekxmpp/features/feature_mechanisms/mechanisms.py @@ -1,6 +1,6 @@ """ SleekXMPP: The Sleek XMPP Library - Copyright (C) 2010 Nathanael C. Fritz + Copyright (C) 2011 Nathanael C. Fritz This file is part of SleekXMPP. See the file LICENSE for copying permission. diff --git a/sleekxmpp/features/feature_session/__init__.py b/sleekxmpp/features/feature_session/__init__.py index 1399f73b..3c84baed 100644 --- a/sleekxmpp/features/feature_session/__init__.py +++ b/sleekxmpp/features/feature_session/__init__.py @@ -1,6 +1,6 @@ """ SleekXMPP: The Sleek XMPP Library - Copyright (C) 2010 Nathanael C. Fritz + Copyright (C) 2011 Nathanael C. Fritz This file is part of SleekXMPP. See the file LICENSE for copying permission. diff --git a/sleekxmpp/features/feature_session/session.py b/sleekxmpp/features/feature_session/session.py index 9c5e0448..0daec5da 100644 --- a/sleekxmpp/features/feature_session/session.py +++ b/sleekxmpp/features/feature_session/session.py @@ -1,6 +1,6 @@ """ SleekXMPP: The Sleek XMPP Library - Copyright (C) 2010 Nathanael C. Fritz + Copyright (C) 2011 Nathanael C. Fritz This file is part of SleekXMPP. See the file LICENSE for copying permission. diff --git a/sleekxmpp/features/feature_session/stanza.py b/sleekxmpp/features/feature_session/stanza.py index 2047a4f0..40ea583d 100644 --- a/sleekxmpp/features/feature_session/stanza.py +++ b/sleekxmpp/features/feature_session/stanza.py @@ -1,6 +1,6 @@ """ SleekXMPP: The Sleek XMPP Library - Copyright (C) 2010 Nathanael C. Fritz + Copyright (C) 2011 Nathanael C. Fritz This file is part of SleekXMPP. See the file LICENSE for copying permission. diff --git a/sleekxmpp/features/feature_starttls/__init__.py b/sleekxmpp/features/feature_starttls/__init__.py index 042e37fa..4ae89433 100644 --- a/sleekxmpp/features/feature_starttls/__init__.py +++ b/sleekxmpp/features/feature_starttls/__init__.py @@ -1,6 +1,6 @@ """ SleekXMPP: The Sleek XMPP Library - Copyright (C) 2010 Nathanael C. Fritz + Copyright (C) 2011 Nathanael C. Fritz This file is part of SleekXMPP. See the file LICENSE for copying permission. diff --git a/sleekxmpp/features/feature_starttls/stanza.py b/sleekxmpp/features/feature_starttls/stanza.py index 5fdafabd..8b09ad94 100644 --- a/sleekxmpp/features/feature_starttls/stanza.py +++ b/sleekxmpp/features/feature_starttls/stanza.py @@ -1,6 +1,6 @@ """ SleekXMPP: The Sleek XMPP Library - Copyright (C) 2010 Nathanael C. Fritz + Copyright (C) 2011 Nathanael C. Fritz This file is part of SleekXMPP. See the file LICENSE for copying permission. diff --git a/sleekxmpp/features/feature_starttls/starttls.py b/sleekxmpp/features/feature_starttls/starttls.py index 841e7a8d..639788a0 100644 --- a/sleekxmpp/features/feature_starttls/starttls.py +++ b/sleekxmpp/features/feature_starttls/starttls.py @@ -1,6 +1,6 @@ """ SleekXMPP: The Sleek XMPP Library - Copyright (C) 2010 Nathanael C. Fritz + Copyright (C) 2011 Nathanael C. Fritz This file is part of SleekXMPP. See the file LICENSE for copying permission. -- cgit v1.2.3 From 540d7496954c38e5483205410662120ec9ccd8c8 Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Sat, 2 Jul 2011 22:50:31 -0700 Subject: Fix ordering bug when retrieving an error condition. --- sleekxmpp/stanza/error.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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): -- cgit v1.2.3 From 0224d028e76ba608400fe55602fdb84f8e70f13b Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Sat, 2 Jul 2011 23:09:29 -0700 Subject: SASL failure event now includes the failure stanza. Broke SASL stanzas into separate files. Fixed typo in feature_bind. --- sleekxmpp/features/feature_bind/bind.py | 2 +- sleekxmpp/features/feature_mechanisms/__init__.py | 5 +- .../features/feature_mechanisms/mechanisms.py | 9 +- sleekxmpp/features/feature_mechanisms/stanza.py | 104 --------------------- .../features/feature_mechanisms/stanza/__init__.py | 14 +++ .../features/feature_mechanisms/stanza/auth.py | 35 +++++++ .../features/feature_mechanisms/stanza/failure.py | 76 +++++++++++++++ .../feature_mechanisms/stanza/mechanisms.py | 55 +++++++++++ .../features/feature_mechanisms/stanza/success.py | 22 +++++ 9 files changed, 212 insertions(+), 110 deletions(-) delete mode 100644 sleekxmpp/features/feature_mechanisms/stanza.py create mode 100644 sleekxmpp/features/feature_mechanisms/stanza/__init__.py create mode 100644 sleekxmpp/features/feature_mechanisms/stanza/auth.py create mode 100644 sleekxmpp/features/feature_mechanisms/stanza/failure.py create mode 100644 sleekxmpp/features/feature_mechanisms/stanza/mechanisms.py create mode 100644 sleekxmpp/features/feature_mechanisms/stanza/success.py diff --git a/sleekxmpp/features/feature_bind/bind.py b/sleekxmpp/features/feature_bind/bind.py index 0b0f2033..de03192c 100644 --- a/sleekxmpp/features/feature_bind/bind.py +++ b/sleekxmpp/features/feature_bind/bind.py @@ -53,7 +53,7 @@ class feature_bind(base_plugin): self.xmpp.set_jid(response['bind']['jid']) self.xmpp.bound = True - self.features.add('bind') + self.xmpp.features.add('bind') log.info("Node set to: %s" % self.xmpp.boundjid.full) diff --git a/sleekxmpp/features/feature_mechanisms/__init__.py b/sleekxmpp/features/feature_mechanisms/__init__.py index b0b9dcc1..5379ef4e 100644 --- a/sleekxmpp/features/feature_mechanisms/__init__.py +++ b/sleekxmpp/features/feature_mechanisms/__init__.py @@ -7,4 +7,7 @@ """ from sleekxmpp.features.feature_mechanisms.mechanisms import feature_mechanisms -from sleekxmpp.features.feature_mechanisms.stanza import * +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 index 210267fa..7a877793 100644 --- a/sleekxmpp/features/feature_mechanisms/mechanisms.py +++ b/sleekxmpp/features/feature_mechanisms/mechanisms.py @@ -8,7 +8,8 @@ import logging -from sleekxmpp.xmlstream import RestartStream +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 @@ -26,6 +27,7 @@ class feature_mechanisms(base_plugin): self.description = "SASL Stream Feature" self.stanza = stanza + register_stanza_plugin(StreamFeatures, stanza.Mechanisms) self.xmpp.register_stanza(stanza.Success) self.xmpp.register_stanza(stanza.Failure) self.xmpp.register_stanza(stanza.Auth) @@ -115,8 +117,7 @@ class feature_mechanisms(base_plugin): def _handle_fail(self, stanza): """SASL authentication failed. Disconnect and shutdown.""" - log.info("Authentication failed.") - self.xmpp.event("failed_auth", direct=True) + log.info("Authentication failed: %s" % stanza['condition']) + self.xmpp.event("failed_auth", stanza, direct=True) self.xmpp.disconnect() - log.debug("Starting SASL Auth") return True diff --git a/sleekxmpp/features/feature_mechanisms/stanza.py b/sleekxmpp/features/feature_mechanisms/stanza.py deleted file mode 100644 index e55a72ad..00000000 --- a/sleekxmpp/features/feature_mechanisms/stanza.py +++ /dev/null @@ -1,104 +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 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) - - -class Success(StanzaBase): - - """ - """ - - name = 'success' - namespace = 'urn:ietf:params:xml:ns:xmpp-sasl' - interfaces = set() - plugin_attrib = name - - -class Failure(StanzaBase): - - """ - """ - - name = 'failure' - namespace = 'urn:ietf:params:xml:ns:xmpp-sasl' - interfaces = set() - plugin_attrib = name - - -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 set_value(self, value): - self.xml.text = value - - def get_value(self): - return self.xml.text - - def del_value(self): - self.xml.text = '' - - -register_stanza_plugin(StreamFeatures, Mechanisms) diff --git a/sleekxmpp/features/feature_mechanisms/stanza/__init__.py b/sleekxmpp/features/feature_mechanisms/stanza/__init__.py new file mode 100644 index 00000000..0d9135d3 --- /dev/null +++ b/sleekxmpp/features/feature_mechanisms/stanza/__init__.py @@ -0,0 +1,14 @@ +""" + 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 + diff --git a/sleekxmpp/features/feature_mechanisms/stanza/auth.py b/sleekxmpp/features/feature_mechanisms/stanza/auth.py new file mode 100644 index 00000000..12208841 --- /dev/null +++ b/sleekxmpp/features/feature_mechanisms/stanza/auth.py @@ -0,0 +1,35 @@ +""" + 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 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 set_value(self, value): + self.xml.text = value + + def get_value(self): + return self.xml.text + + 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..98a1ab80 --- /dev/null +++ b/sleekxmpp/features/feature_mechanisms/stanza/failure.py @@ -0,0 +1,76 @@ +""" + 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' + + 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/success.py b/sleekxmpp/features/feature_mechanisms/stanza/success.py new file mode 100644 index 00000000..2c40f56c --- /dev/null +++ b/sleekxmpp/features/feature_mechanisms/stanza/success.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 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 -- cgit v1.2.3 From 086bf89d699c88ab89ad1e1975d6022335ca5c04 Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Sun, 3 Jul 2011 00:35:36 -0700 Subject: Added XEP-0066: Out-of-Band Data --- sleekxmpp/plugins/xep_0066/__init__.py | 11 +++++ sleekxmpp/plugins/xep_0066/oob.py | 89 ++++++++++++++++++++++++++++++++++ sleekxmpp/plugins/xep_0066/stanza.py | 33 +++++++++++++ tests/test_stream_xep_0066.py | 72 +++++++++++++++++++++++++++ 4 files changed, 205 insertions(+) create mode 100644 sleekxmpp/plugins/xep_0066/__init__.py create mode 100644 sleekxmpp/plugins/xep_0066/oob.py create mode 100644 sleekxmpp/plugins/xep_0066/stanza.py create mode 100644 tests/test_stream_xep_0066.py diff --git a/sleekxmpp/plugins/xep_0066/__init__.py b/sleekxmpp/plugins/xep_0066/__init__.py new file mode 100644 index 00000000..ebfbd0c2 --- /dev/null +++ b/sleekxmpp/plugins/xep_0066/__init__.py @@ -0,0 +1,11 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from sleekxmpp.plugins.xep_0066 import stanza +from sleekxmpp.plugins.xep_0066.stanza import OOB, OOBTransfer +from sleekxmpp.plugins.xep_0066.oob import xep_0066 diff --git a/sleekxmpp/plugins/xep_0066/oob.py b/sleekxmpp/plugins/xep_0066/oob.py new file mode 100644 index 00000000..b4322351 --- /dev/null +++ b/sleekxmpp/plugins/xep_0066/oob.py @@ -0,0 +1,89 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + + +from sleekxmpp.stanza import Message, Presence, Iq +from sleekxmpp.xmlstream import register_stanza_plugin +from sleekxmpp.xmlstream.handler import Callback +from sleekxmpp.xmlstream.matcher import StanzaPath +from sleekxmpp.plugins.base import base_plugin +from sleekxmpp.plugins.xep_0066 import stanza + + +class xep_0066(base_plugin): + + """ + XEP-0066: Out-of-Band Data + + Out-of-Band Data is a basic method for transferring files between + XMPP agents. The URL of the resource in question is sent to the receiving + entity, which then downloads the resource before responding to the OOB + request. OOB is also used as a generic means to transmit URLs in other + stanzas to indicate where to find additional information. + + Also see . + + Events: + oob_transfer -- Raised when a request to download a resource + has been received. + + Methods: + send_oob -- Send a request to another entity to download a file + or other addressable resource. + """ + + def plugin_init(self): + """Start the XEP-0066 plugin.""" + self.xep = '0066' + self.description = 'Out-of-Band Transfer' + self.stanza = stanza + + register_stanza_plugin(Iq, stanza.OOBTransfer) + register_stanza_plugin(Message, stanza.OOB) + register_stanza_plugin(Presence, stanza.OOB) + + self.xmpp.register_handler( + Callback('OOB Transfer', + StanzaPath('iq@type=set/oob_transfer'), + self._handle_transfer)) + + def post_init(self): + """Handle cross-plugin dependencies.""" + base_plugin.post_init(self) + self.xmpp['xep_0030'].add_feature(stanza.OOBTransfer.namespace) + self.xmpp['xep_0030'].add_feature(stanza.OOB.namespace) + + def send_oob(self, to, url, desc=None, ifrom=None, **iqargs): + """ + Initiate a basic file transfer by sending the URL of + a file or other resource. + + Arguments: + url -- The URL of the resource to transfer. + desc -- An optional human readable description of the item + that is to be transferred. + ifrom -- Specifiy the sender's JID. + block -- If true, block and wait for the stanzas' reply. + timeout -- The time in seconds to block while waiting for + a reply. If None, then wait indefinitely. + callback -- Optional callback to execute when a reply is + received instead of blocking and waiting for + the reply. + """ + iq = self.xmpp.Iq() + iq['type'] = 'set' + iq['to'] = to + if ifrom: + iq['from'] = ifrom + iq['oob_transfer']['url'] = url + iq['oob_transfer']['desc'] = desc + return iq.send(**iqargs) + + def _handle_transfer(self, iq): + """Handle receiving an out-of-band transfer request.""" + self.xmpp.event('oob_transfer', iq) diff --git a/sleekxmpp/plugins/xep_0066/stanza.py b/sleekxmpp/plugins/xep_0066/stanza.py new file mode 100644 index 00000000..21387485 --- /dev/null +++ b/sleekxmpp/plugins/xep_0066/stanza.py @@ -0,0 +1,33 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from sleekxmpp.xmlstream import ElementBase + + +class OOBTransfer(ElementBase): + + """ + """ + + name = 'query' + namespace = 'jabber:iq:oob' + plugin_attrib = 'oob_transfer' + interfaces = set(('url', 'desc', 'sid')) + sub_interfaces = set(('url', 'desc')) + + +class OOB(ElementBase): + + """ + """ + + name = 'x' + namespace = 'jabber:x:oob' + plugin_attrib = 'oob' + interfaces = set(('url', 'desc')) + sub_interfaces = interfaces diff --git a/tests/test_stream_xep_0066.py b/tests/test_stream_xep_0066.py new file mode 100644 index 00000000..3dbaf840 --- /dev/null +++ b/tests/test_stream_xep_0066.py @@ -0,0 +1,72 @@ +import time +import threading + +from sleekxmpp.test import * + + +class TestOOB(SleekTest): + + def tearDown(self): + self.stream_close() + + def testSendOOB(self): + """Test sending an OOB transfer request.""" + self.stream_start(plugins=['xep_0066', 'xep_0030']) + + url = 'http://github.com/fritzy/SleekXMPP/blob/master/README' + + t = threading.Thread( + name='send_oob', + target=self.xmpp['xep_0066'].send_oob, + args=('user@example.com', url), + kwargs={'desc': 'SleekXMPP README'}) + + t.start() + + self.send(""" + + + http://github.com/fritzy/SleekXMPP/blob/master/README + SleekXMPP README + + + """) + + self.recv(""" + + """) + + t.join() + + def testReceiveOOB(self): + """Test receiving an OOB request.""" + self.stream_start(plugins=['xep_0066', 'xep_0030']) + + events = [] + + def receive_oob(iq): + events.append(iq['oob_transfer']['url']) + + self.xmpp.add_event_handler('oob_transfer', receive_oob) + + self.recv(""" + + + http://github.com/fritzy/SleekXMPP/blob/master/README + SleekXMPP README + + + """) + + time.sleep(0.1) + + self.assertEqual(events, + ['http://github.com/fritzy/SleekXMPP/blob/master/README'], + 'URL was not received: %s' % events) + + +suite = unittest.TestLoader().loadTestsFromTestCase(TestOOB) -- cgit v1.2.3 From 7ccc67c06d12a1390558636526e6750caf874680 Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Sun, 3 Jul 2011 12:14:59 -0700 Subject: Added XEP-0082 plugin. This should make things much easier for any stanza that uses timestamps. --- sleekxmpp/plugins/xep_0082.py | 202 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 202 insertions(+) create mode 100644 sleekxmpp/plugins/xep_0082.py diff --git a/sleekxmpp/plugins/xep_0082.py b/sleekxmpp/plugins/xep_0082.py new file mode 100644 index 00000000..e36e062b --- /dev/null +++ b/sleekxmpp/plugins/xep_0082.py @@ -0,0 +1,202 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +import datetime as dt +from dateutil import parser +from dateutil.tz import tzoffset, tzutc +from sleekxmpp.plugins.base import base_plugin + + +# ===================================================================== +# To make it easier for stanzas without direct access to plugin objects +# to use the XEP-0082 utility methods, we will define them as top-level +# functions and then just reference them in the plugin itself. + +def parse(time_str): + """ + Convert a string timestamp into a datetime object. + + Arguments: + time_str -- A formatted timestamp string. + """ + return parser.parse(time_str) + +def format_date(time_obj): + """ + Return a formatted string version of a date object. + + Format: + YYYY-MM-DD + + Arguments: + time_obj -- A date or datetime object. + """ + if isinstance(time_obj, dt.datetime): + time_obj = time_obj.date() + return time_obj.isoformat() + +def format_time(time_obj): + """ + Return a formatted string version of a time object. + + format: + hh:mm:ss[.sss][TZD + + arguments: + time_obj -- A time or datetime object. + """ + if isinstance(time_obj, dt.datetime): + time_obj = time_obj.timetz() + timestamp = time_obj.isoformat() + if time_obj.tzinfo == tzutc(): + timestamp = timestamp[:-6] + return '%sZ' % timestamp + return timestamp + +def format_datetime(time_obj): + """ + Return a formatted string version of a datetime object. + + Format: + YYYY-MM-DDThh:mm:ss[.sss]TZD + + arguments: + time_obj -- A datetime object. + """ + timestamp = time_obj.isoformat('T') + if time_obj.tzinfo == tzutc(): + timestamp = timestamp[:-6] + return '%sZ' % timestamp + return timestamp + +def date(year=None, month=None, day=None): + """ + Create a date only timestamp for the given instant. + + Unspecified components default to their current counterparts. + + Arguments: + year -- Integer value of the year (4 digits) + month -- Integer value of the month + day -- Integer value of the day of the month. + """ + today = dt.datetime.today() + if year is None: + year = today.year + if month is None: + month = today.month + if day is None: + day = today.day + return format_date(dt.date(year, month, day)) + +def time(hour=None, min=None, sec=None, micro=None, offset=None): + """ + Create a time only timestamp for the given instant. + + Unspecified components default to their current counterparts. + + Arguments: + hour -- Integer value of the hour. + min -- Integer value of the number of minutes. + sec -- Integer value of the number of seconds. + micro -- Integer value of the number of microseconds. + offset -- A positive or negative number of seconds to + offset from UTC to match a desired timezone. + """ + now = dt.datetime.utcnow() + if hour is None: + hour = now.hour + if min is None: + min = now.minute + if sec is None: + sec = now.second + if micro is None: + micro = now.microsecond + if offset is None: + offset = tzutc() + else: + offset = tzoffset(None, offset) + time = dt.time(hour, min, sec, micro, offset) + return format_time(time) + +def datetime(year=None, month=None, day=None, hour=None, + min=None, sec=None, micro=None, offset=None, + separators=True): + """ + Create a datetime timestamp for the given instant. + + Unspecified components default to their current counterparts. + + Arguments: + year -- Integer value of the year (4 digits) + month -- Integer value of the month + day -- Integer value of the day of the month. + hour -- Integer value of the hour. + min -- Integer value of the number of minutes. + sec -- Integer value of the number of seconds. + micro -- Integer value of the number of microseconds. + offset -- A positive or negative number of seconds to + offset from UTC to match a desired timezone. + """ + now = dt.datetime.utcnow() + if year is None: + year = now.year + if month is None: + month = now.month + if day is None: + day = now.day + if hour is None: + hour = now.hour + if min is None: + min = now.minute + if sec is None: + sec = now.second + if micro is None: + micro = now.microsecond + if offset is None: + offset = tzutc() + else: + offset = tzoffset(None, offset) + + date = dt.datetime(year, month, day, hour, + sec, min, micro, offset) + return format_datetime(date) + +class xep_0082(base_plugin): + + """ + XEP-0082: XMPP Date and Time Profiles + + XMPP uses a subset of the formats allowed by ISO 8601 as a matter of + pragmatism based on the relatively few formats historically used by + the XMPP. + + Also see . + + Methods: + date -- Create a time stamp using the Date profile. + datetime -- Create a time stamp using the DateTime profile. + time -- Create a time stamp using the Time profile. + format_date -- Format an existing date object. + format_datetime -- Format an existing datetime object. + format_time -- Format an existing time object. + parse -- Convert a time string into a Python datetime object. + """ + + def plugin_init(self): + """Start the XEP-0082 plugin.""" + self.xep = '0082' + self.description = 'XMPP Date and Time Profiles' + + self.date = date + self.datetime = datetime + self.time = time + self.format_date = format_date + self.format_datetime = format_datetime + self.format_time = format_time + self.parse = parse -- cgit v1.2.3 From 2e8e542bc9391e49cb901217f77915f42cdd8c17 Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Sun, 3 Jul 2011 12:43:34 -0700 Subject: Added XEP-0203 Delayed Delivery plugin. --- sleekxmpp/plugins/__init__.py | 5 +++-- sleekxmpp/plugins/xep_0203/__init__.py | 12 ++++++++++ sleekxmpp/plugins/xep_0203/delay.py | 36 +++++++++++++++++++++++++++++ sleekxmpp/plugins/xep_0203/stanza.py | 41 ++++++++++++++++++++++++++++++++++ 4 files changed, 92 insertions(+), 2 deletions(-) create mode 100644 sleekxmpp/plugins/xep_0203/__init__.py create mode 100644 sleekxmpp/plugins/xep_0203/delay.py create mode 100644 sleekxmpp/plugins/xep_0203/stanza.py diff --git a/sleekxmpp/plugins/__init__.py b/sleekxmpp/plugins/__init__.py index d27937ae..5630c1d9 100644 --- a/sleekxmpp/plugins/__init__.py +++ b/sleekxmpp/plugins/__init__.py @@ -6,5 +6,6 @@ See the file LICENSE for copying permission. """ __all__ = ['xep_0004', 'xep_0009', 'xep_0012', 'xep_0030', 'xep_0033', - 'xep_0045', 'xep_0050', 'xep_0060', 'xep_0085', 'xep_0086', - 'xep_0092', 'xep_0128', 'xep_0199', 'xep_0202', 'gmail_notify'] + 'xep_0045', 'xep_0050', 'xep_0060', 'xep_0066', 'xep_0082', + 'xep_0085', 'xep_0086', 'xep_0092', 'xep_0128', 'xep_0199', + 'xep_0202', 'xep_0203', 'gmail_notify'] diff --git a/sleekxmpp/plugins/xep_0203/__init__.py b/sleekxmpp/plugins/xep_0203/__init__.py new file mode 100644 index 00000000..445ccf37 --- /dev/null +++ b/sleekxmpp/plugins/xep_0203/__init__.py @@ -0,0 +1,12 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from sleekxmpp.plugins.xep_0203 import stanza +from sleekxmpp.plugins.xep_0203.stanza import Delay +from sleekxmpp.plugins.xep_0203.delay import xep_0203 + diff --git a/sleekxmpp/plugins/xep_0203/delay.py b/sleekxmpp/plugins/xep_0203/delay.py new file mode 100644 index 00000000..8ff14d18 --- /dev/null +++ b/sleekxmpp/plugins/xep_0203/delay.py @@ -0,0 +1,36 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + + +from sleekxmpp.stanza import Message, Presence +from sleekxmpp.xmlstream import register_stanza_plugin +from sleekxmpp.plugins.base import base_plugin +from sleekxmpp.plugins.xep_0203 import stanza + + +class xep_0203(base_plugin): + + """ + XEP-0203: Delayed Delivery + + XMPP stanzas are sometimes withheld for delivery due to the recipient + being offline, or are resent in order to establish recent history as + is the case with MUCS. In any case, it is important to know when the + stanza was originally sent, not just when it was last received. + + Also see . + """ + + def plugin_init(self): + """Start the XEP-0203 plugin.""" + self.xep = '0203' + self.description = 'Delayed Delivery' + self.stanza = stanza + + register_stanza_plugin(Message, stanza.Delay) + register_stanza_plugin(Presence, stanza.Delay) diff --git a/sleekxmpp/plugins/xep_0203/stanza.py b/sleekxmpp/plugins/xep_0203/stanza.py new file mode 100644 index 00000000..baae4cd3 --- /dev/null +++ b/sleekxmpp/plugins/xep_0203/stanza.py @@ -0,0 +1,41 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +import datetime as dt + +from sleekxmpp.xmlstream import ElementBase +from sleekxmpp.plugins import xep_0082 + + +class Delay(ElementBase): + + """ + """ + + name = 'delay' + namespace = 'urn:xmpp:delay' + plugin_attrib = 'delay' + interfaces = set(('from', 'stamp', 'text')) + + def get_stamp(self): + timestamp = self._get_attr('stamp') + return xep_0082.parse(timestamp) + + def set_stamp(self, value): + if isinstance(value, dt.datetime): + value = xep_0082.format_datetime(value) + self._set_attr('stamp', value) + + def get_text(self): + return self.xml.text + + def set_text(self, value): + self.xml.text = value + + def del_text(self): + self.xml.text = '' -- cgit v1.2.3 From c98f5d44509f5b6fdfdbf7408dae3282caccc9db Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Sun, 3 Jul 2011 13:40:57 -0700 Subject: Fix some bugs in time handling. Namely, minutes and seconds were reversed. --- sleekxmpp/plugins/xep_0082.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/sleekxmpp/plugins/xep_0082.py b/sleekxmpp/plugins/xep_0082.py index e36e062b..785ba36b 100644 --- a/sleekxmpp/plugins/xep_0082.py +++ b/sleekxmpp/plugins/xep_0082.py @@ -105,8 +105,9 @@ def time(hour=None, min=None, sec=None, micro=None, offset=None): min -- Integer value of the number of minutes. sec -- Integer value of the number of seconds. micro -- Integer value of the number of microseconds. - offset -- A positive or negative number of seconds to - offset from UTC to match a desired timezone. + offset -- Either a positive or negative number of seconds + to offset from UTC to match a desired timezone, + or a tzinfo object. """ now = dt.datetime.utcnow() if hour is None: @@ -119,7 +120,7 @@ def time(hour=None, min=None, sec=None, micro=None, offset=None): micro = now.microsecond if offset is None: offset = tzutc() - else: + elif not isinstance(offset, dt.tzinfo): offset = tzoffset(None, offset) time = dt.time(hour, min, sec, micro, offset) return format_time(time) @@ -140,8 +141,9 @@ def datetime(year=None, month=None, day=None, hour=None, min -- Integer value of the number of minutes. sec -- Integer value of the number of seconds. micro -- Integer value of the number of microseconds. - offset -- A positive or negative number of seconds to - offset from UTC to match a desired timezone. + offset -- Either a positive or negative number of seconds + to offset from UTC to match a desired timezone, + or a tzinfo object. """ now = dt.datetime.utcnow() if year is None: @@ -160,11 +162,11 @@ def datetime(year=None, month=None, day=None, hour=None, micro = now.microsecond if offset is None: offset = tzutc() - else: + elif not isinstance(offset, dt.tzinfo): offset = tzoffset(None, offset) date = dt.datetime(year, month, day, hour, - sec, min, micro, offset) + min, sec, micro, offset) return format_datetime(date) class xep_0082(base_plugin): -- cgit v1.2.3 From ec3a14e6d91e61c76147d8415f7246086fd9d435 Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Sun, 3 Jul 2011 15:30:06 -0700 Subject: Updated XEP-0202 plugin to new format and use XEP-0082. --- sleekxmpp/plugins/xep_0202.py | 117 ------------------------------ sleekxmpp/plugins/xep_0202/__init__.py | 11 +++ sleekxmpp/plugins/xep_0202/stanza.py | 126 +++++++++++++++++++++++++++++++++ sleekxmpp/plugins/xep_0202/time.py | 90 +++++++++++++++++++++++ 4 files changed, 227 insertions(+), 117 deletions(-) delete mode 100644 sleekxmpp/plugins/xep_0202.py create mode 100644 sleekxmpp/plugins/xep_0202/__init__.py create mode 100644 sleekxmpp/plugins/xep_0202/stanza.py create mode 100644 sleekxmpp/plugins/xep_0202/time.py diff --git a/sleekxmpp/plugins/xep_0202.py b/sleekxmpp/plugins/xep_0202.py deleted file mode 100644 index 3b31c97a..00000000 --- a/sleekxmpp/plugins/xep_0202.py +++ /dev/null @@ -1,117 +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 datetime import datetime, tzinfo -import logging -import time - -from . import base -from .. stanza.iq import Iq -from .. xmlstream.handler.callback import Callback -from .. xmlstream.matcher.xpath import MatchXPath -from .. xmlstream import ElementBase, ET, JID, register_stanza_plugin - - -log = logging.getLogger(__name__) - - -class EntityTime(ElementBase): - name = 'time' - namespace = 'urn:xmpp:time' - plugin_attrib = 'entity_time' - interfaces = set(('tzo', 'utc')) - sub_interfaces = set(('tzo', 'utc')) - - #def get_tzo(self): - # TODO: Right now it returns a string but maybe it should - # return a datetime.tzinfo object or maybe a datetime.timedelta? - #pass - - def set_tzo(self, tzo): - if isinstance(tzo, tzinfo): - td = datetime.now(tzo).utcoffset() # What if we are faking the time? datetime.now() shouldn't be used here' - seconds = td.seconds + td.days * 24 * 3600 - sign = ('+' if seconds >= 0 else '-') - minutes = abs(seconds // 60) - tzo = '{sign}{hours:02d}:{minutes:02d}'.format(sign=sign, hours=minutes//60, minutes=minutes%60) - elif not isinstance(tzo, str): - raise TypeError('The time should be a string or a datetime.tzinfo object.') - self._set_sub_text('tzo', tzo) - - def get_utc(self): - # Returns a datetime object instead the string. Is this a good idea? - value = self._get_sub_text('utc') - if '.' in value: - return datetime.strptime(value, '%Y-%m-%dT%H:%M:%S.%fZ') - else: - return datetime.strptime(value, '%Y-%m-%dT%H:%M:%SZ') - - def set_utc(self, tim=None): - if isinstance(tim, datetime): - if tim.utcoffset(): - tim = tim - tim.utcoffset() - tim = tim.strftime('%Y-%m-%dT%H:%M:%SZ') - elif isinstance(tim, time.struct_time): - tim = time.strftime('%Y-%m-%dT%H:%M:%SZ', tim) - elif not isinstance(tim, str): - raise TypeError('The time should be a string or a datetime.datetime or time.struct_time object.') - - self._set_sub_text('utc', tim) - - -class xep_0202(base.base_plugin): - """ - XEP-0202 Entity Time - """ - def plugin_init(self): - self.description = "Entity Time" - self.xep = "0202" - - self.xmpp.registerHandler( - Callback('Time Request', - MatchXPath('{%s}iq/{%s}time' % (self.xmpp.default_ns, - EntityTime.namespace)), - self.handle_entity_time_query)) - register_stanza_plugin(Iq, EntityTime) - - self.xmpp.add_event_handler('entity_time_request', self.handle_entity_time) - - - def post_init(self): - base.base_plugin.post_init(self) - - self.xmpp.plugin['xep_0030'].add_feature('urn:xmpp:time') - - def handle_entity_time_query(self, iq): - if iq['type'] == 'get': - log.debug("Entity time requested by %s" % iq['from']) - self.xmpp.event('entity_time_request', iq) - elif iq['type'] == 'result': - log.debug("Entity time result from %s" % iq['from']) - self.xmpp.event('entity_time', iq) - - def handle_entity_time(self, iq): - iq = iq.reply() - iq.enable('entity_time') - tzo = time.strftime('%z') # %z is not on all ANSI C libraries - tzo = tzo[:3] + ':' + tzo[3:] - iq['entity_time']['tzo'] = tzo - iq['entity_time']['utc'] = datetime.utcnow() - iq.send() - - def get_entity_time(self, jid): - iq = self.xmpp.makeIqGet() - iq.enable('entity_time') - iq.attrib['to'] = jid - iq.attrib['from'] = self.xmpp.boundjid.full - id = iq.get('id') - result = iq.send() - if result and result is not None and result.get('type', 'error') != 'error': - return {'utc': result['entity_time']['utc'], 'tzo': result['entity_time']['tzo']} - else: - return False diff --git a/sleekxmpp/plugins/xep_0202/__init__.py b/sleekxmpp/plugins/xep_0202/__init__.py new file mode 100644 index 00000000..82338d3a --- /dev/null +++ b/sleekxmpp/plugins/xep_0202/__init__.py @@ -0,0 +1,11 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from sleekxmpp.plugins.xep_0202 import stanza +from sleekxmpp.plugins.xep_0202.stanza import EntityTime +from sleekxmpp.plugins.xep_0202.time import xep_0202 diff --git a/sleekxmpp/plugins/xep_0202/stanza.py b/sleekxmpp/plugins/xep_0202/stanza.py new file mode 100644 index 00000000..bb27692a --- /dev/null +++ b/sleekxmpp/plugins/xep_0202/stanza.py @@ -0,0 +1,126 @@ +""" + 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 datetime as dt +from dateutil.tz import tzoffset, tzutc + +from sleekxmpp.xmlstream import ElementBase +from sleekxmpp.plugins import xep_0082 + + +class EntityTime(ElementBase): + + """ + The