From 1a270dc05cc368000f3545975befa0589031b684 Mon Sep 17 00:00:00 2001
From: Lance Stout <lancestout@gmail.com>
Date: Fri, 28 Jan 2011 00:49:37 -0500
Subject: First pass at re-worked stream features.

Stream features now use stanza objects!

Features are given a ranking that expresses the dependency
relationships (since only one feature is negotiated at a time, the
dependency graph can be replaced by a line).

>>> xmpp.register_feature('my_feature', _my_handler,
>>>                       restart=True,   # Requires stream restart
>>>                       order=600)      # Ranking (out of ~ 10,000,
>>>                                       # lower #'s executed first)

SASL mechanisms may now be added or disabled as needed. Each mechanism
is given a priority value indicating the order in which the client
wishes for mechanisms to be tried. Higher priority numbers are executed
first.

>>> xmpp.register_sasl_mechanism('SASL-MECH', _mech_handler,
>>>                              priority=0)

Disabling a SASL mechanism:

>>> xmpp.remove_sasl_mechanism('ANONYMOUS')
---
 sleekxmpp/clientxmpp.py             | 361 +++++++++++++++++++++---------------
 sleekxmpp/stanza/__init__.py        |   3 +
 sleekxmpp/stanza/bind.py            |  26 +++
 sleekxmpp/stanza/sasl.py            | 104 +++++++++++
 sleekxmpp/stanza/session.py         |  25 +++
 sleekxmpp/stanza/stream_features.py |  52 ++++++
 sleekxmpp/stanza/tls.py             |  50 +++++
 7 files changed, 473 insertions(+), 148 deletions(-)
 create mode 100644 sleekxmpp/stanza/bind.py
 create mode 100644 sleekxmpp/stanza/sasl.py
 create mode 100644 sleekxmpp/stanza/session.py
 create mode 100644 sleekxmpp/stanza/stream_features.py
 create mode 100644 sleekxmpp/stanza/tls.py

(limited to 'sleekxmpp')

diff --git a/sleekxmpp/clientxmpp.py b/sleekxmpp/clientxmpp.py
index a1813985..e05f8e7b 100644
--- a/sleekxmpp/clientxmpp.py
+++ b/sleekxmpp/clientxmpp.py
@@ -18,9 +18,11 @@ import threading
 from sleekxmpp import plugins
 from sleekxmpp import stanza
 from sleekxmpp.basexmpp import BaseXMPP
-from sleekxmpp.stanza import Message, Presence, Iq
+from sleekxmpp.stanza import *
+from sleekxmpp.stanza import tls
+from sleekxmpp.stanza import sasl
 from sleekxmpp.xmlstream import XMLStream, RestartStream
-from sleekxmpp.xmlstream import StanzaBase, ET
+from sleekxmpp.xmlstream import StanzaBase, ET, register_stanza_plugin
 from sleekxmpp.xmlstream.matcher import *
 from sleekxmpp.xmlstream.handler import *
 
@@ -92,14 +94,24 @@ class ClientXMPP(BaseXMPP):
         self.stream_footer = "</stream:stream>"
 
         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(
-            "<starttls xmlns='urn:ietf:params:xml:ns:xmpp-tls' />",
-            self._handle_starttls, True)
-        self.register_feature(
-            "<mechanisms xmlns='urn:ietf:params:xml:ns:xmpp-sasl' />",
-            self._handle_sasl_auth, True)
-        self.register_feature(
-            "<bind xmlns='urn:ietf:params:xml:ns:xmpp-bind' />",
-            self._handle_bind_resource)
-        self.register_feature(
-            "<session xmlns='urn:ietf:params:xml:ns:xmpp-session' />",
-            self._handle_start_session)
-
-    def handle_connected(self, event=None):
-        #TODO: Use stream state here
-        self.authenticated = False
-        self.sessionstarted = False
-        self.bound = False
-        self.bindfail = False
-        self.schedule("session timeout checker", 15,
-                      self._session_timeout_check)
-
-    def _session_timeout_check(self):
-        if not self.session_started_event.isSet():
-            log.debug("Session start has taken more than 15 seconds")
-            self.disconnect(reconnect=self.auto_reconnect)
+        self.register_feature('starttls', self._handle_starttls,
+                              restart=True,
+                              order=0)
+        self.register_feature('mechanisms', self._handle_sasl_auth,
+                              restart=True,
+                              order=100)
+        self.register_feature('bind', self._handle_bind_resource,
+                              restart=False,
+                              order=10000)
+        self.register_feature('session', self._handle_start_session,
+                              restart=False,
+                              order=10001)
+
+        self.register_sasl_mechanism('PLAIN',
+                                     self._handle_sasl_plain,
+                                     priority=1)
+        self.register_sasl_mechanism('ANONYMOUS',
+                                     self._handle_sasl_plain,
+                                     priority=0)
 
     def connect(self, address=tuple(), reattempt=True):
         """
@@ -197,19 +202,54 @@ class ClientXMPP(BaseXMPP):
         return XMLStream.connect(self, address[0], address[1],
                                  use_tls=True, reattempt=reattempt)
 
-    def register_feature(self, mask, pointer, breaker=False):
+    def register_feature(self, name, handler, restart=False, order=5000):
         """
         Register a stream feature.
 
         Arguments:
