Changeset 72:727b4d29c48e in ralphm-patches for session_manager.patch
- Timestamp:
- Jan 27, 2013, 10:40:32 PM (10 years ago)
- Branch:
- default
- File:
-
- 1 edited
Legend:
- Unmodified
- Added
- Removed
-
session_manager.patch
r70 r72 1 1 # HG changeset patch 2 # Parent bc450d2e7ed710c5605545e39bb6a054c368571f2 # Parent fdef0cff7a57368fa21984593ef05e616039e2e2 3 3 4 diff --git a/wokkel/client.py b/wokkel/client.py 5 --- a/wokkel/client.py 6 +++ b/wokkel/client.py 7 @@ -12,19 +12,23 @@ 8 9 import base64 10 11 +from zope.interface import implements 12 + 13 from twisted.application import service 14 -from twisted.cred import credentials, error as ecred 15 +from twisted.cred import credentials, error as ecred, portal 16 from twisted.internet import defer, reactor 17 -from twisted.python import log 4 diff --git a/wokkel/ewokkel.py b/wokkel/ewokkel.py 5 new file mode 100644 6 --- /dev/null 7 +++ b/wokkel/ewokkel.py 8 @@ -0,0 +1,32 @@ 9 +# Copyright (c) Ralph Meijer. 10 +# See LICENSE for details. 11 + 12 +""" 13 +Exceptions for Wokkel. 14 +""" 15 + 16 + 17 +class WokkelError(Exception): 18 + """ 19 + Base exception for Wokkel. 20 + """ 21 + 22 +class NoSuchContact(WokkelError): 23 + """ 24 + Raised when the given contact is not present in the user's roster. 25 + """ 26 + 27 +class NotSubscribed(WokkelError): 28 + """ 29 + Raised when the contact does not have a presence subscription to the user. 30 + """ 31 + 32 +class NoSuchResource(WokkelError): 33 + """ 34 + Raised when the given resource is currently not connected. 35 + """ 36 + 37 +class NoSuchUser(WokkelError): 38 + """ 39 + Raised when there is no user with the given name or JID. 40 + """ 41 diff --git a/wokkel/generic.py b/wokkel/generic.py 42 --- a/wokkel/generic.py 43 +++ b/wokkel/generic.py 44 @@ -7,6 +7,8 @@ 45 Generic XMPP protocol helpers. 46 """ 47 48 +import copy 49 + 50 from zope.interface import implements 51 52 from twisted.internet import defer, protocol 53 @@ -66,6 +68,24 @@ 54 return rootElement 55 56 57 +def cloneElement(element): 58 + """ 59 + Make a deep copy of a serialized element. 60 + 61 + The returned element is an orphaned deep copy of the given original. 62 + 63 + @note: Since the reference to the original parent, if any, is gone, 64 + inherited attributes like C{xml:lang} are not preserved. 65 + 66 + @type element: L{domish.Element}. 67 + """ 68 + parent = element.parent 69 + element.parent = None 70 + clone = copy.deepcopy(element) 71 + element.parent = parent 72 + return clone 73 + 74 + 75 76 class FallbackHandler(XMPPHandler): 77 """ 78 @@ -168,10 +188,23 @@ 79 """ 80 Abstract representation of a stanza. 81 82 + @ivar recipient: The receiving entity. 83 + @type recipient: L{jid.JID} 84 + 85 @ivar sender: The sending entity. 86 @type sender: L{jid.JID} 87 - @ivar recipient: The receiving entity. 88 - @type recipient: L{jid.JID} 89 + 90 + @ivar stanzaKind: One of C{'message'}, C{'presence'}, C{'iq'}. 91 + @type stanzaKind: L{unicode}. 92 + 93 + @ivar stanzaID: The optional stanza identifier. 94 + @type stanzaID: L{unicode}. 95 + 96 + @ivar stanzaType: The optional stanza type. 97 + @type stanzaType: L{unicode}. 98 + 99 + @ivar element: The serialized XML of this stanza. 100 + @type element: L{domish.Element}. 101 """ 102 103 recipient = None 104 @@ -179,6 +212,8 @@ 105 stanzaKind = None 106 stanzaID = None 107 stanzaType = None 108 + element = None 109 + 110 111 def __init__(self, recipient=None, sender=None): 112 self.recipient = recipient 113 @@ -217,6 +252,7 @@ 114 self.sender = jid.internJID(element['from']) 115 if element.hasAttribute('to'): 116 self.recipient = jid.internJID(element['to']) 117 + self.stanzaKind = element.name 118 self.stanzaType = element.getAttribute('type') 119 self.stanzaID = element.getAttribute('id') 120 121 @@ -242,6 +278,7 @@ 122 123 def toElement(self): 124 element = domish.Element((None, self.stanzaKind)) 125 + self.element = element 126 if self.sender is not None: 127 element['from'] = self.sender.full() 128 if self.recipient is not None: 129 diff --git a/wokkel/iwokkel.py b/wokkel/iwokkel.py 130 --- a/wokkel/iwokkel.py 131 +++ b/wokkel/iwokkel.py 132 @@ -996,6 +996,14 @@ 133 """) 134 135 136 + interested = Attribute( 137 + """ 138 + This session represents a I{interested resource}, i.e. the user's 139 + roster has been requested and roster pushes will be sent out to 140 + this session. 141 + """) 142 + 143 + 144 def loggedIn(realm, mind): 145 """ 146 Called by the realm when login occurs. 147 diff --git a/wokkel/test/test_generic.py b/wokkel/test/test_generic.py 148 --- a/wokkel/test/test_generic.py 149 +++ b/wokkel/test/test_generic.py 150 @@ -24,6 +24,27 @@ 151 152 NS_VERSION = 'jabber:iq:version' 153 154 +class CloneElementTest(unittest.TestCase): 155 + """ 156 + Tests for L{xmppim.clonePresence}. 157 + """ 158 + 159 + def test_rootElement(self): 160 + """ 161 + The copied presence stanza is not identical, but renders identically. 162 + """ 163 + parent = object() 164 + originalElement = domish.Element((None, 'presence')) 165 + originalElement.parent = parent 166 + copyElement = generic.cloneElement(originalElement) 167 + 168 + self.assertNotIdentical(copyElement, originalElement) 169 + self.assertEqual(copyElement.toXml(), originalElement.toXml()) 170 + self.assertIdentical(None, copyElement.parent) 171 + self.assertIdentical(parent, originalElement.parent) 172 + 173 + 174 + 175 class VersionHandlerTest(unittest.TestCase): 176 """ 177 Tests for L{wokkel.generic.VersionHandler}. 178 @@ -110,10 +131,21 @@ 179 <message type='chat' from='other@example.org' to='user@example.org'/> 180 """ 181 182 - stanza = generic.Stanza.fromElement(generic.parseXml(xml)) 183 + element = generic.parseXml(xml) 184 + stanza = generic.Stanza.fromElement(element) 185 self.assertEqual('chat', stanza.stanzaType) 186 self.assertEqual(JID('other@example.org'), stanza.sender) 187 self.assertEqual(JID('user@example.org'), stanza.recipient) 188 + self.assertIdentical(element, stanza.element) 189 + 190 + 191 + def test_fromElementStanzaKind(self): 192 + """ 193 + The stanza kind is also recorded in the stanza. 194 + """ 195 + xml = """<presence/>""" 196 + stanza = generic.Stanza.fromElement(generic.parseXml(xml)) 197 + self.assertEqual(u'presence', stanza.stanzaKind) 198 199 200 def test_fromElementChildParser(self): 201 diff --git a/wokkel/test/test_xmppim.py b/wokkel/test/test_xmppim.py 202 --- a/wokkel/test/test_xmppim.py 203 +++ b/wokkel/test/test_xmppim.py 204 @@ -5,6 +5,10 @@ 205 Tests for L{wokkel.xmppim}. 206 """ 207 208 +from zope.interface import verify 209 + 210 +from twisted.cred import checkers, error as ecred 211 +from twisted.cred.portal import IRealm 212 from twisted.internet import defer 213 from twisted.trial import unittest 214 from twisted.words.protocols.jabber import error 215 @@ -12,9 +16,10 @@ 216 from twisted.words.protocols.jabber.xmlstream import toResponse 217 from twisted.words.xish import domish, utility 218 219 -from wokkel import xmppim 220 +from wokkel import ewokkel, component, xmppim 221 from wokkel.generic import ErrorStanza, parseXml 222 from wokkel.test.helpers import TestableRequestHandlerMixin, XmlStreamStub 223 +from wokkel.subprotocols import IQHandlerMixin 224 225 NS_XML = 'http://www.w3.org/XML/1998/namespace' 226 NS_ROSTER = 'jabber:iq:roster' 227 @@ -99,6 +104,7 @@ 228 self.assertEquals(50, presence.priority) 229 230 231 + 232 class PresenceProtocolTest(unittest.TestCase): 233 """ 234 Tests for L{xmppim.PresenceProtocol} 235 @@ -1418,6 +1424,112 @@ 236 237 238 239 +class InMemoryRosterTest(unittest.TestCase): 240 + """ 241 + Tests for L{xmppim.InMemoryRoster}. 242 + """ 243 + 244 + def setUp(self): 245 + contacts = [ 246 + xmppim.RosterItem(JID('contact1@example.org'), 247 + subscriptionFrom=True, 248 + subscriptionTo=False), 249 + xmppim.RosterItem(JID('contact2@example.org'), 250 + subscriptionFrom=False, 251 + subscriptionTo=True), 252 + ] 253 + self.roster = xmppim.InMemoryRoster(contacts) 254 + 255 + 256 + def test_getSubscribers(self): 257 + def gotSubscribers(subscribers): 258 + subscribers = list(subscribers) 259 + self.assertIn(JID('contact1@example.org'), subscribers) 260 + self.assertNotIn(JID('contact2@example.org'), subscribers) 261 + 262 + 263 + d = self.roster.getSubscribers() 264 + d.addCallback(gotSubscribers) 265 + return d 266 + 267 + 268 + def test_getSubscriptions(self): 269 + def gotSubscriptions(subscriptions): 270 + subscriptions = list(subscriptions) 271 + self.assertNotIn(JID('contact1@example.org'), subscriptions) 272 + self.assertIn(JID('contact2@example.org'), subscriptions) 273 + 274 + d = self.roster.getSubscriptions() 275 + d.addCallback(gotSubscriptions) 276 + return d 277 + 278 + 279 +class UserRosterProtocolTest(unittest.TestCase): 280 + """ 281 + Tests for L{xmppim.UserRosterProtocol}. 282 + """ 283 + 284 + def setUp(self): 285 + self.stub = XmlStreamStub() 286 + self.service = xmppim.UserRosterProtocol() 287 + self.service.makeConnection(self.stub.xmlstream) 288 + 289 + entity = JID(u'user@example.org') 290 + user = xmppim.User(entity) 291 + 292 + contact = xmppim.RosterItem(JID(u'contact@example.org')) 293 + user.roster = xmppim.InMemoryRoster([contact]) 294 + 295 + self.session = xmppim.UserSession(user) 296 + self.stub.xmlstream.avatar = self.session 297 + 298 + 299 + def test_getRoster(self): 300 + """ 301 + The returned roster is gotten from the session user. 302 + """ 303 + def gotRoster(result): 304 + self.assertIn(JID(u'contact@example.org'), result) 305 + 306 + request = xmppim.RosterRequest() 307 + d = self.service.getRoster(request) 308 + d.addCallback(gotRoster) 309 + return d 310 + 311 + 312 + def test_getRosterInterested(self): 313 + """ 314 + Requesting the roster marks the session as interested in roster pushes. 315 + """ 316 + def gotRoster(result): 317 + self.assertTrue(self.session.interested) 318 + 319 + request = xmppim.RosterRequest() 320 + d = self.service.getRoster(request) 321 + d.addCallback(gotRoster) 322 + return d 323 + 324 + 325 + def test_handleRequestLocal(self): 326 + """ 327 + Handle requests without recipient (= local server). 328 + """ 329 + called = [] 330 + request = xmppim.RosterRequest(recipient=None) 331 + self.patch(IQHandlerMixin, 'handleRequest', called.append) 332 + self.service.handleRequest(request.toElement()) 333 + self.assertTrue(called) 334 + 335 + 336 + def test_handleRequestOther(self): 337 + """ 338 + If the request has a non-empty recipient, ignore this request. 339 + """ 340 + request = xmppim.RosterRequest(recipient=JID('other.example.org')) 341 + self.assertFalse(self.service.handleRequest(request.toElement())) 342 + 343 + 344 + 345 class MessageTest(unittest.TestCase): 346 """ 347 Tests for L{xmppim.Message}. 348 @@ -1598,3 +1710,297 @@ 349 "was deprecated in Wokkel 0.8.0; " 350 "please use MessageProtocol.messageReceived instead.", 351 warnings[0]['message']) 352 + 353 + 354 + 355 +class UserSessionTest(unittest.TestCase): 356 + """ 357 + Tests for L{xmppim.UserSession}. 358 + """ 359 + 360 + def setUp(self): 361 + self.session = xmppim.UserSession(None) 362 + 363 + 364 + def test_interface(self): 365 + """ 366 + UserSession implements IUserSession. 367 + """ 368 + verify.verifyObject(xmppim.IUserSession, self.session) 369 + 370 + 371 +class UserTest(unittest.TestCase): 372 + """ 373 + Tests for L{xmppim.User}. 374 + """ 375 + 376 + def setUp(self): 377 + self.user = xmppim.User(JID('user@example.org'), 378 + xmppim.InMemoryRoster([])) 379 + self.session = xmppim.UserSession(self.user) 380 + self.session.bindResource('Home') 381 + 382 + 383 + @defer.inlineCallbacks 384 + def test_getPresences(self): 385 + """ 386 + A contact with a subscription to the user gets its presence. 387 + """ 388 + contact = xmppim.RosterItem(JID('contact@example.org'), 389 + subscriptionFrom=True) 390 + self.user.roster.roster[contact.entity] = contact 391 + 392 + presence = xmppim.AvailabilityPresence() 393 + self.session.presence = presence 394 + 395 + presences = yield self.user.getPresences(JID('contact@example.org')) 396 + self.assertEqual([presence], list(presences)) 397 + 398 + defer.returnValue(None) 399 + 400 + 401 + def test_getPresencesNoSubscription(self): 402 + """ 403 + A contact without a subscription raises NotSubscribed. 404 + """ 405 + contact = xmppim.RosterItem(JID('contact@example.org'), 406 + subscriptionFrom=False) 407 + self.user.roster.roster[contact.entity] = contact 408 + 409 + presence = xmppim.AvailabilityPresence() 410 + self.session.presence = presence 411 + 412 + d = self.user.getPresences(JID('contact@example.org')) 413 + self.assertFailure(d, ewokkel.NotSubscribed) 414 + return d 415 + 416 + 417 + def test_getPresencesUnknown(self): 418 + """ 419 + A contact with a subscription to the user gets its presence. 420 + """ 421 + presence = xmppim.AvailabilityPresence() 422 + self.session.presence = presence 423 + d = self.user.getPresences(JID('unknown@example.org')) 424 + self.assertFailure(d, ewokkel.NoSuchContact) 425 + return d 426 + 427 + 428 +class AnonymousRealmTest(unittest.TestCase): 429 + """ 430 + Tests for L{xmppim.AnonymousRealm}. 431 + """ 432 + 433 + def setUp(self): 434 + self.realm = xmppim.AnonymousRealm('example.org') 435 + 436 + 437 + def test_interface(self): 438 + """ 439 + AnonymousRealm implements IRealm. 440 + """ 441 + verify.verifyObject(IRealm, self.realm) 442 + 443 + 444 + def test_requestAvatarAnonymous(self): 445 + """ 446 + An anonymous avatar ID yields a generated JID. 447 + """ 448 + def gotAvatar(result): 449 + def gotUser(user): 450 + self.assertIdentical(user, avatar.user) 451 + 452 + iface, avatar, logout = result 453 + self.assertNotIdentical(None, avatar.user) 454 + self.assertNotIdentical(None, avatar.user.entity) 455 + self.assertNotIdentical(None, avatar.user.entity.host) 456 + self.assertEqual(u'example.org', avatar.user.entity.host) 457 + 458 + d = self.realm.lookupUser(avatar.user.entity) 459 + d.addCallback(gotUser) 460 + return d 461 + 462 + avatarID = checkers.ANONYMOUS 463 + d = self.realm.requestAvatar(avatarID, None, xmppim.IUserSession) 464 + d.addCallback(gotAvatar) 465 + return d 466 + 467 + 468 + def test_requestAvatarAnonymousDifferent(self): 469 + """ 470 + Requesting two anonymous avatar IDs yields different JIDs. 471 + """ 472 + def gotAvatar1(result): 473 + iface, avatar1, logout = result 474 + 475 + def gotAvatar2(result): 476 + iface, avatar2, logout = result 477 + self.assertNotIdentical(avatar1, avatar2) 478 + self.assertNotIdentical(avatar1.user, avatar2.user) 479 + self.assertNotEqual(avatar1.user.entity, 480 + avatar2.user.entity) 481 + 482 + d = self.realm.requestAvatar(avatarID, None, xmppim.IUserSession) 483 + d.addCallback(gotAvatar2) 484 + return d 485 + 486 + avatarID = checkers.ANONYMOUS 487 + d = self.realm.requestAvatar(avatarID, None, xmppim.IUserSession) 488 + d.addCallback(gotAvatar1) 489 + return d 490 + 491 + 492 + def test_logout(self): 493 + """ 494 + When the logout function is called, the user is removed from the realm. 495 + """ 496 + def gotAvatar(result): 497 + iface, avatar, logout = result 498 + 499 + logout() 500 + 501 + entity = avatar.user.entity 502 + d = self.realm.lookupUser(entity) 503 + self.assertFailure(d, ewokkel.NoSuchUser) 504 + return d 505 + 506 + avatarID = checkers.ANONYMOUS 507 + d = self.realm.requestAvatar(avatarID, None, xmppim.IUserSession) 508 + d.addCallback(gotAvatar) 509 + return d 510 + 511 + 512 + 513 +class StaticRealmTest(unittest.TestCase): 514 + """ 515 + Tests for L{xmppim.StaticRealmTest}. 516 + """ 517 + 518 + def setUp(self): 519 + entity = JID(u'\u00e9lise@example.org') 520 + self.user = xmppim.User(entity) 521 + users = {entity: self.user} 522 + self.realm = xmppim.StaticRealm('example.org', users) 523 + 524 + 525 + def test_interface(self): 526 + """ 527 + StaticRealm implements IRealm. 528 + """ 529 + verify.verifyObject(IRealm, self.realm) 530 + 531 + 532 + def test_requestAvatar(self): 533 + """ 534 + A UserSession is initialized and returned from requestAvatar. 535 + """ 536 + def gotAvatar(result): 537 + iface, avatar, logout = result 538 + self.assertIdentical(self.user, avatar.user) 539 + 540 + avatarID = u'\u00e9lise'.encode('utf-8') 541 + d = self.realm.requestAvatar(avatarID, None, xmppim.IUserSession) 542 + d.addCallback(gotAvatar) 543 + return d 544 + 545 + 546 + def test_requestAvatarUnknown(self): 547 + """ 548 + A UserSession is initialized and returned from requestAvatar. 549 + """ 550 + avatarID = u'nobody'.encode('utf-8') 551 + d = self.realm.requestAvatar(avatarID, None, xmppim.IUserSession) 552 + self.assertFailure(d, ecred.LoginDenied) 553 + return d 554 + 555 + 556 + 557 +class TestRouter(component.Router): 558 + """ 559 + Router that only records incoming traffic for testing. 560 + """ 561 + 562 + def __init__(self): 563 + super(TestRouter, self).__init__() 564 + self.output = [] 565 + 566 + 567 + def route(self, stanza): 568 + """ 569 + All routed stanzas are recorded. 570 + """ 571 + self.output.append(stanza) 572 + 573 + 574 + 575 +class SessionManagerTest(unittest.TestCase): 576 + """ 577 + Tests for L{xmppim.SessionManager}. 578 + """ 579 + 580 + def setUp(self): 581 + self.router = TestRouter() 582 + self.sessionManager = xmppim.SessionManager(self.router, 583 + u'example.org') 584 + self.sessionManager.startService() 585 + 586 + self.input = [] 587 + def onElement(element): 588 + self.input.append(element) 589 + 590 + self.sessionManager.xmlstream.addObserver('/*', onElement) 591 + 592 + 593 + def test_routeOrDeliverLocal(self): 594 + """ 595 + Stanzas for local domains are reinjected in to the XML stream. 596 + """ 597 + element = parseXml("""<presence from='test@example.org' 598 + to='other@example.org'/>""") 599 + self.sessionManager.xmlstream.send(element) 600 + 601 + self.assertEqual(1, len(self.input)) 602 + self.assertEqual(0, len(self.router.output)) 603 + 604 + 605 + def test_routeOrDeliverRemote(self): 606 + """ 607 + Stanzas for other domains are sent to the router. 608 + """ 609 + element = parseXml("""<presence from='test@example.org' 610 + to='other@example.com'/>""") 611 + self.sessionManager.xmlstream.send(element) 612 + 613 + self.assertEqual(0, len(self.input)) 614 + self.assertEqual(1, len(self.router.output)) 615 + 616 + 617 + def test_probePresence(self): 618 + """ 619 + Presence probes are sent to contacts with a subscription. 620 + """ 621 + def cb(result): 622 + self.assertEqual(1, len(self.router.output)) 623 + element = self.router.output[-1] 624 + self.assertEqual(u'presence', 625 + element.name) 626 + self.assertEqual(u'probe', 627 + element.getAttribute(u'type')) 628 + self.assertEqual(u'contact2@example.com', 629 + element.getAttribute(u'to')) 630 + 631 + contacts = [ 632 + xmppim.RosterItem(JID(u'contact1@example.com'), 633 + subscriptionFrom=True, 634 + subscriptionTo=False), 635 + xmppim.RosterItem(JID(u'contact2@example.com'), 636 + subscriptionFrom=False, 637 + subscriptionTo=True), 638 + ] 639 + roster = xmppim.InMemoryRoster(contacts) 640 + entity = JID(u'user@example.org') 641 + user = xmppim.User(entity, roster) 642 + 643 + d = self.sessionManager.probePresence(user) 644 + d.addCallback(cb) 645 + return d 646 diff --git a/wokkel/xmppim.py b/wokkel/xmppim.py 647 --- a/wokkel/xmppim.py 648 +++ b/wokkel/xmppim.py 649 @@ -12,12 +12,20 @@ 650 651 import warnings 652 653 +from zope.interface import implementer 654 + 655 +from twisted.cred import error as ecred, portal 656 from twisted.internet import defer 18 657 +from twisted.python import log, randbytes 19 from twisted.names.srvconnect import SRVConnector 20 from twisted.words.protocols.jabber import client, error, sasl, xmlstream 21 -from twisted.words.xish import domish 22 +from twisted.words.protocols.jabber.jid import JID, internJID 23 +from twisted.words.xish import domish, utility 24 25 from wokkel import generic 26 from wokkel.compat import XmlStreamServerFactory 27 from wokkel.iwokkel import IUserSession 28 from wokkel.subprotocols import ServerStreamManager 29 from wokkel.subprotocols import StreamManager 30 +from wokkel.subprotocols import XMPPHandler 31 32 NS_CLIENT = 'jabber:client' 33 34 @@ -482,3 +486,186 @@ 35 return [ 36 generic.StanzaForwarder() 37 ] 38 + 39 + 40 + 658 from twisted.words.protocols.jabber import error 659 from twisted.words.protocols.jabber.jid import JID 660 from twisted.words.xish import domish 661 662 -from wokkel.generic import ErrorStanza, Stanza, Request 663 +from wokkel.component import InternalComponent 664 +from wokkel.ewokkel import NoSuchContact 665 +from wokkel.ewokkel import NotSubscribed, NoSuchResource, NoSuchUser 666 +from wokkel.iwokkel import IUserSession 667 +from wokkel.generic import ErrorStanza, Stanza, Request, cloneElement 668 from wokkel.subprotocols import IQHandlerMixin 669 from wokkel.subprotocols import XMPPHandler 670 from wokkel.subprotocols import asyncObserver 671 @@ -1062,6 +1070,67 @@ 672 673 674 675 +class InMemoryRoster(object): 676 + 677 + def __init__(self, items): 678 + self.roster = Roster(((item.entity, item) for item in items)) 679 + 680 + 681 + def getRoster(self, version=None): 682 + return defer.succeed(self.roster) 683 + 684 + 685 + def getSubscribers(self): 686 + subscribers = (entity for entity, item in self.roster.iteritems() 687 + if item.subscriptionFrom) 688 + return defer.succeed(subscribers) 689 + 690 + 691 + def getSubscriptions(self): 692 + subscriptions = (entity for entity, item in self.roster.iteritems() 693 + if item.subscriptionTo) 694 + return defer.succeed(subscriptions) 695 + 696 + 697 + def getContact(self, entity): 698 + try: 699 + return defer.succeed(self.roster[entity]) 700 + except KeyError: 701 + return defer.fail(NoSuchContact()) 702 + 703 + 704 + 705 + 706 + 707 +class UserRosterProtocol(RosterServerProtocol): 708 + """ 709 + Roster protocol handler for client connections. 710 + 711 + This protocol is meant to be used with 712 + L{wokkel.client.XMPPC2SServerFactory} to interact with L{UserSession} and 713 + L{User}. 714 + """ 715 + 716 + def handleRequest(self, iq): 717 + """ 718 + Ignore roster requests for non-empty recipients. 719 + """ 720 + if iq.getAttribute('to'): 721 + return False 722 + else: 723 + return super(UserRosterProtocol, self).handleRequest(iq) 724 + 725 + 726 + def getRoster(self, request): 727 + """ 728 + Return the roster of the user associated with this session. 729 + """ 730 + session = self.xmlstream.avatar 731 + session.interested = True 732 + return session.user.roster.getRoster() 733 + 734 + 735 + 736 class Message(Stanza): 737 """ 738 A message stanza. 739 @@ -1159,3 +1228,448 @@ 740 741 self.onMessage(message.element) 742 return True 743 + 744 + 745 + 746 +@implementer(IUserSession) 41 747 +class UserSession(object): 42 + 43 + implements(IUserSession) 44 + 45 + realm = None 748 + """ 749 + An XMPP user session. 750 + 751 + This represents the session for a connected client after authenticating 752 + as L{user}. 753 + 754 + @ivar user: The authenticated user for this session. 755 + @type user: L{User} 756 + 757 + @ivar mind: The protocol instance for this session. 758 + @type mind: L{xmlstream.Xmlstream} 759 + 760 + @ivar entity: The full JID of the entity after a resource has been 761 + bound. 762 + @type entity: L{JID} 763 + 764 + @ivar connected: Flag that is C{True} while a resource is bound. 765 + @type connected: L{boolean} 766 + 767 + @ivar interested: Flag to record that the roster has been requested. When 768 + L{True}, this session will receive roster pushes. 769 + @type interested: L{boolean} 770 + 771 + @ivar presence: Last broadcast presence from the client for this session. 772 + @type presence: L{AvailabilityPresence} 773 + """ 774 + 775 + user = None 46 776 + mind = None 47 + 777 + entity = None 48 778 + connected = False 49 779 + interested = False 50 780 + presence = None 51 781 + 52 + clientStream = None 53 + 54 + def __init__(self, entity): 55 + self.entity = entity 782 + def __init__(self, user): 783 + self.user = user 56 784 + 57 785 + 58 786 + def loggedIn(self, realm, mind): 787 + self.mind = mind 59 788 + self.realm = realm 60 + self.mind = mind61 789 + 62 790 + … … 67 795 + return entity 68 796 + 69 + d = self. realm.bindResource(self, resource)797 + d = self.user.bindResource(self, resource) 70 798 + d.addCallback(cb) 71 799 + return d … … 74 802 + def logout(self): 75 803 + self.connected = False 76 + self.realm.unbindResource(self) 804 + 805 + if self.entity: 806 + self.user.unbindResource(self.entity.resource) 77 807 + 78 808 + 79 809 + def send(self, element): 80 + self.realm.onElement(element, self) 81 + 82 + 83 + def receive(self, element): 810 + """ 811 + Called when the client sends a stanza. 812 + """ 813 + self.realm.server.routeOrDeliver(element) 814 + 815 + 816 + def receive(self, element, recipient=None): 817 + """ 818 + Deliver a stanza to the client. 819 + """ 84 820 + self.mind.send(element) 85 821 + 86 822 + 87 + 88 +class SessionManager(XMPPHandler): 823 + def probePresence(self): 824 + self.user.probePresence(self) 825 + 826 + 827 + def broadcastPresence(self, presence, available): 828 + if available: 829 + self.presence = presence 830 + else: 831 + self.presence = None 832 + # TODO: unset probeSent on user? 833 + # TODO: save last unavailable presence? 834 + 835 + self.user.broadcastPresence(presence) 836 + 837 + 838 + 839 +class User(object): 840 + """ 841 + An XMPP user account. 842 + 843 + @ivar entity: The JID of the user. 844 + @type entity: L{JID} 845 + 846 + @ivar roster: The user's roster. 847 + 848 + @ivar sessions: The currently connected sessions for this user, indexed by 849 + resource. 850 + @type sessions: L{dict} 851 + 852 + @ivar probeSent: Flag that is C{True} if presence probes have been sent 853 + out for this user. This is only done on the initial presence broadcast 854 + of the first available resource. Subsequent resources will get the 855 + presences stored in L{contactPresences}. 856 + @type probeSent: L{boolean} 857 + 858 + @ivar contactPresences: Cached presences of contacts as a mapping of L{JID} 859 + to L{AvailabilityPresence}. 860 + @type contactPresences: L{dict} 861 + 862 + @ivar realm: The realm that provided this user object. 863 + @type realm: L{IRealm} provider 864 + """ 865 + 866 + realm = None 867 + 868 + def __init__(self, entity, roster=None): 869 + self.entity = entity 870 + self.roster = roster 871 + self.sessions = {} 872 + self.probeSent = False 873 + self.contactPresences = {} 874 + 875 + 876 + def bindResource(self, session, resource): 877 + if resource is None: 878 + resource = randbytes.secureRandom(8).encode('hex') 879 + elif resource in self.sessions: 880 + resource = resource + ' ' + randbytes.secureRandom(8).encode('hex') 881 + 882 + entity = JID(tuple=(self.entity.user, self.entity.host, resource)) 883 + self.sessions[resource] = session 884 + 885 + return defer.succeed(entity) 886 + 887 + 888 + def unbindResource(self, resource): 889 + del self.sessions[resource] 890 + return defer.succeed(None) 891 + 892 + 893 + def deliverIQ(self, stanza): 894 + try: 895 + session = self.sessions[stanza.recipient.resource] 896 + except KeyError: 897 + raise NoSuchResource() 898 + 899 + session.receive(stanza.element) 900 + 901 + 902 + def deliverMessage(self, stanza): 903 + if stanza.recipient.resource: 904 + try: 905 + session = self.sessions[stanza.recipient.resource] 906 + except KeyError: 907 + if stanza.stanzaType in ('normal', 'chat', 'headline'): 908 + self.deliverMessageAnyResource(stanza) 909 + else: 910 + raise NoSuchResource() 911 + else: 912 + session.receive(stanza.element) 913 + else: 914 + if stanza.stanzaType == 'groupchat': 915 + raise NotImplementedError("Groupchat message to the bare JID") 916 + else: 917 + self.deliverMessageAnyResource(stanza) 918 + 919 + 920 + def deliverMessageAnyResource(self, stanza): 921 + if stanza.stanzaType == 'headline': 922 + recipients = set() 923 + for resource, session in self.sessions.iteritems(): 924 + if session.presence.priority >= 0: 925 + recipients.add(resource) 926 + elif stanza.stanzaType in ('chat', 'normal'): 927 + priorities = {} 928 + for resource, session in self.sessions.iteritems(): 929 + if not session.presence or not session.presence.available: 930 + continue 931 + priority = session.presence.priority 932 + if priority >= 0: 933 + priorities.setdefault(priority, set()).add(resource) 934 + if priorities: 935 + maxPriority = max(priorities.keys()) 936 + recipients = priorities[maxPriority] 937 + else: 938 + # No available resource, offline storage not supported 939 + raise NotImplementedError("Offline storage is not supported") 940 + else: 941 + recipients = set() 942 + 943 + if recipients: 944 + for resource in recipients: 945 + session = self.sessions[resource] 946 + session.receive(stanza.element) 947 + else: 948 + # silently discard 949 + log.msg("Discarding message to %r" % stanza.recipient) 950 + 951 + 952 + def deliverPresence(self, stanza): 953 + if not stanza.recipient.resource: 954 + # record 955 + 956 + for session in self.sessions.itervalues(): 957 + if session.presence: 958 + session.receive(stanza.element) 959 + 960 + 961 + def probePresence(self, session): 962 + """ 963 + Probe presences for this user. 964 + 965 + If this is the first session requesting presence probes, they are 966 + sent out to the contacts via the realm. After that, the last received 967 + presences are sent back to the session directly. 968 + """ 969 + if not self.probeSent: 970 + # send out probes 971 + self.contactPresences = {} 972 + self.realm.server.probePresence(self) 973 + self.probeSent = True 974 + else: 975 + # deliver known contact presences 976 + for presence in self.contactPresences.itervalues(): 977 + session.receive(presence.element) 978 + 979 + 980 + @defer.inlineCallbacks 981 + def broadcastPresence(self, presence): 982 + """ 983 + Broadcast presence to all subscribed contacts and myself. 984 + """ 985 + subscribers = yield self.roster.getSubscribers() 986 + self.realm.server.multicast(presence, subscribers) 987 + 988 + 989 + @defer.inlineCallbacks 990 + def getPresences(self, entity): 991 + """ 992 + Get presences on behalf of a contact. 993 + 994 + @param entity: The contact requesting that initiated a presence probe. 995 + @type entity: L{JID} 996 + 997 + @return: Deferred that fires with an iterable of 998 + L{AvailabilityPresence}. 999 + @rtype: L{defer.Deferred} 1000 + 1001 + @raise NotSubscribed: If the contact does not have a presence 1002 + subscription from this user. 1003 + @raise NoSuchContact: If the requestor is not a contact. 1004 + """ 1005 + bareEntity = entity.userhostJID() 1006 + item = yield self.roster.getContact(bareEntity) 1007 + 1008 + if not item.subscriptionFrom: 1009 + raise NotSubscribed() 1010 + 1011 + presences = (session.presence for session in self.sessions.itervalues() 1012 + if session.presence) 1013 + defer.returnValue(presences) 1014 + # TODO: send last unavailable or unavailable presence? 1015 + 1016 + 1017 + 1018 +@implementer(portal.IRealm) 1019 +class BaseRealm(object): 1020 + server = None 1021 + 1022 + def __init__(self, domain): 1023 + self.domain = domain 1024 + 1025 + 1026 + def lookupUser(self, entity): 1027 + raise NotImplementedError() 1028 + 1029 + 1030 + def createUser(self, entity): 1031 + raise NotImplementedError() 1032 + 1033 + 1034 + def getUser(self, entity): 1035 + def trapNoSuchUser(failure): 1036 + failure.trap(NoSuchUser) 1037 + return self.createUser(entity) 1038 + 1039 + d = self.lookupUser(entity) 1040 + d.addErrback(trapNoSuchUser) 1041 + return d 1042 + 1043 + 1044 + def logoutFactory(self, session): 1045 + return session.logout 1046 + 1047 + 1048 + def entityFromAvatarID(self, avatarId): 1049 + localpart = avatarId.decode('utf-8') 1050 + return JID(tuple=(localpart, self.domain, None)) 1051 + 1052 + 1053 + def requestAvatar(self, avatarId, mind, *interfaces): 1054 + if IUserSession not in interfaces: 1055 + raise NotImplementedError(self, interfaces) 1056 + 1057 + entity = self.entityFromAvatarID(avatarId) 1058 + 1059 + def gotUser(user): 1060 + session = UserSession(user) 1061 + session.loggedIn(self, mind) 1062 + return IUserSession, session, self.logoutFactory(session) 1063 + 1064 + d = self.getUser(entity) 1065 + d.addCallback(gotUser) 1066 + return d 1067 + 1068 + 1069 + 1070 +class AnonymousRealm(BaseRealm): 1071 + 1072 + def __init__(self, domain): 1073 + BaseRealm.__init__(self, domain) 1074 + self.users = {} 1075 + 1076 + 1077 + def entityFromAvatarID(self, avatarId): 1078 + localpart = randbytes.secureRandom(8).encode('hex') 1079 + return JID(tuple=(localpart, self.domain, None)) 1080 + 1081 + 1082 + def lookupUser(self, entity): 1083 + try: 1084 + user = self.users[entity] 1085 + except KeyError: 1086 + return defer.fail(NoSuchUser(entity)) 1087 + return defer.succeed(user) 1088 + 1089 + 1090 + def createUser(self, entity): 1091 + user = User(entity, InMemoryRoster([])) 1092 + user.realm = self 1093 + self.users[entity] = user 1094 + return defer.succeed(user) 1095 + 1096 + 1097 + def logoutFactory(self, session): 1098 + def logout(): 1099 + session.logout() 1100 + del self.users[session.user.entity] 1101 + return logout 1102 + 1103 + 1104 + 1105 +class StaticRealm(BaseRealm): 1106 + 1107 + def __init__(self, domain, users): 1108 + BaseRealm.__init__(self, domain) 1109 + for user in users.itervalues(): 1110 + user.realm = self 1111 + self.users = users 1112 + 1113 + 1114 + def lookupUser(self, entity): 1115 + try: 1116 + user = self.users[entity] 1117 + except KeyError: 1118 + return defer.fail(NoSuchUser(entity)) 1119 + return defer.succeed(user) 1120 + 1121 + 1122 + def createUser(self, entity): 1123 + return defer.fail(ecred.LoginDenied("Can't create a new user")) 1124 + 1125 + 1126 + 1127 +class SessionManager(InternalComponent): 89 1128 + """ 90 1129 + Session Manager. … … 97 1136 + """ 98 1137 + 99 + implements(portal.IRealm) 100 + 101 + def __init__(self, domain, accounts): 102 + XMPPHandler.__init__(self) 103 + self.domain = domain 104 + self.accounts = accounts 105 + 106 + self.sessions = {} 107 + self.clientStream = utility.EventDispatcher() 108 + self.clientStream.addObserver('/*', self.routeOrDeliver, -1) 109 + 110 + 111 + def requestAvatar(self, avatarId, mind, *interfaces): 112 + if IUserSession not in interfaces: 113 + raise NotImplementedError(self, interfaces) 114 + 115 + localpart = avatarId.decode('utf-8') 116 + entity = JID(tuple=(localpart, self.domain, None)) 117 + session = UserSession(entity) 118 + session.loggedIn(self, mind) 119 + return IUserSession, session, session.logout 120 + 121 + 122 + def bindResource(self, session, resource): 123 + localpart = session.entity.user 124 + 125 + try: 126 + userSessions = self.sessions[localpart] 127 + except KeyError: 128 + userSessions = self.sessions[localpart] = {} 129 + 130 + if resource is None: 131 + resource = randbytes.secureRandom(8).encode('hex') 132 + elif resource in self.userSessions: 133 + resource = resource + ' ' + randbytes.secureRandom(8).encode('hex') 134 + 135 + entity = JID(tuple=(session.entity.user, session.entity.host, resource)) 136 + userSessions[resource] = session 137 + 138 + return defer.succeed(entity) 139 + 140 + 141 + def lookupSessions(self, entity): 142 + """ 143 + Return all sessions for a user. 144 + 145 + @param entity: Entity to retrieve sessions for. This the resource part 146 + will be ignored. 147 + @type entity: L{JID<twisted.words.protocols.jabber.jid.JID>} 148 + 149 + @return: Mapping of sessions keyed by resource. 150 + @rtype: C{dict} 151 + """ 152 + localpart = entity.user 153 + 154 + try: 155 + return self.sessions[localpart] 156 + except: 157 + return {} 158 + 159 + 160 + def lookupSession(self, entity): 161 + """ 162 + Return the session for a particular resource of an entity. 163 + 164 + @param entity: Entity to retrieve sessions for. 165 + @type entity: L{JID<twisted.words.protocols.jabber.jid.JID>} 166 + 167 + @return: C{UserSession}. 168 + """ 169 + 170 + userSessions = self.lookupSessions(entity) 171 + return userSessions[entity.resource] 172 + 173 + 174 + 175 + def unbindResource(self, session, reason=None): 176 + session.connected = False 177 + 178 + localpart = session.entity.user 179 + resource = session.entity.resource 180 + 181 + del self.sessions[localpart][resource] 182 + if not self.sessions[localpart]: 183 + del self.sessions[localpart] 184 + 185 + return defer.succeed(None) 186 + 187 + 188 + def onElement(self, element, session): 189 + # Make sure each stanza has a sender address 190 + if (element.name == 'presence' and 191 + element.getAttribute('type') in ('subscribe', 'subscribed', 192 + 'unsubscribe', 'unsubscribed')): 193 + element['from'] = session.entity.userhost() 194 + else: 195 + element['from'] = session.entity.full() 196 + 197 + self.clientStream.dispatch(element) 1138 + def startService(self): 1139 + InternalComponent.startService(self) 1140 + self.xmlstream.send = self.routeOrDeliver 198 1141 + 199 1142 + … … 202 1145 + Deliver a stanza locally or pass on for routing. 203 1146 + """ 204 + if element.handled: 205 + return 206 + 207 + if (not element.hasAttribute('to') or 208 + internJID(element['to']).host == self.domain): 1147 + if (JID(element['to']).host in self.domains): 209 1148 + # This stanza is for local delivery 210 1149 + log.msg("Delivering locally: %r" % element.toXml()) 211 + self. xmlstream.dispatch(element)1150 + self._pipe.source.dispatch(element) 212 1151 + else: 213 1152 + # This stanza is for remote routing 214 1153 + log.msg("Routing remotely: %r" % element.toXml()) 215 + XMPPHandler.send(self, element) 216 + 217 + 218 + def deliverStanza(self, element, recipient): 219 + session = self.lookupSession(recipient) 220 + session.receive(element) 221 diff --git a/wokkel/test/test_client.py b/wokkel/test/test_client.py 222 --- a/wokkel/test/test_client.py 223 +++ b/wokkel/test/test_client.py 224 @@ -7,7 +7,7 @@ 225 226 from base64 import b64encode 227 228 -from zope.interface import implements 229 +from zope.interface import implements, verify 230 231 from twisted.cred.portal import IRealm, Portal 232 from twisted.cred.checkers import InMemoryUsernamePasswordDatabaseDontUse 233 @@ -601,6 +601,7 @@ 234 235 236 237 + 238 class XMPPClientListenAuthenticatorTest(unittest.TestCase): 239 """ 240 Tests for L{client.XMPPClientListenAuthenticator}. 241 @@ -670,3 +671,26 @@ 242 "to='example.com' " 243 "version='1.0'>") 244 self.xmlstream.assertStreamError(self, condition='host-unknown') 245 + 246 + 247 + 248 +class UserSessionTest(unittest.TestCase): 249 + 250 + def setUp(self): 251 + self.session = client.UserSession(JID('user@example.org')) 252 + 253 + 254 + def test_interface(self): 255 + verify.verifyObject(client.IUserSession, self.session) 256 + 257 + 258 + 259 +class SessionManagerTest(unittest.TestCase): 260 + 261 + def setUp(self): 262 + accounts = {'user': None} 263 + self.sessionManager = client.SessionManager('example.org', accounts) 264 + 265 + 266 + def test_interface(self): 267 + verify.verifyObject(IRealm, self.sessionManager) 1154 + self._pipe.sink.dispatch(element) 1155 + 1156 + 1157 + def multicast(self, stanza, recipients): 1158 + """ 1159 + 1160 + @param stanza: The stanza to send. Its C{element} attribute should 1161 + already be set. 1162 + @type stanza: L{wokkel.generic.Stanza}. 1163 + 1164 + @type recipients: iterable of L{JID}. 1165 + """ 1166 + if not stanza.element: 1167 + stanza.toElement() 1168 + 1169 + for recipient in recipients: 1170 + clone = cloneElement(stanza.element) 1171 + clone['to'] = recipient.full() 1172 + clone.handled = False 1173 + self.routeOrDeliver(clone) 1174 + 1175 + 1176 + @defer.inlineCallbacks 1177 + def probePresence(self, user): 1178 + """ 1179 + Request the presences of all contacts the user has a subscription to. 1180 + 1181 + This will send out presence probe stanzas, even to local contacts. 1182 + """ 1183 + subscriptions = yield user.roster.getSubscriptions() 1184 + for entity in subscriptions: 1185 + presence = ProbePresence(recipient=entity, 1186 + sender=user.entity) 1187 + self.routeOrDeliver(presence.toElement())
Note: See TracChangeset
for help on using the changeset viewer.