From bd9bf3f1c7c17606f455ce0cf9c4d0b6b237a7fe Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Thu, 27 Jan 2011 18:05:05 -0500 Subject: [PATCH 01/58] 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 3cf949a..a490510 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 3937a7a..83d8699 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 38b08d8..a6bb6eb 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 1150178..3d1ca3d 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 638e613..e456d28 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__() From 1a270dc05cc368000f3545975befa0589031b684 Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Fri, 28 Jan 2011 00:49:37 -0500 Subject: [PATCH 02/58] 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 | 355 ++++++++++++++++------------ 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, 470 insertions(+), 145 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 a181398..e05f8e7 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) + 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) - 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_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.registered_features.append((MatchXMLMask(mask), - pointer, - breaker)) + self._stream_feature_handlers[name] = (handler, restart) + self._stream_feature_order.append((order, name)) + self._stream_feature_order.sort() + + def 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) + """ + 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) + 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 - # 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): + 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): - """ - Handle encrypting the stream using TLS. - - Restarts the stream. - """ - log.debug("Starting TLS") - if self.start_tls(): - raise RestartStream() - - def _handle_sasl_auth(self, xml): + def _handle_sasl_auth(self, features): """ Handle authenticating using SASL. Arguments: - xml -- The SASL mechanisms stanza. + features -- The stream features stanza. """ - if '{urn:ietf:params:xml:ns:xmpp-tls}starttls' in self.features: - return False - log.debug("Starting SASL Auth") - sasl_ns = 'urn:ietf:params:xml:ns:xmpp-sasl' - self.add_handler("" % 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) + def sasl_success(stanza): + """SASL authentication succeeded. Restart the stream.""" + self.authenticated = True + self.features.append('mechanisms') + raise RestartStream() - 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') + 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 - auth = base64.b64encode(b'\x00' + user + \ - b'\x00' + password).decode('utf-8') + 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() - 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() return True - def _handle_auth_success(self, xml): + def _handle_sasl_plain(self, xmpp): """ - SASL authentication succeeded. Restart the stream. + Attempt to authenticate using SASL PLAIN. Arguments: - xml -- The SASL authentication success element. + xmpp -- The SleekXMPP connection instance. """ - self.authenticated = True - self.features = [] - raise RestartStream() + if not xmpp.boundjid.user: + return False - def _handle_auth_fail(self, xml): + 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_sasl_anonymous(self, xmpp): """ - SASL authentication failed. Disconnect and shutdown. + Attempt to authenticate using SASL ANONYMOUS. Arguments: - xml -- The SASL authentication failure element. + xmpp -- The SleekXMPP connection instance. """ - log.info("Authentication failed.") - self.event("failed_auth", direct=True) - self.disconnect() + if xmpp.boundjid.user: + return False - def _handle_bind_resource(self, xml): + resp = sasl.Auth(xmpp) + resp['mechanism'] = 'ANONYMOUS' + resp.send() + + return True + + 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 dbf7b86..ef44dfb 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 0000000..ae1f96f --- /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 0000000..e55a72a --- /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 0000000..b7b175d --- /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 0000000..5be2e55 --- /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 0000000..d85f9b4 --- /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) From af45b51f4fe32b37e80e51dea66b9fd0ca3d8ad2 Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Fri, 18 Mar 2011 17:57:49 -0400 Subject: [PATCH 03/58] 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 ef44dfb..4481fa4 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 b7b175d..c9d9715 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' From 58aa944a5e07032fc8d5770347f82e7c2ce6c948 Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Wed, 15 Jun 2011 10:55:36 -0700 Subject: [PATCH 04/58] 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 3992a4f..a7d4931 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) From d8d9e8df16c07bd13bbac72e4445a2930407b244 Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Mon, 20 Jun 2011 16:25:56 -0700 Subject: [PATCH 05/58] 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 cb3d344..3518fc7 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 5bc71f0..6282c8d 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 bc01c2a..1143ce2 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.""" From 3b1f3fddf093f9bad80522287b8425a713ea8c5e Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Tue, 28 Jun 2011 11:06:44 -0700 Subject: [PATCH 06/58] Reorganized stream level stanzas. --- sleekxmpp/clientxmpp.py | 3 +-- sleekxmpp/stanza/__init__.py | 9 ++++----- sleekxmpp/stanza/stream/__init__.py | 13 +++++++++++++ sleekxmpp/stanza/{ => stream}/bind.py | 3 ++- .../stanza/{stream_error.py => stream/error.py} | 0 .../{stream_features.py => stream/features.py} | 0 sleekxmpp/stanza/{ => stream}/sasl.py | 0 sleekxmpp/stanza/{ => stream}/session.py | 3 ++- sleekxmpp/stanza/{ => stream}/tls.py | 0 9 files changed, 22 insertions(+), 9 deletions(-) create mode 100644 sleekxmpp/stanza/stream/__init__.py rename sleekxmpp/stanza/{ => stream}/bind.py (86%) rename sleekxmpp/stanza/{stream_error.py => stream/error.py} (100%) rename sleekxmpp/stanza/{stream_features.py => stream/features.py} (100%) rename sleekxmpp/stanza/{ => stream}/sasl.py (100%) rename sleekxmpp/stanza/{ => stream}/session.py (86%) rename sleekxmpp/stanza/{ => stream}/tls.py (100%) diff --git a/sleekxmpp/clientxmpp.py b/sleekxmpp/clientxmpp.py index 3a5f41b..ea9654a 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 4481fa4..05df883 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/stream/__init__.py b/sleekxmpp/stanza/stream/__init__.py new file mode 100644 index 0000000..a386bba --- /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/bind.py b/sleekxmpp/stanza/stream/bind.py similarity index 86% rename from sleekxmpp/stanza/bind.py rename to sleekxmpp/stanza/stream/bind.py index ae1f96f..165afcb 100644 --- a/sleekxmpp/stanza/bind.py +++ b/sleekxmpp/stanza/stream/bind.py @@ -6,7 +6,8 @@ See the file LICENSE for copying permission. """ -from sleekxmpp.stanza import Iq, StreamFeatures +from sleekxmpp.stanza import Iq +from sleekxmpp.stanza.stream import StreamFeatures from sleekxmpp.xmlstream import ElementBase, ET, register_stanza_plugin diff --git a/sleekxmpp/stanza/stream_error.py b/sleekxmpp/stanza/stream/error.py similarity index 100% rename from sleekxmpp/stanza/stream_error.py rename to sleekxmpp/stanza/stream/error.py diff --git a/sleekxmpp/stanza/stream_features.py b/sleekxmpp/stanza/stream/features.py similarity index 100% rename from sleekxmpp/stanza/stream_features.py rename to sleekxmpp/stanza/stream/features.py diff --git a/sleekxmpp/stanza/sasl.py b/sleekxmpp/stanza/stream/sasl.py similarity index 100% rename from sleekxmpp/stanza/sasl.py rename to sleekxmpp/stanza/stream/sasl.py diff --git a/sleekxmpp/stanza/session.py b/sleekxmpp/stanza/stream/session.py similarity index 86% rename from sleekxmpp/stanza/session.py rename to sleekxmpp/stanza/stream/session.py index c9d9715..87f2185 100644 --- a/sleekxmpp/stanza/session.py +++ b/sleekxmpp/stanza/stream/session.py @@ -6,7 +6,8 @@ See the file LICENSE for copying permission. """ -from sleekxmpp.stanza import Iq, StreamFeatures +from sleekxmpp.stanza import Iq +from sleekxmpp.stanza.stream import StreamFeatures from sleekxmpp.xmlstream import ElementBase, ET, register_stanza_plugin diff --git a/sleekxmpp/stanza/tls.py b/sleekxmpp/stanza/stream/tls.py similarity index 100% rename from sleekxmpp/stanza/tls.py rename to sleekxmpp/stanza/stream/tls.py From 9ed972ffeba8f5071d5cae8497322764207fec04 Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Wed, 29 Jun 2011 14:05:27 -0700 Subject: [PATCH 07/58] 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 ea9654a..5d7ca12 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. From 754ac5092a3a37819a71f6565a1e54b3f2547940 Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Thu, 30 Jun 2011 15:40:22 -0700 Subject: [PATCH 08/58] Reorganize features into plugins. --- sleekxmpp/basexmpp.py | 23 ++- sleekxmpp/clientxmpp.py | 203 ++--------------------- 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, 391 insertions(+), 198 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 8e5c762..43ad420 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 5d7ca12..9c2696d 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) + # Setup default stream features + self.register_plugin('feature_starttls') + self.register_plugin('feature_mechanisms') + self.register_plugin('feature_bind') + self.register_plugin('feature_session') - self.register_sasl_mechanism('PLAIN', - self._handle_sasl_plain, - priority=1) - self.register_sasl_mechanism('ANONYMOUS', - self._handle_sasl_anonymous, - priority=0) + # 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 0000000..940a37f --- /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 0000000..caa3844 --- /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 0000000..994c9be --- /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 0000000..5bae358 --- /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 0000000..5367fa4 --- /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 0000000..469d9d1 --- /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 0000000..36c7d9d --- /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 2dd68c8..561421d 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 From 634f5d691bab9855deddc4c201389bb60470d76e Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Fri, 1 Jul 2011 14:45:55 -0700 Subject: [PATCH 09/58] Continued reorganization and streamlining. --- sleekxmpp/basexmpp.py | 8 ++++---- sleekxmpp/clientxmpp.py | 2 -- sleekxmpp/features/__init__.py | 1 + sleekxmpp/features/feature_bind/__init__.py | 10 ++++++++++ .../{feature_bind.py => feature_bind/bind.py} | 7 +++++++ .../bind.py => features/feature_bind/stanza.py} | 7 +------ sleekxmpp/features/feature_mechanisms/__init__.py | 10 ++++++++++ .../mechanisms.py} | 0 .../feature_mechanisms/stanza.py} | 0 sleekxmpp/features/feature_session/__init__.py | 10 ++++++++++ .../session.py} | 8 ++++++++ .../feature_session/stanza.py} | 7 +------ sleekxmpp/features/feature_starttls/__init__.py | 10 ++++++++++ .../tls.py => features/feature_starttls/stanza.py} | 3 --- .../starttls.py} | 13 +++++++++---- sleekxmpp/stanza/__init__.py | 6 ++---- sleekxmpp/stanza/stream/__init__.py | 5 ----- .../stanza/{stream/error.py => stream_error.py} | 0 .../{stream/features.py => stream_features.py} | 0 19 files changed, 73 insertions(+), 34 deletions(-) create mode 100644 sleekxmpp/features/feature_bind/__init__.py rename sleekxmpp/features/{feature_bind.py => feature_bind/bind.py} (84%) rename sleekxmpp/{stanza/stream/bind.py => features/feature_bind/stanza.py} (73%) create mode 100644 sleekxmpp/features/feature_mechanisms/__init__.py rename sleekxmpp/features/{feature_mechanisms.py => feature_mechanisms/mechanisms.py} (100%) rename sleekxmpp/{stanza/stream/sasl.py => features/feature_mechanisms/stanza.py} (100%) create mode 100644 sleekxmpp/features/feature_session/__init__.py rename sleekxmpp/features/{feature_session.py => feature_session/session.py} (79%) rename sleekxmpp/{stanza/stream/session.py => features/feature_session/stanza.py} (71%) create mode 100644 sleekxmpp/features/feature_starttls/__init__.py rename sleekxmpp/{stanza/stream/tls.py => features/feature_starttls/stanza.py} (94%) rename sleekxmpp/features/{feature_starttls.py => feature_starttls/starttls.py} (79%) rename sleekxmpp/stanza/{stream/error.py => stream_error.py} (100%) rename sleekxmpp/stanza/{stream/features.py => stream_features.py} (100%) diff --git a/sleekxmpp/basexmpp.py b/sleekxmpp/basexmpp.py index 43ad420..b188e76 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 9c2696d..7245053 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 940a37f..65d2bdb 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/__init__.py b/sleekxmpp/features/feature_bind/__init__.py new file mode 100644 index 0000000..fce94dd --- /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.py b/sleekxmpp/features/feature_bind/bind.py similarity index 84% rename from sleekxmpp/features/feature_bind.py rename to sleekxmpp/features/feature_bind/bind.py index caa3844..e177d7b 100644 --- a/sleekxmpp/features/feature_bind.py +++ b/sleekxmpp/features/feature_bind/bind.py @@ -8,6 +8,9 @@ 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 @@ -22,12 +25,16 @@ class feature_bind(base_plugin): 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. diff --git a/sleekxmpp/stanza/stream/bind.py b/sleekxmpp/features/feature_bind/stanza.py similarity index 73% rename from sleekxmpp/stanza/stream/bind.py rename to sleekxmpp/features/feature_bind/stanza.py index 165afcb..f3e025f 100644 --- a/sleekxmpp/stanza/stream/bind.py +++ b/sleekxmpp/features/feature_bind/stanza.py @@ -6,8 +6,7 @@ See the file LICENSE for copying permission. """ -from sleekxmpp.stanza import Iq -from sleekxmpp.stanza.stream import StreamFeatures +from sleekxmpp.stanza import Iq, StreamFeatures from sleekxmpp.xmlstream import ElementBase, ET, register_stanza_plugin @@ -21,7 +20,3 @@ class Bind(ElementBase): interfaces = set(('resource', 'jid')) sub_interfaces = interfaces plugin_attrib = 'bind' - - -register_stanza_plugin(Iq, Bind) -register_stanza_plugin(StreamFeatures, Bind) diff --git a/sleekxmpp/features/feature_mechanisms/__init__.py b/sleekxmpp/features/feature_mechanisms/__init__.py new file mode 100644 index 0000000..a93b2b6 --- /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.py b/sleekxmpp/features/feature_mechanisms/mechanisms.py similarity index 100% rename from sleekxmpp/features/feature_mechanisms.py rename to sleekxmpp/features/feature_mechanisms/mechanisms.py diff --git a/sleekxmpp/stanza/stream/sasl.py b/sleekxmpp/features/feature_mechanisms/stanza.py similarity index 100% rename from sleekxmpp/stanza/stream/sasl.py rename to sleekxmpp/features/feature_mechanisms/stanza.py diff --git a/sleekxmpp/features/feature_session/__init__.py b/sleekxmpp/features/feature_session/__init__.py new file mode 100644 index 0000000..1399f73 --- /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.py b/sleekxmpp/features/feature_session/session.py similarity index 79% rename from sleekxmpp/features/feature_session.py rename to sleekxmpp/features/feature_session/session.py index 5bae358..4d17b2d 100644 --- a/sleekxmpp/features/feature_session.py +++ b/sleekxmpp/features/feature_session/session.py @@ -8,10 +8,14 @@ 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__) @@ -22,12 +26,16 @@ class feature_session(base_plugin): 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. diff --git a/sleekxmpp/stanza/stream/session.py b/sleekxmpp/features/feature_session/stanza.py similarity index 71% rename from sleekxmpp/stanza/stream/session.py rename to sleekxmpp/features/feature_session/stanza.py index 87f2185..2047a4f 100644 --- a/sleekxmpp/stanza/stream/session.py +++ b/sleekxmpp/features/feature_session/stanza.py @@ -6,8 +6,7 @@ See the file LICENSE for copying permission. """ -from sleekxmpp.stanza import Iq -from sleekxmpp.stanza.stream import StreamFeatures +from sleekxmpp.stanza import Iq, StreamFeatures from sleekxmpp.xmlstream import ElementBase, ET, register_stanza_plugin @@ -20,7 +19,3 @@ class Session(ElementBase): 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/features/feature_starttls/__init__.py b/sleekxmpp/features/feature_starttls/__init__.py new file mode 100644 index 0000000..042e37f --- /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/stanza/stream/tls.py b/sleekxmpp/features/feature_starttls/stanza.py similarity index 94% rename from sleekxmpp/stanza/stream/tls.py rename to sleekxmpp/features/feature_starttls/stanza.py index d85f9b4..5fdafab 100644 --- a/sleekxmpp/stanza/stream/tls.py +++ b/sleekxmpp/features/feature_starttls/stanza.py @@ -45,6 +45,3 @@ class Failure(StanzaBase): name = 'failure' namespace = 'urn:ietf:params:xml:ns:xmpp-tls' interfaces = set() - - -register_stanza_plugin(StreamFeatures, STARTTLS) diff --git a/sleekxmpp/features/feature_starttls.py b/sleekxmpp/features/feature_starttls/starttls.py similarity index 79% rename from sleekxmpp/features/feature_starttls.py rename to sleekxmpp/features/feature_starttls/starttls.py index 5367fa4..cbb94be 100644 --- a/sleekxmpp/features/feature_starttls.py +++ b/sleekxmpp/features/feature_starttls/starttls.py @@ -8,11 +8,12 @@ import logging -from sleekxmpp.stanza.stream import tls -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 +from sleekxmpp.features.feature_starttls import stanza log = logging.getLogger(__name__) @@ -24,11 +25,11 @@ class feature_starttls(base_plugin): self.name = "STARTTLS" self.rfc = '6120' self.description = "STARTTLS Stream Feature" + self.stanza = stanza - self.xmpp.register_stanza(tls.Proceed) self.xmpp.register_handler( Callback('STARTTLS Proceed', - MatchXPath(tls.Proceed.tag_name()), + MatchXPath(stanza.Proceed.tag_name()), self._handle_starttls_proceed, instream=True)) self.xmpp.register_feature('starttls', @@ -36,6 +37,10 @@ class feature_starttls(base_plugin): 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. diff --git a/sleekxmpp/stanza/__init__.py b/sleekxmpp/stanza/__init__.py index 05df883..4bd37dc 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 a386bba..2cb7967 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/error.py b/sleekxmpp/stanza/stream_error.py similarity index 100% rename from sleekxmpp/stanza/stream/error.py rename to sleekxmpp/stanza/stream_error.py diff --git a/sleekxmpp/stanza/stream/features.py b/sleekxmpp/stanza/stream_features.py similarity index 100% rename from sleekxmpp/stanza/stream/features.py rename to sleekxmpp/stanza/stream_features.py From 2a2ac73845ffc8695e2bc55746f45e1a18d55e6c Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Fri, 1 Jul 2011 15:15:13 -0700 Subject: [PATCH 10/58] 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 | 101 +++++++++++++++++++++++++++++-- tests/test_stream_xep_0030.py | 14 ++--- tests/test_stream_xep_0128.py | 1 - tests/test_stream_xep_0249.py | 1 - 6 files changed, 112 insertions(+), 39 deletions(-) diff --git a/sleekxmpp/stanza/rootstanza.py b/sleekxmpp/stanza/rootstanza.py index bc11476..9e1d1cf 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 6282c8d..c7d0d3a 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. + Meant to be overridden. + + Arguments: + exception -- An unhandled exception object. """ - init_old = threading.Thread.__init__ - - def init(self, *args, **kwargs): - init_old(self, *args, **kwargs) - run_old = self.run - - 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 + 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 1143ce2..c41edbb 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,6 @@ class TestStreamExceptions(SleekTest): 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 +41,49 @@ class TestStreamExceptions(SleekTest): """) + 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) + + 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. + + + + + """) + + 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 c960fc7..1666d3a 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 6fee655..42fc914 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 f49d1f7..9a25253 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): From b0297af38d6dcd9ebfdaa0131ea798c9fe2b8c63 Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Sat, 2 Jul 2011 21:43:02 -0700 Subject: [PATCH 11/58] 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 7245053..17a7582 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 994c9be..3cdb1b0 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 469d9d1..71a4b2e 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 36c7d9d..270d28f 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) From fba235a801a3a1c06d1769cdc944b72dce33f88a Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Sat, 2 Jul 2011 21:57:50 -0700 Subject: [PATCH 12/58] 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 17a7582..5b36e84 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 3cdb1b0..a8a046e 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 71a4b2e..98a0d36 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 270d28f..427660a 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 2cb7967..0000000 --- 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. -""" - From b898b14b77d739cb1c118c9e3648aa268348d293 Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Sat, 2 Jul 2011 22:30:34 -0700 Subject: [PATCH 13/58] 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 5b36e84..5eb9c90 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 e177d7b..c5d9395 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 a8a046e..011010f 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 4d17b2d..9c5e044 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 cbb94be..841e7a8 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() From 219df582dab2a5dd3c9e2bbfef27d3cfa814841d Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Sat, 2 Jul 2011 22:49:34 -0700 Subject: [PATCH 14/58] 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 65d2bdb..5c86cfe 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 fce94dd..aa854f8 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 c5d9395..0b0f203 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 f3e025f..2c1484e 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 a93b2b6..b0b9dcc 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 011010f..210267f 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 1399f73..3c84bae 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 9c5e044..0daec5d 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 2047a4f..40ea583 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 042e37f..4ae8943 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 5fdafab..8b09ad9 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 841e7a8..639788a 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. From 540d7496954c38e5483205410662120ec9ccd8c8 Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Sat, 2 Jul 2011 22:50:31 -0700 Subject: [PATCH 15/58] 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 5d1ce50..93231a4 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): From 0224d028e76ba608400fe55602fdb84f8e70f13b Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Sat, 2 Jul 2011 23:09:29 -0700 Subject: [PATCH 16/58] 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 +- .../features/feature_mechanisms/__init__.py | 5 +- .../features/feature_mechanisms/mechanisms.py | 9 ++- .../feature_mechanisms/stanza/__init__.py | 14 ++++ .../feature_mechanisms/stanza/auth.py | 35 +++++++++ .../feature_mechanisms/stanza/failure.py | 76 +++++++++++++++++++ .../{stanza.py => stanza/mechanisms.py} | 51 +------------ .../feature_mechanisms/stanza/success.py | 22 ++++++ 8 files changed, 158 insertions(+), 56 deletions(-) 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 rename sleekxmpp/features/feature_mechanisms/{stanza.py => stanza/mechanisms.py} (59%) 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 0b0f203..de03192 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 b0b9dcc..5379ef4 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 210267f..7a87779 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/__init__.py b/sleekxmpp/features/feature_mechanisms/stanza/__init__.py new file mode 100644 index 0000000..0d9135d --- /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 0000000..1220884 --- /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 0000000..98a1ab8 --- /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.py b/sleekxmpp/features/feature_mechanisms/stanza/mechanisms.py similarity index 59% rename from sleekxmpp/features/feature_mechanisms/stanza.py rename to sleekxmpp/features/feature_mechanisms/stanza/mechanisms.py index e55a72a..1189cd8 100644 --- a/sleekxmpp/features/feature_mechanisms/stanza.py +++ b/sleekxmpp/features/feature_mechanisms/stanza/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. @@ -53,52 +53,3 @@ class Mechanisms(ElementBase): 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/success.py b/sleekxmpp/features/feature_mechanisms/stanza/success.py new file mode 100644 index 0000000..2c40f56 --- /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 From 086bf89d699c88ab89ad1e1975d6022335ca5c04 Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Sun, 3 Jul 2011 00:35:36 -0700 Subject: [PATCH 17/58] 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 0000000..ebfbd0c --- /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 0000000..b432235 --- /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 0000000..2138748 --- /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 0000000..3dbaf84 --- /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) From 7ccc67c06d12a1390558636526e6750caf874680 Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Sun, 3 Jul 2011 12:14:59 -0700 Subject: [PATCH 18/58] 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 0000000..e36e062 --- /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 From 2e8e542bc9391e49cb901217f77915f42cdd8c17 Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Sun, 3 Jul 2011 12:43:34 -0700 Subject: [PATCH 19/58] 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 d27937a..5630c1d 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 0000000..445ccf3 --- /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 0000000..8ff14d1 --- /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 0000000..baae4cd --- /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 = '' From c98f5d44509f5b6fdfdbf7408dae3282caccc9db Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Sun, 3 Jul 2011 13:40:57 -0700 Subject: [PATCH 20/58] 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 e36e062..785ba36 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): From ec3a14e6d91e61c76147d8415f7246086fd9d435 Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Sun, 3 Jul 2011 15:30:06 -0700 Subject: [PATCH 21/58] 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 3b31c97..0000000 --- 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 0000000..82338d3 --- /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 0000000..bb27692 --- /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