-            mask    -- An XML string matching the feature's element.
-            pointer -- The function to execute if the feature is received.
-            breaker -- Indicates if feature processing should halt with
+            name    -- The name of the stream feature.
+            handler -- The function to execute if the feature is received.
+            restart -- Indicates if feature processing should halt with
                        this feature. Defaults to False.
+            order   -- The relative ordering in which the feature should
+                       be negotiated. Lower values will be attempted
+                       earlier when available.
+        """
+        self._stream_feature_handlers[name] = (handler, restart)
+        self._stream_feature_order.append((order, name))
+        self._stream_feature_order.sort()
+
+    def register_sasl_mechanism(self, name, handler, priority=0):
+        """
+        Register a handler for a SASL authentication mechanism.
+
+        Arguments:
+            name     -- The name of the mechanism (all caps)
+            handler  -- The function that will perform the
+                        authentication. The function must
+                        return True if it is able to carry
+                        out the authentication, False if
+                        a required condition is not met.
+            priority -- An integer value indicating the
+                        preferred ordering for the mechanism.
+                        High values will be attempted first.
+        """
+        self._sasl_mechanism_handlers[name] = handler
+        self._sasl_mechanism_priorities.append((priority, name))
+        self._sasl_mechanism_priorities.sort(reverse=True)
+
+    def remove_sasl_mechanism(self, name):
+        """
+        Remove support for a given SASL authentication mechanism.
+
+        Arguments:
+            name -- The name of the mechanism to remove (all caps)
         """
-        self.registered_features.append((MatchXMLMask(mask),
-                                         pointer,
-                                         breaker))
+        if name in self._sasl_mechanism_handlers:
+            del self._sasl_mechanism_handlers[name]
+
+        p = self._sasl_mechanism_priorities
+        self._sasl_mechanism_priorities = [i for i in p if i[1] != name]
 
     def update_roster(self, jid, name=None, subscription=None, groups=[]):
         """
@@ -223,7 +263,8 @@ class ClientXMPP(BaseXMPP):
                             to 'remove', the entry will be deleted.
             groups       -- The roster groups that contain this item.
         """
-        iq = self.Iq()._set_stanza_values({'type': 'set'})
+        iq = self.Iq()
+        iq['type'] = 'set'
         iq['roster']['items'] = {jid: {'name': name,
                                        'subscription': subscription,
                                        'groups': groups}}
@@ -242,10 +283,27 @@ class ClientXMPP(BaseXMPP):
 
     def get_roster(self):
         """Request the roster from the server."""
-        iq = self.Iq()._set_stanza_values({'type': 'get'}).enable('roster')
+        iq = self.Iq()
+        iq['type'] = 'get'
+        iq.enable('roster')
         response = iq.send()
         self._handle_roster(response, request=True)
 
+    def _handle_connected(self, event=None):
+        #TODO: Use stream state here
+        self.authenticated = False
+        self.sessionstarted = False
+        self.bound = False
+        self.bindfail = False
+        self.features = []
+
+        def session_timeout():
+            if not self.session_started_event.isSet():
+                log.debug("Session start has taken more than 15 seconds")
+                self.disconnect(reconnect=self.auto_reconnect)
+
+        self.schedule("session timeout checker", 15, session_timeout)
+
     def _handle_stream_features(self, features):
         """
         Process the received stream features.
