Ignore:
Timestamp:
May 9, 2012, 2:24:28 PM (10 years ago)
Author:
Ralph Meijer <ralphm@…>
Branch:
default
Message:

Clean up of RosterItem? and RosterClientProtocol?.

RosterItem:

  • Renamed attributes jid and ask to entity and pendingOut respectively.
  • Can represent roster items to be removed or that have been removed.
  • Now has fromElement and toElement methods.

RosterRequest is a new class to represent roster request stanzas.

RosterClientProtocol:

  • Roster returned from getRoster is now indexed by JIDs (instead of the unicode representation of the JID).
  • Outgoing requests are now done using RosterRequest.
  • onRosterSet and onRosterRemove are deprecated in favor of setReceived and removeReceived, respectively. These are called with a RosterRequest to have access to addressing and roster version information.

RosterPushIgnored can be raised to return a service-unavailable stanza
error for unwanted pushes.

This also fixes a problem with checking the sender address for roster pushes.

Author: ralphm.
Fixes: #71.

File:
1 edited

Legend:

Unmodified
Added
Removed
  • wokkel/xmppim.py

    r166 r172  
    88
    99This module provides generic implementations for the protocols defined in
    10 U{RFC 3921<http://xmpp.org/rfcs/rfc3921.html>} (XMPP IM).
    11 
    12 All of it should eventually move to Twisted.
     10U{RFC 6121<http://www.xmpp.org/rfcs/rfc6121.html>} (XMPP IM).
    1311"""
    1412
     13import warnings
     14
     15from twisted.internet import defer
     16from twisted.words.protocols.jabber import error
    1517from twisted.words.protocols.jabber.jid import JID
    1618from twisted.words.xish import domish
    1719
    18 from wokkel.compat import IQ
    19 from wokkel.generic import ErrorStanza, Stanza
     20from wokkel.generic import ErrorStanza, Stanza, Request
     21from wokkel.subprotocols import IQHandlerMixin
    2022from wokkel.subprotocols import XMPPHandler
    2123
    2224NS_XML = 'http://www.w3.org/XML/1998/namespace'
    2325NS_ROSTER = 'jabber:iq:roster'
     26
     27XPATH_ROSTER_SET = "/iq[@type='set']/query[@xmlns='%s']" % NS_ROSTER
     28
     29
    2430
    2531class Presence(domish.Element):
     
    606612    This represents one contact from an XMPP contact list known as roster.
    607613
    608     @ivar jid: The JID of the contact.
    609     @type jid: L{JID}
    610     @ivar name: The optional associated nickname for this contact.
     614    @ivar entity: The JID of the contact.
     615    @type entity: L{JID}
     616    @ivar name: The associated nickname for this contact.
    611617    @type name: C{unicode}
    612618    @ivar subscriptionTo: Subscription state to contact's presence. If C{True},
     
    617623                            contact is subscribed to the presence information
    618624                            of the roster owner.
    619     @type subscriptionTo: C{bool}
    620     @ivar ask: Whether subscription is pending.
    621     @type ask: C{bool}
     625    @type subscriptionFrom: C{bool}
     626    @ivar pendingOut: Whether the subscription request to this contact is
     627        pending.
     628    @type pendingOut: C{bool}
    622629    @ivar groups: Set of groups this contact is categorized in. Groups are
    623630                  represented by an opaque identifier of type C{unicode}.
    624631    @type groups: C{set}
    625     """
    626 
    627     def __init__(self, jid):
    628         self.jid = jid
    629         self.name = None
    630         self.subscriptionTo = False
    631         self.subscriptionFrom = False
    632         self.ask = None
    633         self.groups = set()
    634 
    635 
    636 class RosterClientProtocol(XMPPHandler):
     632    @ivar approved: Signals pre-approved subscription.
     633    @type approved: C{bool}
     634    @ivar remove: Signals roster item removal.
     635    @type remove: C{bool}
     636    """
     637
     638    __subscriptionStates = {(False, False): None,
     639                            (True, False): 'to',
     640                            (False, True): 'from',
     641                            (True, True): 'both'}
     642
     643    def __init__(self, entity, subscriptionTo=False, subscriptionFrom=False,
     644                       name=u'', groups=None):
     645        self.entity = entity
     646        self.subscriptionTo = subscriptionTo
     647        self.subscriptionFrom = subscriptionFrom
     648        self.name = name
     649        self.groups = groups or set()
     650
     651        self.pendingOut = False
     652        self.approved = False
     653        self.remove = False
     654
     655
     656    def __getJID(self):
     657        warnings.warn(
     658            "wokkel.xmppim.RosterItem.jid is deprecated. "
     659            "Use RosterItem.entity instead.",
     660            DeprecationWarning)
     661        return self.entity
     662
     663
     664    def __setJID(self, value):
     665        warnings.warn(
     666            "wokkel.xmppim.RosterItem.jid is deprecated. "
     667            "Use RosterItem.entity instead.",
     668            DeprecationWarning)
     669        self.entity = value
     670
     671
     672    jid = property(__getJID, __setJID, doc="""
     673            JID of the contact. Deprecated in favour of C{entity}.""")
     674
     675
     676    def __getAsk(self):
     677        warnings.warn(
     678            "wokkel.xmppim.RosterItem.ask is deprecated. "
     679            "Use RosterItem.pendingOut instead.",
     680            DeprecationWarning)
     681        return self.pendingOut
     682
     683
     684    def __setAsk(self, value):
     685        warnings.warn(
     686            "wokkel.xmppim.RosterItem.ask is deprecated. "
     687            "Use RosterItem.pendingOut instead.",
     688            DeprecationWarning)
     689        self.pendingOut = value
     690
     691
     692    ask = property(__getAsk, __setAsk, doc="""
     693            Pending out subscription. Deprecated in favour of C{pendingOut}.""")
     694
     695
     696    def toElement(self):
     697        element = domish.Element((NS_ROSTER, 'item'))
     698        element['jid'] = self.entity.full()
     699
     700        if self.remove:
     701            subscription = 'remove'
     702        else:
     703            subscription = self.__subscriptionStates[self.subscriptionTo,
     704                                                     self.subscriptionFrom]
     705
     706            if self.pendingOut:
     707                element['ask'] = u'subscribe'
     708
     709            if self.name:
     710                element['name'] = self.name
     711
     712            if self.approved:
     713                element['approved'] = u'true'
     714
     715            if self.groups:
     716                for group in self.groups:
     717                    element.addElement('group', content=group)
     718
     719        if subscription:
     720            element['subscription'] = subscription
     721
     722        return element
     723
     724
     725    @classmethod
     726    def fromElement(Class, element):
     727        entity = JID(element['jid'])
     728        item = Class(entity)
     729        subscription = element.getAttribute('subscription')
     730        if subscription == 'remove':
     731            item.remove = True
     732        else:
     733            item.name = element.getAttribute('name', u'')
     734            item.subscriptionTo = subscription in ('to', 'both')
     735            item.subscriptionFrom = subscription in ('from', 'both')
     736            item.pendingOut = element.getAttribute('ask') == 'subscribe'
     737            item.approved = element.getAttribute('approved') in ('true', '1')
     738            for subElement in domish.generateElementsQNamed(element.children,
     739                                                            'group', NS_ROSTER):
     740                item.groups.add(unicode(subElement))
     741        return item
     742
     743
     744
     745class RosterRequest(Request):
     746    """
     747    Roster request.
     748
     749    @ivar item: Roster item to be set or pushed.
     750    @type item: L{RosterItem}.
     751    """
     752    item = None
     753
     754    def parseRequest(self, element):
     755        for child in element.elements(NS_ROSTER, 'item'):
     756            self.item = RosterItem.fromElement(child)
     757            break
     758
     759
     760    def toElement(self):
     761        element = Request.toElement(self)
     762        query = element.addElement((NS_ROSTER, 'query'))
     763        if self.item:
     764            query.addChild(self.item.toElement())
     765        return element
     766
     767
     768
     769class RosterPushIgnored(Exception):
     770    """
     771    Raised when this entity doesn't want to accept/trust a roster push.
     772
     773    To avert presence leaks, a handler can raise L{RosterPushIgnored} when
     774    not accepting a roster push (directly or via Deferred). This will
     775    result in a C{'service-unavailable'} error being sent in return.
     776    """
     777
     778
     779
     780class RosterClientProtocol(XMPPHandler, IQHandlerMixin):
    637781    """
    638782    Client side XMPP roster protocol.
    639     """
     783
     784    The roster can be retrieved using L{getRoster}. Subsequent changes to the
     785    roster will be pushed, resulting in calls to L{setReceived} or
     786    L{removeReceived}. These methods should be overridden to handle the
     787    roster pushes.
     788
     789    RFC 6121 specifically allows entities other than a user's server to
     790    hold a roster for that user. However, how a client should deal with
     791    that is currently not yet specfied.
     792
     793    By default roster pushes from other source. I.e. when C{request.sender}
     794    is set but the sender's bare JID is different from the user's bare JID.
     795    Set L{allowAnySender} to allow roster pushes from any sender. To
     796    avert presence leaks, L{RosterPushIgnored} should then be raised for
     797    pushes from untrusted senders.
     798
     799    @cvar allowAnySender: Flag to allow roster pushes from any sender.
     800        C{False} by default.
     801    @type allowAnySender: C{boolean}
     802    """
     803
     804    allowAnySender = False
     805    iqHandlers = {XPATH_ROSTER_SET: "_onRosterSet"}
     806
    640807
    641808    def connectionInitialized(self):
    642         ROSTER_SET = "/iq[@type='set']/query[@xmlns='%s']" % NS_ROSTER
    643         self.xmlstream.addObserver(ROSTER_SET, self._onRosterSet)
    644 
    645     def _parseRosterItem(self, element):
    646         jid = JID(element['jid'])
    647         item = RosterItem(jid)
    648         item.name = element.getAttribute('name')
    649         subscription = element.getAttribute('subscription')
    650         item.subscriptionTo = subscription in ('to', 'both')
    651         item.subscriptionFrom = subscription in ('from', 'both')
    652         item.ask = element.getAttribute('ask') == 'subscribe'
    653         for subElement in domish.generateElementsQNamed(element.children,
    654                                                         'group', NS_ROSTER):
    655             item.groups.add(unicode(subElement))
    656 
    657         return item
     809        self.xmlstream.addObserver(XPATH_ROSTER_SET, self.handleRequest)
     810
    658811
    659812    def getRoster(self):
     
    669822            for element in domish.generateElementsQNamed(result.query.children,
    670823                                                         'item', NS_ROSTER):
    671                 item = self._parseRosterItem(element)
    672                 roster[item.jid.userhost()] = item
     824                item = RosterItem.fromElement(element)
     825                roster[item.entity] = item
    673826
    674827            return roster
    675828
    676         iq = IQ(self.xmlstream, 'get')
    677         iq.addElement((NS_ROSTER, 'query'))
    678         d = iq.send()
     829        request = RosterRequest(stanzaType='get')
     830        d = self.request(request)
    679831        d.addCallback(processRoster)
    680832        return d
     
    689841        @rtype: L{twisted.internet.defer.Deferred}
    690842        """
    691         iq = IQ(self.xmlstream, 'set')
    692         iq.addElement((NS_ROSTER, 'query'))
    693         item = iq.query.addElement('item')
    694         item['jid'] = entity.full()
    695         item['subscription'] = 'remove'
    696         return iq.send()
     843        request = RosterRequest(stanzaType='set')
     844        request.item = RosterItem(entity)
     845        request.item.remove = True
     846        return self.request(request)
    697847
    698848
    699849    def _onRosterSet(self, iq):
    700         if iq.handled or \
    701            iq.hasAttribute('from') and iq['from'] != self.xmlstream:
    702             return
    703 
    704         iq.handled = True
    705 
    706         itemElement = iq.query.item
    707 
    708         if unicode(itemElement['subscription']) == 'remove':
    709             self.onRosterRemove(JID(itemElement['jid']))
     850        def trapIgnored(failure):
     851            failure.trap(RosterPushIgnored)
     852            raise error.StanzaError('service-unavailable')
     853
     854        request = RosterRequest.fromElement(iq)
     855
     856        if (not self.allowAnySender and
     857                request.sender and
     858                request.sender.userhostJID() !=
     859                self.parent.jid.userhostJID()):
     860            d = defer.fail(RosterPushIgnored())
     861        elif request.item.remove:
     862            d = defer.maybeDeferred(self.removeReceived, request)
    710863        else:
    711             item = self._parseRosterItem(iq.query.item)
    712             self.onRosterSet(item)
    713 
    714     def onRosterSet(self, item):
     864            d = defer.maybeDeferred(self.setReceived, request)
     865        d.addErrback(trapIgnored)
     866        return d
     867
     868
     869    def setReceived(self, request):
    715870        """
    716871        Called when a roster push for a new or update item was received.
    717872
    718         @param item: The pushed roster item.
    719         @type item: L{RosterItem}
    720         """
    721 
    722     def onRosterRemove(self, entity):
     873        @param request: The push request.
     874        @type request: L{RosterRequest}
     875        """
     876        if hasattr(self, 'onRosterSet'):
     877            warnings.warn(
     878                "wokkel.xmppim.RosterClientProtocol.onRosterSet "
     879                "is deprecated. "
     880                "Use RosterClientProtocol.setReceived instead.",
     881                DeprecationWarning)
     882            return defer.maybeDeferred(self.onRosterSet, request.item)
     883
     884
     885    def removeReceived(self, request):
    723886        """
    724887        Called when a roster push for the removal of an item was received.
    725888
    726         @param entity: The entity for which the roster item has been removed.
    727         @type entity: L{JID}
    728         """
     889        @param request: The push request.
     890        @type request: L{RosterRequest}
     891        """
     892        if hasattr(self, 'onRosterRemove'):
     893            warnings.warn(
     894                "wokkel.xmppim.RosterClientProtocol.onRosterRemove "
     895                "is deprecated. "
     896                "Use RosterClientProtocol.removeReceived instead.",
     897                DeprecationWarning)
     898            return defer.maybeDeferred(self.onRosterRemove,
     899                                       request.item.entity)
    729900
    730901
Note: See TracChangeset for help on using the changeset viewer.