source: ralphm-patches/c2s_stanza_handlers.patch @ 66:b713f442b222

Last change on this file since 66:b713f442b222 was 66:b713f442b222, checked in by Ralph Meijer <ralphm@…>, 8 years ago

Add many tests, docstrings for authenticator, make example functional.

File size: 22.0 KB
  • new file doc/examples/client_service.tac

    # HG changeset patch
    # Parent 41c2620133080ea88612732ff211561c660cfdec
    Add c2s protocol handlers for iq, message and presence stanzas.
    
    TODO:
     * Add tests.
     * Add docstrings.
     * Save last unavailable presence for future probes.
    
    diff --git a/doc/examples/client_service.tac b/doc/examples/client_service.tac
    new file mode 100644
    - +  
     1from twisted.application import service, strports
     2from twisted.cred.portal import Portal
     3from twisted.cred.checkers import InMemoryUsernamePasswordDatabaseDontUse
     4from twisted.internet import defer
     5
     6from wokkel import client, xmppim
     7from wokkel.component import InternalComponent, Router
     8from wokkel.generic import FallbackHandler
     9from wokkel.ping import PingHandler
     10from wokkel.xmppim import RosterItem
     11
     12from twisted.words.protocols.jabber.jid import internJID as JID
     13
     14import socket
     15domain = socket.gethostname()
     16
     17RALPHM = JID('ralphm@'+domain)
     18INTOSI = JID('intosi@'+domain)
     19TERMIE = JID('termie@'+domain)
     20
     21roster = {
     22    'ralphm': {
     23        INTOSI: RosterItem(INTOSI,
     24                           subscriptionTo=True,
     25                           subscriptionFrom=True,
     26                           name='Intosi',
     27                           groups=set(['Friends'])),
     28        TERMIE: RosterItem(TERMIE,
     29                           subscriptionTo=True,
     30                           subscriptionFrom=True,
     31                           name='termie'),
     32        },
     33    'termie': {
     34        RALPHM: RosterItem(RALPHM,
     35                           subscriptionTo=True,
     36                           subscriptionFrom=True,
     37                           name='ralphm'),
     38        }
     39    }
     40
     41accounts = set(roster.keys())
     42
     43
     44class StaticRoster(xmppim.RosterServerProtocol):
     45
     46    def __init__(self, roster):
     47        xmppim.RosterServerProtocol.__init__(self)
     48        self.roster = roster
     49
     50    def getRoster(self, request):
     51        user = request.sender.user
     52        return defer.succeed(self.roster[user].values())
     53
     54
     55
     56application = service.Application("Jabber server")
     57
     58router = Router()
     59component = InternalComponent(router, domain)
     60component.setServiceParent(application)
     61
     62sessionManager = client.SessionManager(domain, accounts)
     63sessionManager.setHandlerParent(component)
     64
     65checker = InMemoryUsernamePasswordDatabaseDontUse(ralphm='secret',
     66                                                  termie='secret')
     67portal = Portal(sessionManager, (checker,))
     68portals = {JID(domain): portal}
     69
     70xmppim.AccountIQHandler(sessionManager).setHandlerParent(component)
     71xmppim.AccountMessageHandler(sessionManager).setHandlerParent(component)
     72xmppim.PresenceServerHandler(sessionManager, domain, roster).setHandlerParent(component)
     73FallbackHandler().setHandlerParent(component)
     74StaticRoster(roster).setHandlerParent(component)
     75PingHandler().setHandlerParent(component)
     76
     77c2sFactory = client.XMPPC2SServerFactory(portals)
     78c2sFactory.logTraffic = True
     79c2sService = strports.service('5224', c2sFactory)
     80c2sService.setServiceParent(application)
     81
     82sessionManager.connectionManager = c2sFactory
  • wokkel/test/test_xmppim.py

    diff --git a/wokkel/test/test_xmppim.py b/wokkel/test/test_xmppim.py
    a b  
    1313from twisted.words.xish import domish, utility
    1414
    1515from wokkel import xmppim
    16 from wokkel.generic import ErrorStanza, parseXml
     16from wokkel.generic import ErrorStanza, Stanza, parseXml
    1717from wokkel.test.helpers import TestableRequestHandlerMixin, XmlStreamStub
    1818
    1919NS_XML = 'http://www.w3.org/XML/1998/namespace'
     
    13331333
    13341334
    13351335
     1336class AccountIQHandlerTest(unittest.TestCase):
     1337    """
     1338    Tests for L{xmppim.AccountIQHandler}.
     1339    """
     1340
     1341    def setUp(self):
     1342        self.stub = XmlStreamStub()
     1343        self.protocol = xmppim.AccountIQHandler(None)
     1344        self.protocol.makeConnection(self.stub.xmlstream)
     1345        self.protocol.connectionInitialized()
     1346
     1347
     1348    def test_onIQNotUser(self):
     1349        """
     1350        IQs to JIDs without local part are ignored.
     1351        """
     1352        xml = """
     1353          <iq to='example.org'>
     1354            <query xmlns='jabber:iq:version'/>
     1355          </iq>
     1356        """
     1357
     1358        iq = parseXml(xml)
     1359        self.stub.send(iq)
     1360
     1361        self.assertFalse(getattr(iq, 'handled'))
     1362
     1363
     1364
     1365class AccountMessageHandlerTest(unittest.TestCase):
     1366    """
     1367    Tests for L{xmppim.AccountMessageHandler}.
     1368    """
     1369
     1370    def setUp(self):
     1371        self.stub = XmlStreamStub()
     1372        self.protocol = xmppim.AccountMessageHandler(None)
     1373        self.protocol.makeConnection(self.stub.xmlstream)
     1374        self.protocol.connectionInitialized()
     1375
     1376
     1377    def test_onMessageNotUser(self):
     1378        """
     1379        Messages to JIDs without local part are ignored.
     1380        """
     1381        xml = """
     1382          <message to='example.org'>
     1383            <body>Hello</body>
     1384          </message>
     1385        """
     1386
     1387        message = parseXml(xml)
     1388        self.stub.send(message)
     1389
     1390        self.assertFalse(getattr(message, 'handled'))
     1391
     1392
     1393
     1394class ClonePresenceTest(unittest.TestCase):
     1395    """
     1396    Tests for L{xmppim.clonePresence}.
     1397    """
     1398
     1399    def test_rootElement(self):
     1400        """
     1401        The copied presence stanza is not identical, but renders identically.
     1402        """
     1403        originalElement = domish.Element((None, 'presence'))
     1404        stanza = Stanza.fromElement(originalElement)
     1405        copyElement = xmppim.clonePresence(stanza)
     1406
     1407        self.assertNotIdentical(copyElement, originalElement)
     1408        self.assertEquals(copyElement.toXml(), originalElement.toXml())
     1409
     1410
     1411
    13361412class RosterServerProtocolTest(unittest.TestCase, TestableRequestHandlerMixin):
    13371413    """
    13381414    Tests for L{xmppim.RosterServerProtocol}.
  • wokkel/xmppim.py

    diff --git a/wokkel/xmppim.py b/wokkel/xmppim.py
    a b  
    1212
    1313import warnings
    1414
     15import copy
     16
    1517from twisted.internet import defer
     18from twisted.python import log
    1619from twisted.words.protocols.jabber import error
    1720from twisted.words.protocols.jabber.jid import JID
    1821from twisted.words.xish import domish
     
    408411
    409412
    410413
    411     def _onPresence(self, element):
    412         """
    413         Called when a presence stanza has been received.
    414         """
     414    def parsePresence(self, element):
    415415        stanza = Stanza.fromElement(element)
    416416
    417417        presenceType = stanza.stanzaType or 'available'
     
    421421        except KeyError:
    422422            return
    423423
    424         presence = parser.fromElement(element)
     424        return parser.fromElement(element)
     425
     426
     427    def _onPresence(self, element):
     428        """
     429        Called when a presence stanza has been received.
     430        """
     431        presence = self.parsePresence(element)
     432        presenceType = presence.stanzaType or 'available'
    425433
    426434        try:
    427435            handler = getattr(self, '%sReceived' % presenceType)
    428436        except AttributeError:
    429437            return
    430438        else:
    431             handler(presence)
     439            element.handled = handler(presence)
    432440
    433441
    434442
     
    10671075
    10681076
    10691077
     1078class AccountIQHandler(XMPPHandler):
     1079
     1080    def __init__(self, sessionManager):
     1081        XMPPHandler.__init__(self)
     1082        self.sessionManager = sessionManager
     1083
     1084
     1085    def connectionMade(self):
     1086        self.xmlstream.addObserver('/iq', self.onIQ, 1)
     1087
     1088
     1089    def onIQ(self, iq):
     1090        """
     1091        Handler for iq stanzas to user accounts' connected resources.
     1092
     1093        If the recipient is a bare JID or there is no associated user, this
     1094        handler ignores the stanza, so that other handlers have a chance
     1095        to pick it up. If used, L{generic.FallbackHandler} will respond with a
     1096        C{'service-unavailable'} stanza error if no other handlers handle
     1097        the iq.
     1098        """
     1099
     1100        if iq.handled:
     1101            return
     1102
     1103        try:
     1104            recipient = JID(iq['to'])
     1105        except KeyError:
     1106            return
     1107
     1108        if not recipient.user:
     1109            # This is not for an account, ignore it
     1110            return
     1111        elif recipient.user not in self.sessionManager.accounts:
     1112            # This is not a user, ignore it
     1113            return
     1114        elif not recipient.resource:
     1115            # Bare JID at local domain, ignore it
     1116            return
     1117
     1118        userSessions = self.sessionManager.sessions.get(recipient.user,
     1119                                                        {})
     1120        if recipient.resource in userSessions:
     1121            self.sessionManager.deliverStanza(iq, recipient)
     1122        else:
     1123            # Full JID without connected resource, return error
     1124            exc = error.StanzaError('service-unavailable')
     1125            if iq['type'] in ('result', 'error'):
     1126                log.err(exc, 'Could not deliver IQ response')
     1127            else:
     1128                self.send(exc.toResponse(iq))
     1129
     1130        iq.handled = True
     1131
     1132
     1133
     1134class AccountMessageHandler(XMPPHandler):
     1135
     1136    def __init__(self, sessionManager):
     1137        XMPPHandler.__init__(self)
     1138        self.sessionManager = sessionManager
     1139
     1140
     1141    def connectionMade(self):
     1142        self.xmlstream.addObserver('/message', self.onMessage, 1)
     1143
     1144
     1145    def onMessage(self, message):
     1146        """
     1147        Handler for message stanzas to user accounts.
     1148        """
     1149
     1150        if message.handled:
     1151            return
     1152
     1153        try:
     1154            recipient = JID(message['to'])
     1155        except KeyError:
     1156            return
     1157
     1158        stanzaType = message.getAttribute('type', 'normal')
     1159
     1160        try:
     1161            if not recipient.user:
     1162                # This is not for an account, ignore it
     1163                return
     1164            elif recipient.user not in self.sessionManager.accounts:
     1165                # This is not a user, ignore it
     1166                return
     1167            elif recipient.resource:
     1168                userSessions = self.sessionManager.sessions.get(recipient.user,
     1169                                                                {})
     1170                if recipient.resource in userSessions:
     1171                    self.sessionManager.deliverStanza(message, recipient)
     1172                else:
     1173                    if stanzaType in ('normal', 'chat', 'headline'):
     1174                        self.onMessageBareJID(message, recipient.userhostJID())
     1175                    elif stanzaType == 'error':
     1176                        log.msg("Dropping message to unconnected resource %r" %
     1177                                recipient.full())
     1178                    elif stanzaType == 'groupchat':
     1179                        raise error.StanzaError('service-unavailable')
     1180            else:
     1181                self.onMessageBareJID(message, recipient)
     1182        except error.StanzaError, exc:
     1183            if stanzaType == 'error':
     1184                log.err(exc, "Undeliverable error")
     1185            else:
     1186                self.send(exc.toResponse(message))
     1187
     1188        message.handled = True
     1189
     1190
     1191    def onMessageBareJID(self, message, bareJID):
     1192        stanzaType = message.getAttribute('type', 'normal')
     1193
     1194        userSessions = self.sessionManager.sessions.get(bareJID.user, {})
     1195
     1196        recipients = set()
     1197
     1198        if stanzaType == 'headline':
     1199            for session in userSessions:
     1200                if session.presence.priority >= 0:
     1201                    recipients.add(session.entity)
     1202        elif stanzaType in ('chat', 'normal'):
     1203            priorities = {}
     1204            for session in userSessions.itervalues():
     1205                if not session.presence or not session.presence.available:
     1206                    continue
     1207                priority = session.presence.priority
     1208                if priority >= 0:
     1209                    priorities.setdefault(priority, set()).add(session.entity)
     1210                maxPriority = max(priorities.keys())
     1211                recipients.update(priorities[maxPriority])
     1212        elif stanzaType == 'groupchat':
     1213            raise error.StanzaError('service-unavailable')
     1214
     1215        if recipients:
     1216            for recipient in recipients:
     1217                self.sessionManager.deliverStanza(message, recipient)
     1218        elif stanzaType in ('chat', 'normal'):
     1219            raise error.StanzaError('service-unavailable')
     1220        else:
     1221            # silently discard
     1222            log.msg("Discarding message to %r" % message['to'])
     1223
     1224
     1225
     1226
     1227def clonePresence(presence):
     1228    """
     1229    Make a deep copy of a presence stanza.
     1230
     1231    The returned presence stanza is an orphaned deep copy of the given
     1232    original.
     1233
     1234    @note: Since the reference to the original parent, if any, is gone,
     1235    inherited attributes like C{xml:lang} are not preserved.
     1236    """
     1237    element = presence.element
     1238
     1239    parent = element.parent
     1240    element.parent = None
     1241    newElement = copy.deepcopy(element)
     1242    element.parent = parent
     1243    return newElement
     1244
     1245
     1246
     1247class PresenceServerHandler(PresenceProtocol):
     1248
     1249    def __init__(self, sessionManager, domain, roster):
     1250        PresenceProtocol.__init__(self)
     1251        self.sessionManager = sessionManager
     1252        self.domain = domain
     1253        self.roster = roster
     1254        self.presences = {} # user -> resource -> presence
     1255        self.offlinePresences = {} # user -> presence
     1256        self.remotePresences = {} # user -> remote entity -> presence
     1257
     1258        self.sessionManager.clientStream.addObserver('/presence',
     1259                                                     self._onPresenceOutbound)
     1260
     1261
     1262    def _onPresenceOutbound(self, element):
     1263        log.msg("Got outbound presence: %r" % element.toXml())
     1264        presence = self.parsePresence(element)
     1265
     1266        presenceType = presence.stanzaType or 'available'
     1267        method = '%sReceivedOutbound' % presenceType
     1268        print method
     1269
     1270        try:
     1271            handler = getattr(self, method)
     1272        except AttributeError:
     1273            return
     1274        else:
     1275            element.handled = handler(presence)
     1276
     1277
     1278    def _broadcastToOtherResources(self, presence):
     1279        """
     1280        Broadcast presence to other available resources.
     1281        """
     1282        fromJID = presence.sender
     1283        for otherResource in self.presences[fromJID.user]:
     1284            if otherResource == fromJID.resource:
     1285                continue
     1286
     1287            resourceJID = JID(tuple=(fromJID.user,
     1288                                     fromJID.host,
     1289                                     otherResource))
     1290            outPresence = clonePresence(presence)
     1291            outPresence['to'] = resourceJID.full()
     1292            self.sessionManager.deliverStanza(outPresence, resourceJID)
     1293
     1294
     1295    def _broadcastToContacts(self, presence):
     1296        """
     1297        Broadcast presence to subscribed entities.
     1298        """
     1299        fromJID = presence.sender
     1300        roster = self.roster[fromJID.user]
     1301
     1302        for item in roster.itervalues():
     1303            if not item.subscriptionFrom:
     1304                continue
     1305
     1306            outPresence = clonePresence(presence)
     1307            outPresence['to'] = item.entity.full()
     1308
     1309            if item.entity.host == self.domain:
     1310                # local contact
     1311                if item.entity.user in self.presences:
     1312                    # broadcast to contact's available resources
     1313                    for itemResource in self.presences[item.entity.user]:
     1314                        resourceJID = JID(tuple=(item.entity.user,
     1315                                                 item.entity.host,
     1316                                                 itemResource))
     1317                        self.sessionManager.deliverStanza(outPresence,
     1318                                                          resourceJID)
     1319            else:
     1320                # remote contact
     1321                self.send(outPresence)
     1322
     1323
     1324    def _on_availableBroadcast(self, presence):
     1325        fromJID = presence.sender
     1326        user, resource = fromJID.user, fromJID.resource
     1327        roster = self.roster[user]
     1328
     1329        if user not in self.presences:
     1330            # initial presence
     1331            self.presences[user] = {}
     1332            self.remotePresences[user] = {}
     1333
     1334            # send out probes
     1335            for item in roster.itervalues():
     1336                if item.subscriptionTo and item.entity.host != self.domain:
     1337                    self.probe(item.entity, fromJID)
     1338        else:
     1339            if resource not in self.presences[user]:
     1340                # initial presence with another available resource
     1341
     1342                # send last known presences from remote contacts
     1343                remotePresences = self.remotePresences[user]
     1344                for entity, remotePresence in remotePresences.iteritems():
     1345                    self.sessionManager.deliverStanza(remotePresence.element,
     1346                                                      fromJID)
     1347
     1348            # send presence to other resources
     1349            self._broadcastToOtherResources(presence)
     1350
     1351        # Send last known local presences
     1352        if user not in self.presences or resource not in self.presences[user]:
     1353            for item in roster.itervalues():
     1354                if item.subscriptionTo and \
     1355                   item.entity.host == self.domain and \
     1356                   item.entity.user in self.presences:
     1357                    for contactPresence in \
     1358                            self.presences[item.entity.user].itervalues():
     1359                        outPresence = clonePresence(contactPresence)
     1360                        outPresence['to'] = fromJID.userhost()
     1361                        self.sessionManager.deliverStanza(outPresence, fromJID)
     1362
     1363        # broadcast presence
     1364        self._broadcastToContacts(presence)
     1365
     1366        # save presence
     1367        self.presences[user][resource] = presence
     1368        self.sessionManager.sessions[user][resource].presence = presence
     1369
     1370        return True
     1371
     1372
     1373    def _on_availableDirected(self, presence):
     1374        self.send(presence.element)
     1375        return True
     1376
     1377
     1378    def availableReceivedOutbound(self, presence):
     1379        if presence.recipient:
     1380            return self._on_availableDirected(presence)
     1381        else:
     1382            return self._on_availableBroadcast(presence)
     1383
     1384
     1385    def availableReceived(self, presence):
     1386        fromJID = presence.sender
     1387        toJID = presence.recipient
     1388
     1389        if toJID.user not in self.roster:
     1390            return False
     1391
     1392        if toJID.user in self.presences:
     1393            for resource in self.presences[toJID.user]:
     1394                resourceJID = JID(tuple=(toJID.user,
     1395                                         toJID.host,
     1396                                         resource))
     1397                self.sessionManager.deliverStanza(presence.element, resourceJID)
     1398            self.remotePresences[toJID.user][fromJID] = presence
     1399        else:
     1400            # no such user or no available resource, ignore this stanza
     1401            pass
     1402
     1403        return True
     1404
     1405
     1406    def _on_unavailableBroadcast(self, presence):
     1407        fromJID = presence.sender
     1408        user, resource = fromJID.user, fromJID.resource
     1409
     1410        # broadcast presence
     1411        self._broadcastToContacts(presence)
     1412
     1413        if user in self.presences:
     1414            # send presence to other resources
     1415            self._broadcastToOtherResources(presence)
     1416
     1417            # update stored presences
     1418            if resource in self.presences[user]:
     1419                del self.presences[user][resource]
     1420
     1421            if not self.presences[user]:
     1422                # last resource to become unavailable
     1423                del self.presences[user]
     1424
     1425                # TODO: save last unavailable presence
     1426
     1427        return True
     1428
     1429
     1430    def _on_unavailableDirected(self, presence):
     1431        self.send(presence.element)
     1432        return True
     1433
     1434
     1435    def unavailableReceivedOutbound(self, presence):
     1436        if presence.recipient:
     1437            return self._on_unavailableDirected(presence)
     1438        else:
     1439            return self._on_unavailableBroadcast(presence)
     1440
     1441#    def unavailableReceived(self, presence):
     1442
     1443
     1444    def subscribedReceivedOutbound(self, presence):
     1445        log.msg("%r subscribed %s to its presence" % (presence.sender,
     1446                                                      presence.recipient))
     1447        self.send(presence.element)
     1448        return True
     1449
     1450
     1451    def subscribedReceived(self, presence):
     1452        log.msg("%r subscribed %s to its presence" % (presence.sender,
     1453                                                      presence.recipient))
     1454
     1455
     1456    def unsubscribedReceivedOutbound(self, presence):
     1457        log.msg("%r unsubscribed %s from its presence" % (presence.sender,
     1458                                                          presence.recipient))
     1459        self.send(presence.element)
     1460        return True
     1461
     1462
     1463    def unsubscribedReceived(self, presence):
     1464        log.msg("%r unsubscribed %s from its presence" % (presence.sender,
     1465                                                          presence.recipient))
     1466
     1467
     1468    def subscribeReceivedOutbound(self, presence):
     1469        log.msg("%r requests subscription to %s" % (presence.sender,
     1470                                                    presence.recipient))
     1471        self.send(presence.element)
     1472        return True
     1473
     1474
     1475    def subscribeReceived(self, presence):
     1476        log.msg("%r requests subscription to %s" % (presence.sender,
     1477                                                    presence.recipient))
     1478
     1479
     1480    def unsubscribeReceivedOutbound(self, presence):
     1481        log.msg("%r requests unsubscription from %s" % (presence.sender,
     1482                                                        presence.recipient))
     1483        self.send(presence.element)
     1484        return True
     1485
     1486
     1487    def unsubscribeReceived(self, presence):
     1488        log.msg("%r requests unsubscription from %s" % (presence.sender,
     1489                                                        presence.recipient))
     1490
     1491
     1492    def probeReceived(self, presence):
     1493        fromJID = presence.sender
     1494        toJID = presence.recipient
     1495
     1496        if toJID.user not in self.roster or \
     1497           fromJID.userhost() not in self.roster[toJID.user] or \
     1498           not self.roster[toJID.user][fromJID.userhost()].subscriptionFrom:
     1499            # send unsubscribed
     1500            pass
     1501        elif toJID.user not in self.presences:
     1502            # send last unavailable or nothing
     1503            pass
     1504        else:
     1505            for resourcePresence in self.presences[toJID.user].itervalues():
     1506                outPresence = clonePresence(resourcePresence)
     1507                outPresence['to'] = fromJID.userhost()
     1508                self.send(outPresence)
     1509
     1510
     1511
    10701512class RosterServerProtocol(XMPPHandler, IQHandlerMixin):
    10711513    """
    10721514    XMPP subprotocol handler for the roster, server side.
Note: See TracBrowser for help on using the repository browser.