@@ -253,167 +311,174 @@ class ClientXMPP(BaseXMPP):
         Arguments:
             features -- The features stanza.
         """
-        # Record all of the features.
-        self.features = []
-        for sub in features.xml:
-            self.features.append(sub.tag)
-
-        # Process the features.
-        for sub in features.xml:
-            for feature in self.registered_features:
-                mask, handler, halt = feature
-                if mask.match(sub):
-                    if handler(sub) and halt:
-                        # Don't continue if the feature was
-                        # marked as a breaker.
-                        return True
-
-    def _handle_starttls(self, xml):
+        for order, name in self._stream_feature_order:
+            if name in features['features']:
+                handler, restart = self._stream_feature_handlers[name]
+                if handler(features) and restart:
+                    # Don't continue if the feature requires
+                    # restarting the XML stream.
+                    return True
+
+    def _handle_starttls(self, features):
         """
         Handle notification that the server supports TLS.
 
         Arguments:
-            xml -- The STARTLS proceed element.
+            features -- The stream:features element.
         """
-        if not self.authenticated and self.ssl_support:
-            tls_ns = 'urn:ietf:params:xml:ns:xmpp-tls'
-            self.add_handler("<proceed xmlns='%s' />" % tls_ns,
-                             self._handle_tls_start,
-                             name='TLS Proceed',
-                             instream=True)
-            self.send_xml(xml)
+
+        def tls_proceed(proceed):
+            """Restart the XML stream when TLS is accepted."""
+            log.debug("Starting TLS")
+            if self.start_tls():
+                self.features.append('starttls')
+                raise RestartStream()
+
+        if self.ssl_support:
+            self.register_handler(
+                    Callback('STARTTLS Proceed',
+                            MatchXPath(tls.Proceed.tag_name()),
+                            tls_proceed,
+                            instream=True))
+            self.send(features['starttls'])
             return True
         else:
             log.warning("The module tlslite is required to log in" +\
                             " to some servers, and has not been found.")
             return False
 
-    def _handle_tls_start(self, xml):
+    def _handle_sasl_auth(self, features):
         """
-        Handle encrypting the stream using TLS.
+        Handle authenticating using SASL.
 
-        Restarts the stream.
+        Arguments:
+            features -- The stream features stanza.
         """
-        log.debug("Starting TLS")
-        if self.start_tls():
+
+        def sasl_success(stanza):
+            """SASL authentication succeeded. Restart the stream."""
+            self.authenticated = True
+            self.features.append('mechanisms')
             raise RestartStream()
 
-    def _handle_sasl_auth(self, xml):
+        def sasl_fail(stanza):
+            """SASL authentication failed. Disconnect and shutdown."""
+            log.info("Authentication failed.")
+            self.event("failed_auth", direct=True)
+            self.disconnect()
+            log.debug("Starting SASL Auth")
+            return True
+
+        self.register_handler(
+                Callback('SASL Success',
+                         MatchXPath(sasl.Success.tag_name()),
+                         sasl_success,
+                         instream=True,
+                         once=True))
+
+        self.register_handler(
+                Callback('SASL Failure',
+                         MatchXPath(sasl.Failure.tag_name()),
+                         sasl_fail,
+                         instream=True,
+                         once=True))
+
+        for priority, mech in self._sasl_mechanism_priorities:
+            if mech in self._sasl_mechanism_handlers:
+                handler = self._sasl_mechanism_handlers[mech]
+                if handler(self):
+                    break
+        else:
+            log.error("No appropriate login method.")
+            self.disconnect()
+
+        return True
+
+    def _handle_sasl_plain(self, xmpp):
         """
-        Handle authenticating using SASL.
+        Attempt to authenticate using SASL PLAIN.
 
         Arguments:
-            xml -- The SASL mechanisms stanza.
+            xmpp -- The SleekXMPP connection instance.
         """
-        if '{urn:ietf:params:xml:ns:xmpp-tls}starttls' in self.features:
+        if not xmpp.boundjid.user:
             return False
 
-        log.debug("Starting SASL Auth")
-        sasl_ns = 'urn:ietf:params:xml:ns:xmpp-sasl'
-        self.add_handler("<success xmlns='%s' />" % sasl_ns,
-                         self._handle_auth_success,
-                         name='SASL Sucess',
-                         instream=True)
-        self.add_handler("<failure xmlns='%s' />" % sasl_ns,
-                         self._handle_auth_fail,
-                         name='SASL Failure',
-                         instream=True)
-
-        sasl_mechs = xml.findall('{%s}mechanism' % sasl_ns)
-        if sasl_mechs:
-            for sasl_mech in sasl_mechs:
-                self.features.append("sasl:%s" % sasl_mech.text)
-            if 'sasl:PLAIN' in self.features and self.boundjid.user:
-                if sys.version_info < (3, 0):
-                    user = bytes(self.boundjid.user)
-                    password = bytes(self.password)
-                else:
-                    user = bytes(self.boundjid.user, 'utf-8')
-                    password = bytes(self.password, 'utf-8')
-
-                auth = base64.b64encode(b'\x00' + user + \
-                                        b'\x00' + password).decode('utf-8')
-
-                self.send("<auth xmlns='%s' mechanism='PLAIN'>%s</auth>" % (
-                    sasl_ns,
-                    auth))
-            elif 'sasl:ANONYMOUS' in self.features and not self.boundjid.user:
-                self.send("<auth xmlns='%s' mechanism='%s' />" % (
-                    sasl_ns,
-                    'ANONYMOUS'))
-            else:
-                log.error("No appropriate login method.")
-                self.disconnect()
+        if sys.version_info < (3, 0):
+            user = bytes(self.boundjid.user)
+            password = bytes(self.password)
+        else:
+            user = bytes(self.boundjid.user, 'utf-8')
+            password = bytes(self.password, 'utf-8')
+
+        auth = base64.b64encode(b'\x00' + user + \
+                                b'\x00' + password).decode('utf-8')
+
+        resp = sasl.Auth(xmpp)
+        resp['mechanism'] = 'PLAIN'
+        resp['value'] = auth
+        resp.send()
+
         return True
 
-    def _handle_auth_success(self, xml):
+    def _handle_sasl_anonymous(self, xmpp):
         """
-        SASL authentication succeeded. Restart the stream.
+        Attempt to authenticate using SASL ANONYMOUS.
 
         Arguments:
-            xml -- The SASL authentication success element.
+            xmpp -- The SleekXMPP connection instance.
         """
-        self.authenticated = True
-        self.features = []
-        raise RestartStream()
+        if xmpp.boundjid.user:
+            return False
 
-    def _handle_auth_fail(self, xml):
-        """
-        SASL authentication failed. Disconnect and shutdown.
+        resp = sasl.Auth(xmpp)
+        resp['mechanism'] = 'ANONYMOUS'
+        resp.send()
 
-        Arguments:
-            xml -- The SASL authentication failure element.
-        """
-        log.info("Authentication failed.")
-        self.event("failed_auth", direct=True)
-        self.disconnect()
+        return True
 
-    def _handle_bind_resource(self, xml):
+    def _handle_bind_resource(self, features):
         """
         Handle requesting a specific resource.
 
         Arguments:
-            xml -- The bind feature element.
+            features -- The stream features stanza.
         """
         log.debug("Requesting resource: %s" % self.boundjid.resource)
-        xml.clear()
-        iq = self.Iq(stype='set')
+        iq = self.Iq()
+        iq['type'] = 'set'
+        iq.enable('bind')
         if self.boundjid.resource:
-            res = ET.Element('resource')
-            res.text = self.boundjid.resource
-            xml.append(res)
-        iq.append(xml)
+            iq['bind']['resource'] = self.boundjid.resource
         response = iq.send()
 
-        bind_ns = 'urn:ietf:params:xml:ns:xmpp-bind'
-        self.set_jid(response.xml.find('{%s}bind/{%s}jid' % (bind_ns,
-                                                             bind_ns)).text)
+        self.set_jid(response['bind']['jid'])
         self.bound = True
+
         log.info("Node set to: %s" % self.boundjid.full)
-        session_ns = 'urn:ietf:params:xml:ns:xmpp-session'
-        if "{%s}session" % session_ns not in self.features or self.bindfail:
+
+        if 'session' not in features['features']:
             log.debug("Established Session")
             self.sessionstarted = True
             self.session_started_event.set()
             self.event("session_start")
 
-    def _handle_start_session(self, xml):
+    def _handle_start_session(self, features):
         """
         Handle the start of the session.
 
         Arguments:
-            xml -- The session feature element.
+            feature -- The stream features element.
         """
-        if self.authenticated and self.bound:
-            iq = self.makeIqSet(xml)
-            response = iq.send()
-            log.debug("Established Session")
-            self.sessionstarted = True
-            self.session_started_event.set()
-            self.event("session_start")
-        else:
-            # Bind probably hasn't happened yet.
-            self.bindfail = True
+        iq = self.Iq()
+        iq['type'] = 'set'
+        iq.enable('session')
+        response = iq.send()
+
+        log.debug("Established Session")
+        self.sessionstarted = True
+        self.session_started_event.set()
+        self.event("session_start")
 
     def _handle_roster(self, iq, request=False):
         """
diff --git a/sleekxmpp/stanza/__init__.py b/sleekxmpp/stanza/__init__.py
index dbf7b86f..ef44dfb2 100644
--- a/sleekxmpp/stanza/__init__.py
+++ b/sleekxmpp/stanza/__init__.py
@@ -12,3 +12,6 @@ from sleekxmpp.stanza.stream_error import StreamError
 from sleekxmpp.stanza.iq import Iq
 from sleekxmpp.stanza.message import Message
 from sleekxmpp.stanza.presence import Presence
+from sleekxmpp.stanza.stream_features import StreamFeatures
+from sleekxmpp.stanza.bind import Bind
+
diff --git a/sleekxmpp/stanza/bind.py b/sleekxmpp/stanza/bind.py
new file mode 100644
index 00000000..ae1f96f0
--- /dev/null
+++ b/sleekxmpp/stanza/bind.py
@@ -0,0 +1,26 @@
+"""
+    SleekXMPP: The Sleek XMPP Library
+    Copyright (C) 2010  Nathanael C. Fritz
+    This file is part of SleekXMPP.
+
+    See the file LICENSE for copying permission.
+"""
+
+from sleekxmpp.stanza import Iq, StreamFeatures
+from sleekxmpp.xmlstream import ElementBase, ET, register_stanza_plugin
+
+
+class Bind(ElementBase):
+
+    """
+    """
+
+    name = 'bind'
+    namespace = 'urn:ietf:params:xml:ns:xmpp-bind'
+    interfaces = set(('resource', 'jid'))
+    sub_interfaces = interfaces
+    plugin_attrib = 'bind'
+
+
+register_stanza_plugin(Iq, Bind)
+register_stanza_plugin(StreamFeatures, Bind)
diff --git a/sleekxmpp/stanza/sasl.py b/sleekxmpp/stanza/sasl.py
new file mode 100644
index 00000000..e55a72ad
--- /dev/null
+++ b/sleekxmpp/stanza/sasl.py
@@ -0,0 +1,104 @@
+"""
+    SleekXMPP: The Sleek XMPP Library
+    Copyright (C) 2010  Nathanael C. Fritz
+    This file is part of SleekXMPP.
+
+    See the file LICENSE for copying permission.
+"""
+
+from sleekxmpp.stanza import StreamFeatures
+from sleekxmpp.xmlstream import ElementBase, StanzaBase, ET
+from sleekxmpp.xmlstream import register_stanza_plugin
+
+
+class Mechanisms(ElementBase):
+
+    """
+    """
+
+    name = 'mechanisms'
+    namespace = 'urn:ietf:params:xml:ns:xmpp-sasl'
+    interfaces = set(('mechanisms', 'required'))
+    plugin_attrib = name
+    is_extension = True
+
+    def get_required(self):
+        """
+        """
+        return True
+
+    def get_mechanisms(self):
+        """
+        """
+        results = []
+        mechs = self.findall('{%s}mechanism' % self.namespace)
+        if mechs:
+            for mech in mechs:
+                results.append(mech.text)
+        return results
+
+    def set_mechanisms(self, values):
+        """
+        """
+        self.del_mechanisms()
+        for val in values:
+           mech = ET.Element('{%s}mechanism' % self.namespace)
+           mech.text = val
+           self.append(mech)
+
+    def del_mechanisms(self):
+        """
+        """
+        mechs = self.findall('{%s}mechanism' % self.namespace)
+        if mechs:
+            for mech in mechs:
+                self.xml.remove(mech)
+
+
+class Success(StanzaBase):
+
+    """
+    """
+
+    name = 'success'
+    namespace = 'urn:ietf:params:xml:ns:xmpp-sasl'
+    interfaces = set()
+    plugin_attrib = name
+
+
+class Failure(StanzaBase):
+
+    """
+    """
+
+    name = 'failure'
+    namespace = 'urn:ietf:params:xml:ns:xmpp-sasl'
+    interfaces = set()
+    plugin_attrib = name
+
+
+class Auth(StanzaBase):
+
+    """
+    """
+
+    name = 'auth'
+    namespace = 'urn:ietf:params:xml:ns:xmpp-sasl'
+    interfaces = set(('mechanism', 'value'))
+    plugin_attrib = name
+
+    def setup(self, xml):
+        StanzaBase.setup(self, xml)
+        self.xml.tag = self.tag_name()
+
+    def set_value(self, value):
+        self.xml.text = value
+
+    def get_value(self):
+        return self.xml.text
+
+    def del_value(self):
+        self.xml.text = ''
+
+
+register_stanza_plugin(StreamFeatures, Mechanisms)
diff --git a/sleekxmpp/stanza/session.py b/sleekxmpp/stanza/session.py
new file mode 100644
index 00000000..b7b175d2
--- /dev/null
+++ b/sleekxmpp/stanza/session.py
@@ -0,0 +1,25 @@
+"""
+    SleekXMPP: The Sleek XMPP Library
+    Copyright (C) 2010  Nathanael C. Fritz
+    This file is part of SleekXMPP.
+
+    See the file LICENSE for copying permission.
+"""
+
+from sleekxmpp.stanza import Iq, StreamFeatures
+from sleekxmpp.xmlstream import ElementBase, ET, register_stanza_plugin
+
+
+class Session(ElementBase):
+
+    """
+    """
+
+    name = 'bind'
+    namespace = 'urn:ietf:params:xml:ns:xmpp-session'
+    interfaces = set()
+    plugin_attrib = 'session'
+
+
+register_stanza_plugin(Iq, Session)
+register_stanza_plugin(StreamFeatures, Session)
diff --git a/sleekxmpp/stanza/stream_features.py b/sleekxmpp/stanza/stream_features.py
new file mode 100644
index 00000000..5be2e55f
--- /dev/null
+++ b/sleekxmpp/stanza/stream_features.py
@@ -0,0 +1,52 @@
+"""
+    SleekXMPP: The Sleek XMPP Library
+    Copyright (C) 2010  Nathanael C. Fritz
+    This file is part of SleekXMPP.
+
+    See the file LICENSE for copying permission.
+"""
+
+from sleekxmpp.xmlstream import ElementBase, StanzaBase, ET
+from sleekxmpp.xmlstream import register_stanza_plugin
+
+
+class StreamFeatures(StanzaBase):
+
+    """
+    """
+
+    name = 'features'
+    namespace = 'http://etherx.jabber.org/streams'
+    interfaces = set(('features', 'required', 'optional'))
+    sub_interfaces = interfaces
+
+    def setup(self, xml):
+        StanzaBase.setup(self, xml)
+        self.values = self.values
+
+    def get_features(self):
+        """
+        """
+        return self.plugins
+
+    def set_features(self, value):
+        """
+        """
+        pass
+
+    def del_features(self):
+        """
+        """
+        pass
+
+    def get_required(self):
+        """
+        """
+        features = self['features']
+        return [f for n, f in features.items() if f['required']]
+
+    def get_optional(self):
+        """
+        """
+        features = self['features']
+        return [f for n, f in features.items() if not f['required']]
diff --git a/sleekxmpp/stanza/tls.py b/sleekxmpp/stanza/tls.py
new file mode 100644
index 00000000..d85f9b49
--- /dev/null
+++ b/sleekxmpp/stanza/tls.py
@@ -0,0 +1,50 @@
+"""
+    SleekXMPP: The Sleek XMPP Library
+    Copyright (C) 2010  Nathanael C. Fritz
+    This file is part of SleekXMPP.
+
+    See the file LICENSE for copying permission.
+"""
+
+from sleekxmpp.stanza import StreamFeatures
+from sleekxmpp.xmlstream import StanzaBase, ElementBase
+from sleekxmpp.xmlstream import register_stanza_plugin
+
+
+class STARTTLS(ElementBase):
+
+    """
+    """
+
+    name = 'starttls'
+    namespace = 'urn:ietf:params:xml:ns:xmpp-tls'
+    interfaces = set(('required',))
+    plugin_attrib = name
+
+    def get_required(self):
+        """
+        """
+        return True
+
+
+class Proceed(StanzaBase):
+
+    """
+    """
+
+    name = 'proceed'
+    namespace = 'urn:ietf:params:xml:ns:xmpp-tls'
+    interfaces = set()
+
+
+class Failure(StanzaBase):
+
+    """
+    """
+
+    name = 'failure'
+    namespace = 'urn:ietf:params:xml:ns:xmpp-tls'
+    interfaces = set()
+
+
+register_stanza_plugin(StreamFeatures, STARTTLS)
-- 
cgit v1.2.3