Changeset 66:b713f442b222 in ralphm-patches
- Timestamp:
- Sep 1, 2012, 12:40:53 PM (10 years ago)
- Branch:
- default
- Files:
-
- 6 edited
Legend:
- Unmodified
- Added
- Removed
-
c2s_server_factory.patch
r57 r66 1 1 # HG changeset patch 2 # Parent d76497171af8f3acf1efd2c8433fbdc3c4a55f922 # Parent 23dbe722e4482b8286153d29bf87902b464f919f 3 3 Add factory for accepting client connections. 4 4 … … 21 21 * Add tests. 22 22 23 diff - r d76497171af8wokkel/client.py24 --- a/wokkel/client.py Wed Nov 30 09:31:07 2011 +010025 +++ b/wokkel/client.py Wed Nov 30 09:32:01 2011 +010026 @@ -2 0,6 +20,7 @@23 diff --git a/wokkel/client.py b/wokkel/client.py 24 --- a/wokkel/client.py 25 +++ b/wokkel/client.py 26 @@ -21,6 +21,7 @@ 27 27 from twisted.words.xish import domish 28 28 29 29 from wokkel import generic 30 30 +from wokkel.compat import XmlStreamServerFactory 31 from wokkel.iwokkel import IUserSession 31 32 from wokkel.subprotocols import StreamManager 32 33 33 NS_CLIENT = 'jabber:client' 34 @@ -347,3 +348,98 @@ 35 36 37 38 +class RecipientUnavailable(Exception): 39 + """ 40 + The addressed entity is not, or no longer, available. 41 + """ 34 @@ -401,3 +402,61 @@ 35 self.portal = self.portals[self.xmlstream.thisEntity] 36 except KeyError: 37 raise error.StreamError('host-unknown') 42 38 + 43 39 + … … 45 41 +class XMPPC2SServerFactory(XmlStreamServerFactory): 46 42 + 47 + def __init__(self, service):48 + self. service = service43 + def __init__(self, portal): 44 + self.portal = portal 49 45 + 50 46 + def authenticatorFactory(): 51 + return XMPPClientListenAuthenticator( service)47 + return XMPPClientListenAuthenticator(portal) 52 48 + 53 49 + XmlStreamServerFactory.__init__(self, authenticatorFactory) … … 58 54 + 59 55 + self.serial = 0 60 + self.streams = {}61 56 + 62 57 + … … 82 77 + xs.rawDataOutFn = logDataOut 83 78 + 79 + xs.addObserver(xmlstream.STREAM_END_EVENT, self.onConnectionLost, 80 + 0, xs) 84 81 + xs.addObserver(xmlstream.STREAM_ERROR_EVENT, self.onError) 85 82 + … … 88 85 + log.msg("Client connection %d authenticated" % xs.serial) 89 86 + 90 + xs.addObserver(xmlstream.STREAM_END_EVENT, self.onConnectionLost, 91 + 0, xs) 92 + xs.addObserver('/*', self.onElement, 0, xs) 93 + 94 + # Record this stream as bound to the authenticated JID 95 + self.streams[xs.otherEntity] = xs 87 + xs.addObserver('/*', xs.avatar.send) 96 88 + 97 89 + … … 99 91 + log.msg("Client connection %d disconnected" % xs.serial) 100 92 + 101 + entity = xs.otherEntity102 + self.service.unbindResource(entity.user,103 + entity.host,104 + entity.resource,105 + reason)106 +107 + # If the lost connections had been bound, remove the reference108 + if xs.otherEntity in self.streams:109 + del self.streams[xs.otherEntity]110 +111 93 + 112 94 + def onError(self, reason): 113 95 + log.err(reason, "Stream Error") 114 +115 +116 + def onElement(self, xs, stanza):117 + """118 + Called when an element was received from one of the connected streams.119 +120 + """121 + if stanza.handled:122 + return123 + else:124 + self.service.onElement(stanza, xs.otherEntity)125 +126 +127 + def deliverStanza(self, element, recipient):128 + if recipient in self.streams:129 + self.streams[recipient].send(element)130 + else:131 + raise RecipientUnavailable(u"There is no connection for %s" %132 + recipient.full()) -
c2s_stanza_handlers.patch
r62 r66 1 1 # HG changeset patch 2 # Parent 8c6fa8ea95402e5c968f57e7fde2f8e249c11d122 # Parent 41c2620133080ea88612732ff211561c660cfdec 3 3 Add c2s protocol handlers for iq, message and presence stanzas. 4 4 … … 12 12 --- /dev/null 13 13 +++ b/doc/examples/client_service.tac 14 @@ -0,0 +1, 75@@14 @@ -0,0 +1,82 @@ 15 15 +from twisted.application import service, strports 16 +from twisted.cred.portal import Portal 17 +from twisted.cred.checkers import InMemoryUsernamePasswordDatabaseDontUse 16 18 +from twisted.internet import defer 17 19 + … … 75 77 +sessionManager.setHandlerParent(component) 76 78 + 79 +checker = InMemoryUsernamePasswordDatabaseDontUse(ralphm='secret', 80 + termie='secret') 81 +portal = Portal(sessionManager, (checker,)) 82 +portals = {JID(domain): portal} 83 + 77 84 +xmppim.AccountIQHandler(sessionManager).setHandlerParent(component) 78 85 +xmppim.AccountMessageHandler(sessionManager).setHandlerParent(component) … … 82 89 +PingHandler().setHandlerParent(component) 83 90 + 84 +c2sFactory = client.XMPPC2SServerFactory( sessionManager)91 +c2sFactory = client.XMPPC2SServerFactory(portals) 85 92 +c2sFactory.logTraffic = True 86 93 +c2sService = strports.service('5224', c2sFactory) -
client_listen_authenticator.patch
r65 r66 1 1 # HG changeset patch 2 # Parent a d0f4165244b1c661023f03d8f7557fe5352337f2 # Parent a1648553ea06b7f0b38fec775db71ac03782e3d6 3 3 Add authenticator for accepting XMPP client connections. 4 4 5 5 The new authenticator XMPPClientListenAuthenticator is to be used together 6 with an `XmlStream` created for an incoming XMPP stream. It handles the 7 SASL PLAIN mechanism only. 6 with an `XmlStream` created for an incoming XMPP stream. It uses the 7 new initializers for SASL (PLAIN only), resource binding and session 8 establishement. 8 9 9 10 This authenticator needs at least one Twisted Cred portal to hold the … … 13 14 logout callback is called. 14 15 15 TODO:16 17 * Add tests.18 * Add docstrings.19 20 16 diff --git a/wokkel/client.py b/wokkel/client.py 21 17 --- a/wokkel/client.py 22 18 +++ b/wokkel/client.py 23 @@ -10,14 +10,2 8@@19 @@ -10,14 +10,27 @@ 24 20 that should probably eventually move there. 25 21 """ 26 22 27 23 +import base64 28 +29 +from zope.interface import Interface30 24 + 31 25 from twisted.application import service … … 40 34 41 35 from wokkel import generic 36 +from wokkel.iwokkel import IUserSession 42 37 from wokkel.subprotocols import StreamManager 43 38 … … 52 47 """ 53 48 Check what authentication methods are available. 54 @@ -51,7 +6 5,7 @@49 @@ -51,7 +64,7 @@ 55 50 autentication. 56 51 """ … … 61 56 def __init__(self, jid, password): 62 57 xmlstream.ConnectAuthenticator.__init__(self, jid.host) 63 @@ -186,3 + 200,210@@58 @@ -186,3 +199,246 @@ 64 59 c = XMPPClientConnector(reactor, domain, factory) 65 60 c.connect() … … 74 69 + 75 70 + 76 +class IAccount(Interface): 77 + pass 78 + 79 + 80 + 81 +class SASLReceivingInitializer(object): 71 + 72 +class AuthorizationIdentifierNotSupported(Exception): 73 + """ 74 + Authorization Identifiers are not supported. 75 + """ 76 + 77 + 78 + 79 +class SASLReceivingInitializer(generic.BaseReceivingInitializer): 80 + """ 81 + Stream initializer for SASL authentication, receiving side. 82 + """ 83 + 82 84 + required = True 83 85 + 84 + def __init__(self, xs, portal):85 + self.xmlstream = xs86 + def __init__(self, name, xs, portal): 87 + generic.BaseReceivingInitializer.__init__(self, name, xs) 86 88 + self.portal = portal 87 + self.deferred = defer.Deferred()88 89 + self.failureGrace = 3 89 90 + … … 96 97 + 97 98 + def initialize(self): 98 + self.xmlstream.addObserver(XPATH_AUTH, self.onAuth) 99 + self.xmlstream.avatar = None 100 + self.xmlstream.addObserver(XPATH_AUTH, self._onAuth) 99 101 + return self.deferred 100 102 + 101 103 + 102 + def onAuth(self, auth): 104 + def _onAuth(self, auth): 105 + """ 106 + Called when the start of the SASL negotiation is received. 107 + 108 + @type auth: L{domish.Element}. 109 + """ 103 110 + auth.handled = True 104 111 + … … 107 114 + self.xmlstream.send(response) 108 115 + self.xmlstream.reset() 116 + self.deferred.callback(xmlstream.Reset) 109 117 + 110 118 + def eb(failure): … … 113 121 + elif failure.check(InvalidMechanism): 114 122 + condition = 'invalid-mechanism' 123 + elif failure.check(AuthorizationIdentifierNotSupported): 124 + condition = 'invalid-authz' 115 125 + else: 116 126 + log.err(failure) … … 124 134 + self.failureGrace -= 1 125 135 + if self.failureGrace == 0: 126 + raise error.StreamError('policy-violation') 127 + #self.xmlstream.sendStreamError(exc) 128 + 129 + d = defer.maybeDeferred(self.doAuth, auth) 136 + self.deferred.errback(error.StreamError('policy-violation')) 137 + else: 138 + return 139 + 140 + d = defer.maybeDeferred(self._doAuth, auth) 130 141 + d.addCallbacks(cb, eb) 131 + d.chainDeferred(self.deferred) 132 + 133 + 134 + def credentialsFromPlain(self, auth): 142 + 143 + 144 + def _credentialsFromPlain(self, auth): 145 + """ 146 + Create credentials from the initial response for PLAIN. 147 + """ 135 148 + initialResponse = base64.b64decode(unicode(auth)) 136 149 + authzid, authcid, passwd = initialResponse.split('\x00') 137 150 + 138 + # FIXME: bail if authzid is set 139 + 140 + creds = credentials.UsernamePassword(username=authcid.encode('utf-8'), 151 + if authzid: 152 + raise AuthorizationIdentifierNotSupported() 153 + 154 + creds = credentials.UsernamePassword(username=authcid, 141 155 + password=passwd) 142 156 + return creds 143 157 + 144 158 + 145 + def doAuth(self, auth): 146 + 159 + def _doAuth(self, auth): 160 + """ 161 + Start authentication. 162 + """ 147 163 + if auth.getAttribute('mechanism') != 'PLAIN': 148 164 + raise InvalidMechanism() 149 165 + 150 + creds = self. credentialsFromPlain(auth)166 + creds = self._credentialsFromPlain(auth) 151 167 + 152 168 + def cb((iface, avatar, logout)): … … 155 171 + lambda _: logout()) 156 172 + 157 + d = self.portal.login(creds, None, IAccount)173 + d = self.portal.login(creds, self.xmlstream, IUserSession) 158 174 + d.addCallback(cb) 159 175 + return d … … 161 177 + 162 178 + 163 +class BindReceivingInitializer(object): 179 +class BindReceivingInitializer(generic.BaseReceivingInitializer): 180 + """ 181 + Stream initializer for resource binding, receiving side. 182 + """ 183 + 164 184 + required = True 165 +166 + def __init__(self, xs):167 + self.xmlstream = xs168 + self.deferred = defer.Deferred()169 +170 185 + 171 186 + def getFeatures(self): … … 186 201 + response.addElement((client.NS_XMPP_BIND, 'bind')) 187 202 + response.bind.addElement((client.NS_XMPP_BIND, 'jid'), 188 + content=boundJID.full())203 + content=boundJID.full()) 189 204 + 190 205 + return response 191 +192 + def eb(failure):193 + if not isinstance(failure, error.StanzaError):194 + log.msg(failure)195 + exc = error.StanzaError('internal-server-error')196 + else:197 + exc = failure.value198 +199 + return exc.toResponse(iq)200 206 + 201 207 + iq.handled = True … … 203 209 + d = self.xmlstream.avatar.bindResource(resource) 204 210 + d.addCallback(cb) 205 + d.addErrback(eb)206 211 + d.addCallback(self.xmlstream.send) 207 212 + d.chainDeferred(self.deferred) … … 209 214 + 210 215 + 211 +class SessionReceivingInitializer(object): 216 +class SessionReceivingInitializer(generic.BaseReceivingInitializer): 217 + """ 218 + Stream initializer for session establishment, receiving side. 219 + 220 + This is mostly a no-op and just returns a result stanza. If resource 221 + binding hasn't yet completed, this will return a stanza error with the 222 + condition C{'forbidden'}. 223 + 224 + Note that RFC 6120 deprecated the session establishment protocol. This 225 + is provided for backwards compatibility. 226 + """ 227 + 212 228 + required = False 213 +214 + def __init__(self, xs):215 + self.xmlstream = xs216 + self.deferred = defer.Deferred()217 +218 229 + 219 230 + def getFeatures(self): … … 233 244 + 234 245 + if self.xmlstream.otherEntity: 235 + reply = xmlstream.toResponse(iq )246 + reply = xmlstream.toResponse(iq, 'result') 236 247 + else: 237 248 + reply = error.StanzaError('forbidden').toResponse(iq) … … 242 253 + 243 254 +class XMPPClientListenAuthenticator(generic.FeatureListenAuthenticator): 255 + """ 256 + XML Stream authenticator for XMPP clients, server side. 257 + 258 + @ivar portals: Mapping of server JIDs to Cred Portals. 259 + @type portals: C{dict} of L{twisted.words.protocols.jabber.jid.JID} to 260 + L{twisted.cred.portal.Portal}. 261 + """ 262 + 244 263 + namespace = NS_CLIENT 245 264 + … … 251 270 + 252 271 + def getInitializers(self): 272 + """ 273 + Return initializers based on previously completed initializers. 274 + 275 + This has three stages: 1. SASL, 2. Resource binding and session 276 + establishment. 3. Completed. Note that session establishment 277 + is optional. 278 + """ 253 279 + if not self.completedInitializers: 254 + return [SASLReceivingInitializer(self.xmlstream, self.portal)] 255 + elif isinstance(self.completedInitializers[-1], 256 + SASLReceivingInitializer): 257 + return [BindReceivingInitializer(self.xmlstream), 258 + SessionReceivingInitializer(self.xmlstream)] 280 + return [SASLReceivingInitializer('sasl', self.xmlstream, self.portal)] 281 + elif self.completedInitializers[-1] == 'sasl': 282 + return [BindReceivingInitializer('bind', self.xmlstream), 283 + SessionReceivingInitializer('session', self.xmlstream)] 259 284 + else: 260 285 + return [] … … 262 287 + 263 288 + def checkStream(self): 289 + """ 290 + Check that the stream header has proper addressing. 291 + 292 + The C{'to'} attribute must be present and there should have a matching 293 + portal in L{portals}. 294 + """ 264 295 + generic.FeatureListenAuthenticator.checkStream(self) 265 296 + … … 272 303 + except KeyError: 273 304 + raise error.StreamError('host-unknown') 305 diff --git a/wokkel/generic.py b/wokkel/generic.py 306 --- a/wokkel/generic.py 307 +++ b/wokkel/generic.py 308 @@ -465,6 +465,7 @@ 309 310 def __init__(self): 311 self.completedInitializers = [] 312 + self._initialized = False 313 314 315 def _onElementFallback(self, element): 316 @@ -556,11 +557,12 @@ 317 318 self.xmlstream.send(features) 319 320 - if not required: 321 + if not required and not self._initialized: 322 # There are no required initializers anymore. This stream is 323 # now ready for the exchange of stanzas. 324 self.xmlstream.removeObserver(XPATH_ALL, self._onElementFallback) 325 self.xmlstream.dispatch(self.xmlstream, xmlstream.STREAM_AUTHD_EVENT) 326 + self._initialized = True 327 328 if ds: 329 d = defer.DeferredList(ds, fireOnOneCallback=True, 330 diff --git a/wokkel/iwokkel.py b/wokkel/iwokkel.py 331 --- a/wokkel/iwokkel.py 332 +++ b/wokkel/iwokkel.py 333 @@ -985,6 +985,45 @@ 334 335 336 337 +class IUserSession(Interface): 338 + def loggedIn(realm, mind): 339 + """ 340 + Called by the realm when login occurs. 341 + 342 + @param realm: The realm though which login is occurring. 343 + @param mind: The mind object. 344 + """ 345 + 346 + 347 + def bindResource(resource): 348 + """ 349 + Bind a resource to this session. 350 + 351 + @type resource: C{unicode}. 352 + """ 353 + 354 + 355 + def logout(): 356 + """ 357 + End this session. 358 + 359 + This is called when the stream is disconnected. 360 + """ 361 + 362 + 363 + def send(element): 364 + """ 365 + Called when the client sends a stanza. 366 + """ 367 + 368 + 369 + def receive(element): 370 + """ 371 + Have the client receive a stanza. 372 + """ 373 + 374 + 375 + 376 class IReceivingInitializer(Interface): 377 """ 378 Interface for XMPP stream initializers for receiving entities. 274 379 diff --git a/wokkel/test/test_client.py b/wokkel/test/test_client.py 275 380 --- a/wokkel/test/test_client.py 276 381 +++ b/wokkel/test/test_client.py 277 @@ -5,1 4 +5,21@@382 @@ -5,16 +5,29 @@ 278 383 Tests for L{wokkel.client}. 279 384 """ 280 385 386 +from base64 import b64encode 387 + 281 388 +from zope.interface import implements 282 389 + … … 284 391 +from twisted.cred.checkers import InMemoryUsernamePasswordDatabaseDontUse 285 392 from twisted.internet import defer 393 +from twisted.test import proto_helpers 286 394 from twisted.trial import unittest 287 from twisted.words.protocols.jabber import xmlstream 395 -from twisted.words.protocols.jabber import xmlstream 396 +from twisted.words.protocols.jabber import error, xmlstream 397 +from twisted.words.protocols.jabber.client import NS_XMPP_BIND 398 +from twisted.words.protocols.jabber.client import NS_XMPP_SESSION 288 399 from twisted.words.protocols.jabber.client import XMPPAuthenticator 289 400 from twisted.words.protocols.jabber.jid import JID … … 296 407 +from twisted.words.xish import xpath 297 408 298 from wokkel import client 299 300 @@ -155,3 +162,167 @@ 409 -from wokkel import client 410 +from wokkel import client, iwokkel 411 +from wokkel.generic import TestableXmlStream, FeatureListenAuthenticator 412 413 class XMPPClientTest(unittest.TestCase): 414 """ 415 @@ -155,3 +168,505 @@ 301 416 self.assertEqual(factory.deferred, d2) 302 417 … … 304 419 + 305 420 + 306 +class TestAccount(object): 307 + implements(client.IAccount) 421 + 422 +class TestSession(object): 423 + implements(iwokkel.IUserSession) 424 + 425 + def __init__(self, domain, user): 426 + self.domain = domain 427 + self.user = user 428 + 429 + 430 + def bindResource(self, resource): 431 + return defer.succeed(JID(tuple=(self.user, self.domain, resource))) 432 + 308 433 + 309 434 + … … 314 439 + logoutCalled = False 315 440 + 441 + def __init__(self, domain): 442 + self.domain = domain 443 + 444 + 316 445 + def requestAvatar(self, avatarId, mind, *interfaces): 317 + return (client.IAccount, TestAccount(), self.logout) 446 + return (iwokkel.IUserSession, 447 + TestSession(self.domain, avatarId.decode('utf-8')), 448 + self.logout) 318 449 + 319 450 + … … 322 453 + 323 454 + 324 +class TestableXmlStream(xmlstream.XmlStream): 325 + 326 + def __init__(self, authenticator): 327 + xmlstream.XmlStream.__init__(self, authenticator) 328 + self.headerSent = False 329 + self.footerSent = False 330 + self.streamErrors = [] 331 + self.output = [] 332 + 333 + 334 + def reset(self): 335 + xmlstream.XmlStream.reset(self) 336 + self.headerSent = False 337 + 338 + 339 + def sendHeader(self): 340 + self.headerSent = True 341 + 342 + 343 + def sendFooter(self): 344 + self.footerSent = True 345 + 346 + 347 + def sendStreamError(self, streamError): 348 + self.streamErrors.append(streamError) 349 + 350 + 351 + def send(self, obj): 352 + self.output.append(obj) 455 + 456 +class TestableFeatureListenAuthenticator(FeatureListenAuthenticator): 457 + namespace = 'jabber:client' 458 + 459 + initialized = None 460 + 461 + def __init__(self, getInitializers): 462 + """ 463 + Set up authenticator. 464 + 465 + @param getInitializers: Function to override the getInitializers 466 + method. It will receive C{self} as the only argument. 467 + """ 468 + FeatureListenAuthenticator.__init__(self) 469 + 470 + import types 471 + self.getInitializers = types.MethodType(getInitializers, self) 472 + 473 + xs = TestableXmlStream(self) 474 + xs.makeConnection(proto_helpers.StringTransport()) 475 + 476 + 477 + def streamStarted(self, rootElement): 478 + """ 479 + Set up observers for authentication events. 480 + """ 481 + def authenticated(_): 482 + self.initialized = True 483 + 484 + self.xmlstream.addObserver(STREAM_AUTHD_EVENT, authenticated) 485 + FeatureListenAuthenticator.streamStarted(self, rootElement) 486 + 487 + 488 + 489 +class SASLReceivingInitializerTest(unittest.TestCase): 490 + """ 491 + Tests for L{client.SASLReceivingInitializer}. 492 + """ 493 + 494 + def setUp(self): 495 + realm = TestRealm(u'example.org') 496 + checker = InMemoryUsernamePasswordDatabaseDontUse(test='secret') 497 + self.portal = portal = Portal(realm, (checker,)) 498 + 499 + def getInitializers(self): 500 + self.initializer = client.SASLReceivingInitializer('sasl', 501 + self.xmlstream, 502 + portal) 503 + return [self.initializer] 504 + 505 + self.authenticator = TestableFeatureListenAuthenticator(getInitializers) 506 + self.xmlstream = self.authenticator.xmlstream 507 + 508 + 509 + def test_getFeatures(self): 510 + """ 511 + The stream features list SASL with the PLAIN mechanism. 512 + """ 513 + xs = self.xmlstream 514 + xs.dataReceived("<stream:stream xmlns='jabber:client' " 515 + "xmlns:stream='http://etherx.jabber.org/streams' " 516 + "to='example.org' " 517 + "version='1.0'>") 518 + 519 + self.assertTrue(xs.headerSent) 520 + 521 + # Check SASL mechanisms 522 + features = xs.output[-1] 523 + self.assertTrue(xpath.matches("/features[@xmlns='%s']" 524 + "/mechanisms[@xmlns='%s']" 525 + "/mechanism[@xmlns='%s' and " 526 + "text()='PLAIN']" % 527 + (NS_STREAMS, NS_XMPP_SASL, NS_XMPP_SASL), 528 + features)) 529 + 530 + 531 + def test_auth(self): 532 + """ 533 + Authenticating causes an avatar to be set on the authenticator. 534 + """ 535 + xs = self.xmlstream 536 + xs.dataReceived("<stream:stream xmlns='jabber:client' " 537 + "xmlns:stream='http://etherx.jabber.org/streams' " 538 + "to='example.org' " 539 + "version='1.0'>") 540 + xs.output = [] 541 + response = b64encode('\x00'.join(['', 'test', 'secret'])) 542 + xs.dataReceived("<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' " 543 + "mechanism='PLAIN'>%s</auth>" % response) 544 + self.assertTrue(iwokkel.IUserSession.providedBy(self.xmlstream.avatar)) 545 + self.assertFalse(xs.headerSent) 546 + self.assertEqual(1, len(xs.output)) 547 + self.assertFalse(self.authenticator.initialized) 548 + 549 + 550 + def test_authInvalidMechanism(self): 551 + """ 552 + Authenticating with an invalid SASL mechanism causes a streamError. 553 + """ 554 + xs = self.xmlstream 555 + xs.dataReceived("<stream:stream xmlns='jabber:client' " 556 + "xmlns:stream='http://etherx.jabber.org/streams' " 557 + "to='example.org' " 558 + "version='1.0'>") 559 + xs.output = [] 560 + xs.dataReceived("<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' " 561 + "mechanism='unknown'/>") 562 + self.assertTrue(xpath.matches(("/failure[@xmlns='%s']" 563 + "/invalid-mechanism[@xmlns='%s']" % 564 + (NS_XMPP_SASL, NS_XMPP_SASL)), 565 + xs.output[-1])) 566 + 567 + 568 + def test_authFail(self): 569 + """ 570 + Authenticating causes an avatar to be set on the authenticator. 571 + """ 572 + xs = self.xmlstream 573 + xs.dataReceived("<stream:stream xmlns='jabber:client' " 574 + "xmlns:stream='http://etherx.jabber.org/streams' " 575 + "to='example.org' " 576 + "version='1.0'>") 577 + xs.output = [] 578 + response = b64encode('\x00'.join(['', 'test', 'bad'])) 579 + xs.dataReceived("<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' " 580 + "mechanism='PLAIN'>%s</auth>" % response) 581 + self.assertIdentical(None, self.xmlstream.avatar) 582 + self.assertTrue(xs.headerSent) 583 + 584 + self.assertTrue(xpath.matches(("/failure[@xmlns='%s']" 585 + "/not-authorized[@xmlns='%s']" % 586 + (NS_XMPP_SASL, NS_XMPP_SASL)), 587 + xs.output[-1])) 588 + 589 + self.assertFalse(self.authenticator.initialized) 590 + 591 + 592 + def test_authFailMultiple(self): 593 + """ 594 + Authenticating causes an avatar to be set on the authenticator. 595 + """ 596 + xs = self.xmlstream 597 + xs.dataReceived("<stream:stream xmlns='jabber:client' " 598 + "xmlns:stream='http://etherx.jabber.org/streams' " 599 + "to='example.org' " 600 + "version='1.0'>") 601 + 602 + xs.output = [] 603 + response = b64encode('\x00'.join(['', 'test', 'bad'])) 604 + 605 + attempts = self.authenticator.initializer.failureGrace 606 + for attempt in xrange(attempts): 607 + xs.dataReceived("<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' " 608 + "mechanism='PLAIN'>%s</auth>" % response) 609 + self.assertTrue(xpath.matches(("/failure[@xmlns='%s']" 610 + "/not-authorized[@xmlns='%s']" % 611 + (NS_XMPP_SASL, NS_XMPP_SASL)), 612 + xs.output[-1])) 613 + self.xmlstream.assertStreamError(self, condition='policy-violation') 614 + self.assertFalse(self.authenticator.initialized) 615 + 616 + 617 + def test_authException(self): 618 + """ 619 + Other authentication exceptions yield temporary-auth-failure. 620 + """ 621 + class Error(Exception): 622 + pass 623 + 624 + def login(credentials, mind, *interfaces): 625 + raise Error() 626 + 627 + self.portal.login = login 628 + 629 + xs = self.xmlstream 630 + xs.dataReceived("<stream:stream xmlns='jabber:client' " 631 + "xmlns:stream='http://etherx.jabber.org/streams' " 632 + "to='example.org' " 633 + "version='1.0'>") 634 + xs.output = [] 635 + response = b64encode('\x00'.join(['', 'test', 'bad'])) 636 + xs.dataReceived("<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' " 637 + "mechanism='PLAIN'>%s</auth>" % response) 638 + self.assertIdentical(None, self.xmlstream.avatar) 639 + self.assertTrue(xs.headerSent) 640 + 641 + self.assertTrue(xpath.matches(("/failure[@xmlns='%s']" 642 + "/temporary-auth-failure[@xmlns='%s']" % 643 + (NS_XMPP_SASL, NS_XMPP_SASL)), 644 + xs.output[-1])) 645 + self.assertFalse(self.authenticator.initialized) 646 + self.assertEqual(1, len(self.flushLoggedErrors(Error))) 647 + 648 + 649 + def test_authNonAsciiUsername(self): 650 + """ 651 + Authenticating causes an avatar to be set on the authenticator. 652 + """ 653 + xs = self.xmlstream 654 + xs.dataReceived("<stream:stream xmlns='jabber:client' " 655 + "xmlns:stream='http://etherx.jabber.org/streams' " 656 + "to='example.org' " 657 + "version='1.0'>") 658 + xs.output = [] 659 + response = b64encode('\x00'.join(['', 'test\xa1', 'secret'])) 660 + xs.dataReceived("<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' " 661 + "mechanism='PLAIN'>%s</auth>" % response) 662 + self.assertIdentical(None, self.xmlstream.avatar) 663 + self.assertTrue(xs.headerSent) 664 + 665 + self.assertEqual(1, len(xs.output)) 666 + failure = xs.output[-1] 667 + condition = failure.elements().next() 668 + self.assertEqual('not-authorized', condition.name) 669 + 670 + 671 + def test_authAuthorizationIdentifier(self): 672 + """ 673 + Authorization Identifiers are not supported. 674 + """ 675 + xs = self.xmlstream 676 + xs.dataReceived("<stream:stream xmlns='jabber:client' " 677 + "xmlns:stream='http://etherx.jabber.org/streams' " 678 + "to='example.org' " 679 + "version='1.0'>") 680 + xs.output = [] 681 + response = b64encode('\x00'.join(['other', 'test', 'secret'])) 682 + xs.dataReceived("<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' " 683 + "mechanism='PLAIN'>%s</auth>" % response) 684 + self.assertIdentical(None, self.xmlstream.avatar) 685 + self.assertTrue(xs.headerSent) 686 + 687 + self.assertEqual(1, len(xs.output)) 688 + failure = xs.output[-1] 689 + condition = failure.elements().next() 690 + self.assertEqual('invalid-authz', condition.name) 691 + 692 + 693 + 694 +class BindReceivingInitializerTest(unittest.TestCase): 695 + """ 696 + Tests for L{client.BindReceivingInitializer}. 697 + """ 698 + 699 + def setUp(self): 700 + def getInitializers(self): 701 + self.initializer = client.BindReceivingInitializer('bind', 702 + self.xmlstream) 703 + return [self.initializer] 704 + 705 + self.authenticator = TestableFeatureListenAuthenticator(getInitializers) 706 + self.xmlstream = self.authenticator.xmlstream 707 + self.xmlstream.avatar = TestSession('example.org', 'test') 708 + 709 + 710 + def test_getFeatures(self): 711 + """ 712 + The stream features include resource binding. 713 + """ 714 + xs = self.xmlstream 715 + xs.dataReceived("<stream:stream xmlns='jabber:client' " 716 + "xmlns:stream='http://etherx.jabber.org/streams' " 717 + "to='example.org' " 718 + "version='1.0'>") 719 + 720 + features = xs.output[-1] 721 + self.assertTrue(xpath.matches("/features[@xmlns='%s']" 722 + "/bind[@xmlns='%s']" % 723 + (NS_STREAMS, NS_XMPP_BIND), 724 + features)) 725 + 726 + 727 + def test_bind(self): 728 + """ 729 + To bind a resource, the avatar is requested one and a JID is returned. 730 + """ 731 + xs = self.xmlstream 732 + xs.dataReceived("<stream:stream xmlns='jabber:client' " 733 + "xmlns:stream='http://etherx.jabber.org/streams' " 734 + "to='example.org' " 735 + "version='1.0'>") 736 + 737 + # This initializer is required. 738 + self.assertFalse(self.authenticator.initialized) 739 + 740 + xs.output = [] 741 + xs.dataReceived("""<iq type='set'> 742 + <bind xmlns='%s'>Home</bind> 743 + </iq>""" % NS_XMPP_BIND) 744 + 745 + self.assertTrue(xs.headerSent, "Unexpected stream restart") 746 + 747 + # In response to the bind request, a result iq and the new stream 748 + # features are sent 749 + response = xs.output[-2] 750 + self.assertTrue(xpath.matches("/iq[@type='result']" 751 + "/bind[@xmlns='%s']" 752 + "/jid[@xmlns='%s' and " 753 + "text()='%s']" % 754 + (NS_XMPP_BIND, 755 + NS_XMPP_BIND, 756 + 'test@example.org/Home'), 757 + response)) 758 + 759 + self.assertTrue(self.authenticator.initialized) 760 + 761 + 762 + 763 +class SessionReceivingInitializerTest(unittest.TestCase): 764 + """ 765 + Tests for L{client.SessionReceivingInitializer}. 766 + """ 767 + 768 + def setUp(self): 769 + def getInitializers(self): 770 + self.initializer = client.SessionReceivingInitializer('session', 771 + self.xmlstream) 772 + return [self.initializer] 773 + 774 + self.authenticator = TestableFeatureListenAuthenticator(getInitializers) 775 + self.xmlstream = self.authenticator.xmlstream 776 + 777 + 778 + def test_getFeatures(self): 779 + """ 780 + The stream features include session establishment. 781 + """ 782 + xs = self.xmlstream 783 + xs.dataReceived("<stream:stream xmlns='jabber:client' " 784 + "xmlns:stream='http://etherx.jabber.org/streams' " 785 + "to='example.org' " 786 + "version='1.0'>") 787 + 788 + features = xs.output[-1] 789 + self.assertTrue(xpath.matches("/features[@xmlns='%s']" 790 + "/session[@xmlns='%s']" % 791 + (NS_STREAMS, NS_XMPP_SESSION), 792 + features)) 793 + 794 + def test_session(self): 795 + """ 796 + Session establishment is a no-op iq exchange. 797 + """ 798 + xs = self.xmlstream 799 + xs.dataReceived("<stream:stream xmlns='jabber:client' " 800 + "xmlns:stream='http://etherx.jabber.org/streams' " 801 + "to='example.org' " 802 + "version='1.0'>") 803 + 804 + # This initializer is not required. 805 + self.assertTrue(self.authenticator.initialized) 806 + 807 + # If resource binding has completed, xs.otherEntity has been set. 808 + xs.otherEntity = JID('test@example.org/Home') 809 + 810 + xs.output = [] 811 + xs.dataReceived("""<iq type='set'> 812 + <session xmlns='%s'/> 813 + </iq>""" % NS_XMPP_SESSION) 814 + 815 + self.assertTrue(xs.headerSent, "Unexpected stream restart") 816 + 817 + # In response to the session request, a result iq and the new stream 818 + # features are sent 819 + response = xs.output[-2] 820 + self.assertTrue(xpath.matches("/iq[@type='result']", response)) 821 + 822 + 823 + 824 + def test_sessionNoBind(self): 825 + """ 826 + Session establishment requires resource binding being completed. 827 + """ 828 + xs = self.xmlstream 829 + xs.dataReceived("<stream:stream xmlns='jabber:client' " 830 + "xmlns:stream='http://etherx.jabber.org/streams' " 831 + "to='example.org' " 832 + "version='1.0'>") 833 + 834 + # This initializer is not required. 835 + self.assertTrue(self.authenticator.initialized) 836 + 837 + xs.output = [] 838 + xs.dataReceived("""<iq type='set'> 839 + <session xmlns='%s'/> 840 + </iq>""" % NS_XMPP_SESSION) 841 + 842 + self.assertTrue(xs.headerSent, "Unexpected stream restart") 843 + 844 + # In response to the session request, a result iq and the new stream 845 + # features are sent 846 + response = xs.output[-2] 847 + stanzaError = error.exceptionFromStanza(response) 848 + self.assertEqual('forbidden', stanzaError.condition) 849 + 353 850 + 354 851 + … … 359 856 + 360 857 + def setUp(self): 361 + self.output = [] 362 + realm = TestRealm() 363 + checker = InMemoryUsernamePasswordDatabaseDontUse(test='secret') 364 + portal = Portal(realm, (checker,)) 365 + portals = {JID('example.org'): portal} 858 + portals = {JID('example.org'): None} 366 859 + self.authenticator = client.XMPPClientListenAuthenticator(portals) 367 860 + self.xmlstream = TestableXmlStream(self.authenticator) … … 369 862 + 370 863 + 371 + def loseConnection(self): 372 + """ 373 + Stub loseConnection because we are a transport. 374 + """ 375 + self.xmlstream.connectionLost("no reason") 376 + 377 + 378 + def test_streamStarted(self): 379 + xs = self.xmlstream 380 + xs.dataReceived("<stream:stream xmlns='jabber:client' " 381 + "xmlns:stream='http://etherx.jabber.org/streams' " 382 + "to='example.org' " 383 + "version='1.0'>") 384 + 385 + self.assertTrue(xs.headerSent) 386 + 387 + # Extract SASL mechanisms 388 + features = xs.output[-1] 389 + self.assertEquals(NS_STREAMS, features.uri) 390 + self.assertEquals('features', features.name) 391 + parent = features.elements(NS_XMPP_SASL, 'mechanisms').next() 392 + mechanisms = set() 393 + for child in parent.elements(NS_XMPP_SASL, 'mechanism'): 394 + mechanisms.add(unicode(child)) 395 + 396 + self.assertIn('PLAIN', mechanisms) 864 + def test_getInitializersStart(self): 865 + """ 866 + Upon the start of negotation, only the SASL initializer is available. 867 + """ 868 + inits = self.authenticator.getInitializers() 869 + (init,) = inits 870 + self.assertEqual('sasl', init.name) 871 + self.assertIsInstance(init, client.SASLReceivingInitializer) 872 + 873 + 874 + def test_getInitializersPostSASL(self): 875 + """ 876 + After SASL, the resource binding and session establishment initializers 877 + are available. 878 + """ 879 + self.authenticator.completedInitializers = ['sasl'] 880 + inits = self.authenticator.getInitializers() 881 + (bind, session) = inits 882 + self.assertEqual('bind', bind.name) 883 + self.assertIsInstance(bind, client.BindReceivingInitializer) 884 + self.assertEqual('session', session.name) 885 + self.assertIsInstance(session, client.SessionReceivingInitializer) 397 886 + 398 887 + … … 406 895 + "to='example.org' " 407 896 + "version='1.0'>") 408 + streamError = xs.streamErrors[-1] 409 + self.assertEquals('invalid-namespace', streamError.condition) 897 + self.xmlstream.assertStreamError(self, condition='invalid-namespace') 410 898 + 411 899 + … … 418 906 + "xmlns:stream='http://etherx.jabber.org/streams' " 419 907 + "version='1.0'>") 420 + streamError = xs.streamErrors[-1] 421 + self.assertEquals('improper-addressing', streamError.condition) 908 + self.xmlstream.assertStreamError(self, condition='improper-addressing') 422 909 + 423 910 + … … 431 918 + "to='example.com' " 432 919 + "version='1.0'>") 433 + streamError = xs.streamErrors[-1] 434 + self.assertEquals('host-unknown', streamError.condition) 435 + 436 + 437 + def test_auth(self): 438 + """ 439 + Authenticating causes an avatar to be set on the authenticator. 440 + """ 441 + xs = self.xmlstream 442 + xs.dataReceived("<stream:stream xmlns='jabber:client' " 443 + "xmlns:stream='http://etherx.jabber.org/streams' " 444 + "to='example.org' " 445 + "version='1.0'>") 446 + xs.output = [] 447 + xs.dataReceived("<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' " 448 + "mechanism='PLAIN'>AHRlc3QAc2VjcmV0</auth>") 449 + self.assertTrue(client.IAccount.providedBy(self.xmlstream.avatar)) 450 + 451 + 452 + def test_authInvalidMechanism(self): 453 + """ 454 + Authenticating with an invalid SASL mechanism causes a streamError. 455 + """ 456 + xs = self.xmlstream 457 + xs.dataReceived("<stream:stream xmlns='jabber:client' " 458 + "xmlns:stream='http://etherx.jabber.org/streams' " 459 + "to='example.org' " 460 + "version='1.0'>") 461 + xs.output = [] 462 + xs.dataReceived("<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' " 463 + "mechanism='unknown'/>") 464 + xpath.matches(("/failure[@xmlns='%s']" 465 + "/invalid-mechanism[@xmlns='%s']" % 466 + (NS_XMPP_SASL, NS_XMPP_SASL)), 467 + xs.output[-1]) 920 + self.xmlstream.assertStreamError(self, condition='host-unknown') 921 diff --git a/wokkel/test/test_generic.py b/wokkel/test/test_generic.py 922 --- a/wokkel/test/test_generic.py 923 +++ b/wokkel/test/test_generic.py 924 @@ -331,15 +331,12 @@ 925 """ 926 927 def setUp(self): 928 - self.gotAuthenticated = False 929 - self.initFailure = None 930 + self.gotAuthenticated = 0 931 self.authenticator = generic.FeatureListenAuthenticator() 932 self.authenticator.namespace = 'jabber:server' 933 self.xmlstream = generic.TestableXmlStream(self.authenticator) 934 self.xmlstream.addObserver('//event/stream/authd', 935 self.onAuthenticated) 936 - self.xmlstream.addObserver('//event/xmpp/initfailed', 937 - self.onInitFailed) 938 939 self.init = TestableReceivingInitializer('init', self.xmlstream, 940 'testns', 'test') 941 @@ -351,11 +348,7 @@ 942 943 944 def onAuthenticated(self, obj): 945 - self.gotAuthenticated = True 946 - 947 - 948 - def onInitFailed(self, failure): 949 - self.initFailure = failure 950 + self.gotAuthenticated += 1 951 952 953 def test_getInitializers(self): 954 @@ -537,6 +530,38 @@ 955 " <query xmlns='jabber:iq:version'/>" 956 "</iq>") 957 958 + def test_streamStartedInitializerNotRequired(self): 959 + """ 960 + If no initializers are required, initialization is done. 961 + """ 962 + self.init.required = False 963 + xs = self.xmlstream 964 + xs.makeConnection(proto_helpers.StringTransport()) 965 + xs.dataReceived("<stream:stream xmlns='jabber:server' " 966 + "xmlns:stream='http://etherx.jabber.org/streams' " 967 + "from='example.com' to='example.org' id='12345' " 968 + "version='1.0'>") 969 + 970 + self.assertEqual(1, self.gotAuthenticated) 971 + 972 + 973 + def test_streamStartedInitializerNotRequiredDoneOnce(self): 974 + """ 975 + If no initializers are required, the authd event is not sent again. 976 + """ 977 + self.init.required = False 978 + xs = self.xmlstream 979 + xs.makeConnection(proto_helpers.StringTransport()) 980 + xs.dataReceived("<stream:stream xmlns='jabber:server' " 981 + "xmlns:stream='http://etherx.jabber.org/streams' " 982 + "from='example.com' to='example.org' id='12345' " 983 + "version='1.0'>") 984 + 985 + self.assertEqual(1, self.gotAuthenticated) 986 + xs.output = [] 987 + self.init.deferred.callback(None) 988 + self.assertEqual(1, self.gotAuthenticated) 989 + 990 991 def test_streamStartedXmlStanzasHandledIgnored(self): 992 """ -
listening-authenticator-stream-features.patch
r65 r66 1 1 # HG changeset patch 2 # Parent 8b17590769db3088336f1fa65710c48e5ad5dcc12 # Parent 9393ad83138bbe6ab1c5249a6a115b8e56144622 3 3 Add FeatureListeningAuthenticator. 4 4 … … 6 6 for stream negotiation, similar to inializers for clients. 7 7 8 TODO:9 10 * Add docstrings.11 * Add support for stream restarts.12 13 8 diff --git a/wokkel/generic.py b/wokkel/generic.py 14 9 --- a/wokkel/generic.py 15 10 +++ b/wokkel/generic.py 11 @@ -10,13 +10,13 @@ 12 from zope.interface import implements 13 14 from twisted.internet import defer, protocol 15 -from twisted.python import reflect 16 +from twisted.python import log, reflect 17 from twisted.words.protocols.jabber import error, jid, xmlstream 18 from twisted.words.protocols.jabber.xmlstream import toResponse 19 from twisted.words.xish import domish, utility 20 from twisted.words.xish.xmlstream import BootstrapMixin 21 22 -from wokkel.iwokkel import IDisco 23 +from wokkel.iwokkel import IDisco, IReceivingInitializer 24 from wokkel.subprotocols import XMPPHandler 25 26 IQ_GET = '/iq[@type="get"]' 16 27 @@ -25,6 +25,8 @@ 17 28 NS_VERSION = 'jabber:iq:version' … … 23 34 """ 24 35 Parse serialized XML into a DOM structure. 25 @@ -327,3 +329, 116@@36 @@ -327,3 +329,287 @@ 26 37 27 38 def clientConnectionFailed(self, connector, reason): … … 31 42 + 32 43 +class TestableXmlStream(xmlstream.XmlStream): 44 + """ 45 + XML Stream that buffers outgoing data and catches special events. 46 + 47 + This implementation overrides relevant methods to prevent any data 48 + to be sent out on a transport. Instead it buffers all outgoing stanzas, 49 + sets flags instead of sending the stream header and footer and logs stream 50 + errors so it can be caught by a logging observer. 51 + 52 + @ivar output: Sequence of objects sent out using L{send}. Usually these are 53 + L{domish.Element} instances. 54 + @type output: C{list} 55 + 56 + @ivar headerSent: Flag set when a stream header would have been sent. When 57 + a stream restart occurs through L{reset}, this flag is reset as well. 58 + @type headerSent: C{bool} 59 + 60 + @ivar footerSent: Flag set when a stream footer would have been sent 61 + explicitly. Note that it is not set when a stream error is sent 62 + using L{sendStreamError}. 63 + @type footerSent: C{bool} 64 + """ 65 + 33 66 + 34 67 + def __init__(self, authenticator): … … 36 69 + self.headerSent = False 37 70 + self.footerSent = False 38 + self.streamError = None39 71 + self.output = [] 40 72 + … … 54 86 + 55 87 + def sendStreamError(self, streamError): 56 + self.streamError = streamError 88 + """ 89 + Log a stream error. 90 + 91 + If this is called from a Twisted Trial test case, the stream error 92 + will be observed by the Trial logging observer. If it is not explicitly 93 + tested for (i.e. flushed), this will cause the test case to 94 + automatically fail. See L{assertStreamError} for a convenience method 95 + to test for stream errors. 96 + 97 + @type streamError: L{error.StreamError} 98 + """ 99 + log.err(streamError) 100 + 101 + 102 + @staticmethod 103 + def assertStreamError(testcase, condition=None, exc=None): 104 + """ 105 + Check if a stream error was sent out. 106 + 107 + To check for stream errors sent out by L{sendStreamError}, this method 108 + will flush logged stream errors and inspect the last one. If 109 + C{condition} was passed, the logged error is asserted to match that 110 + condition. If C{exc} was passed, the logged error is asserted to be 111 + identical to it. 112 + 113 + Note that this is takes the calling test case as the first argument, to 114 + be able to hook into its methods for flushing errors and making 115 + assertions. 116 + 117 + @param testcase: The test case instance that is calling this method. 118 + @type testcase: {twisted.trial.unittest.TestCase} 119 + 120 + @param condition: The optional stream error condition to match against. 121 + @type condition: C{unicode}. 122 + 123 + @param exc: The optional stream error to check identity against. 124 + @type exc: L{error.StreamError} 125 + """ 126 + 127 + loggedErrors = testcase.flushLoggedErrors(error.StreamError) 128 + testcase.assertTrue(loggedErrors, "No stream error was sent") 129 + streamError = loggedErrors[-1].value 130 + if condition: 131 + testcase.assertEqual(condition, streamError.condition) 132 + elif exc: 133 + testcase.assertIdentical(exc, streamError) 57 134 + 58 135 + 59 136 + def send(self, obj): 137 + """ 138 + Buffer all outgoing stanzas. 139 + 140 + @type obj: L{domish.Element} 141 + """ 60 142 + self.output.append(obj) 143 + 144 + 145 + 146 +class BaseReceivingInitializer(object): 147 + """ 148 + Base stream initializer for receiving entities. 149 + """ 150 + implements(IReceivingInitializer) 151 + 152 + required = False 153 + 154 + def __init__(self, name, xs): 155 + self.name = name 156 + self.xmlstream = xs 157 + self.deferred = defer.Deferred() 158 + 159 + 160 + def getFeatures(self): 161 + raise NotImplementedError() 162 + 163 + 164 + def initialize(self): 165 + return self.deferred 61 166 + 62 167 + … … 91 196 + 92 197 + 198 + def connectionMade(self): 199 + """ 200 + Called when the connection has been made. 201 + 202 + Adds an observer to reject XML Stanzas until stream feature negotiation 203 + has completed. 204 + """ 205 + xmlstream.ListenAuthenticator.connectionMade(self) 206 + self.xmlstream.addObserver(XPATH_ALL, self._onElementFallback, -1) 207 + 208 + 209 + def _cbInit(self, result): 210 + """ 211 + Mark the initializer as completed and continue to the next. 212 + """ 213 + result, index = result 214 + self.completedInitializers.append(self._initializers[index].name) 215 + del self._initializers[index] 216 + 217 + if result is xmlstream.Reset: 218 + # The initializer initiated a stream restart, bail. 219 + return 220 + else: 221 + self._initializeStream() 222 + 223 + 224 + def _ebInit(self, failure): 225 + """ 226 + Called when an initializer raises an exception. 227 + 228 + If the exception is a L{error.StreamError} it is sent out, otherwise 229 + the error is logged and a stream error with condition 230 + C{'internal-server-error'} is sent out instead. 231 + """ 232 + firstError = failure.value 233 + subFailure = firstError.subFailure 234 + if subFailure.check(error.StreamError): 235 + exc = subFailure.value 236 + else: 237 + log.err(subFailure, index=firstError.index) 238 + exc = error.StreamError('internal-server-error') 239 + 240 + self.xmlstream.sendStreamError(exc) 241 + 242 + 93 243 + def _initializeStream(self): 94 + def cb(result): 95 + result, index = result 96 + self.completedInitializers.append(self._initializers[index]) 97 + del self._initializers[index] 98 + self._initializeStream() 99 + 244 + """ 245 + Initialize the stream. 246 + 247 + This walks all initializers to retrieve their features and determine 248 + if there is at least one required initializer. If not, the stream is 249 + ready for the exchange of stanzas. The features are sent out and each 250 + initializer will have its C{initialize} method called. 251 + 252 + If negation has completed, L{xmlstream.STREAM_AUTHD_EVENT} is 253 + dispatched and the observer rejecting incoming stanzas is removed. 254 + """ 100 255 + features = domish.Element((xmlstream.NS_STREAMS, 'features')) 101 256 + ds = [] … … 107 262 + d = initializer.initialize() 108 263 + ds.append(d) 264 + 109 265 + self.xmlstream.send(features) 266 + 110 267 + if not required: 268 + # There are no required initializers anymore. This stream is 269 + # now ready for the exchange of stanzas. 111 270 + self.xmlstream.removeObserver(XPATH_ALL, self._onElementFallback) 112 271 + self.xmlstream.dispatch(self.xmlstream, xmlstream.STREAM_AUTHD_EVENT) 272 + 113 273 + if ds: 114 + d = defer.DeferredList(ds, fireOnOneCallback=True) 115 + d.addCallback(cb) 274 + d = defer.DeferredList(ds, fireOnOneCallback=True, 275 + fireOnOneErrback=True, 276 + consumeErrors=True) 277 + d.addCallbacks(self._cbInit, self._ebInit) 278 + 116 279 + 117 280 + def getInitializers(self): 118 + return [] 281 + """ 282 + Get the initializers for the current stage of stream negotiation. 283 + 284 + This will be called at the start of each stream start to retrieve 285 + the initializers for which the features are advertised and initiated. 286 + 287 + @rtype: C{list} of C{IReceivingInitializer} instances. 288 + """ 289 + raise NotImplementedError() 119 290 + 120 291 + 121 292 + def checkStream(self): 122 + # check namespace 293 + """ 294 + Check the stream before sending out a stream header and initialization. 295 + 296 + This is the place to inspect the stream properties and raise a relevant 297 + L{error.StreamError} if needed. 298 + """ 299 + # Check stream namespace 123 300 + if self.xmlstream.namespace != self.namespace: 124 301 + self.xmlstream.namespace = self.namespace … … 127 304 + 128 305 + def streamStarted(self, rootElement): 306 + """ 307 + Called when the stream header has been received. 308 + 309 + Check the stream properties through L{checkStream}, send out 310 + a stream header, and retrieve and initialize the stream initializers. 311 + """ 129 312 + xmlstream.ListenAuthenticator.streamStarted(self, rootElement) 130 313 + … … 135 318 + return 136 319 + 137 + self.xmlstream.addObserver(XPATH_ALL, self._onElementFallback, -1)138 320 + self.xmlstream.sendHeader() 139 321 + 140 322 + self._initializers = self.getInitializers() 141 323 + self._initializeStream() 324 diff --git a/wokkel/iwokkel.py b/wokkel/iwokkel.py 325 --- a/wokkel/iwokkel.py 326 +++ b/wokkel/iwokkel.py 327 @@ -11,7 +11,7 @@ 328 'IPubSubClient', 'IPubSubService', 'IPubSubResource', 329 'IMUCClient', 'IMUCStatuses'] 330 331 -from zope.interface import Interface 332 +from zope.interface import Attribute, Interface 333 from twisted.python.deprecate import deprecatedModuleAttribute 334 from twisted.python.versions import Version 335 from twisted.words.protocols.jabber.ijabber import IXMPPHandler 336 @@ -982,3 +982,50 @@ 337 """ 338 Return the number of status conditions. 339 """ 340 + 341 + 342 + 343 +class IReceivingInitializer(Interface): 344 + """ 345 + Interface for XMPP stream initializers for receiving entities. 346 + """ 347 + 348 + required = Attribute( 349 + """ 350 + This initializer is required to complete feature negotiation. 351 + """) 352 + name = Attribute( 353 + """ 354 + Identifier for this initializer. 355 + 356 + This identifier is included in 357 + L{wokkel.generic.FeatureListenAuthenticator} when an initializer has 358 + completed. 359 + """) 360 + xmlstream = Attribute( 361 + """ 362 + The XML Stream. 363 + """) 364 + deferred = Attribute( 365 + """ 366 + The deferred returned from initialize. 367 + """) 368 + 369 + 370 + def getFeatures(): 371 + """ 372 + Get stream features for this initializer. 373 + 374 + @rtype: C{list} of L{twisted.words.xish.domish.Element} 375 + """ 376 + 377 + 378 + def initialize(): 379 + """ 380 + Initialize the initializer. 381 + 382 + This is where observers for feature negotiation are set up. When 383 + the returned deferred fires, it is assumed to have completed. 384 + 385 + @rtype: L{twisted.internet.defer.Deferred} 386 + """ 142 387 diff --git a/wokkel/test/test_generic.py b/wokkel/test/test_generic.py 143 388 --- a/wokkel/test/test_generic.py 144 389 +++ b/wokkel/test/test_generic.py 145 @@ -5, 9 +5,12@@390 @@ -5,11 +5,16 @@ 146 391 Tests for L{wokkel.generic}. 147 392 """ 148 393 394 +from zope.interface import verify 395 + 149 396 +from twisted.internet import defer 150 397 +from twisted.test import proto_helpers … … 154 401 +from twisted.words.protocols.jabber import error, xmlstream 155 402 156 from wokkel import generic 403 -from wokkel import generic 404 +from wokkel import generic, iwokkel 157 405 from wokkel.test.helpers import XmlStreamStub 158 @@ -268,3 +271,218 @@ 406 407 NS_VERSION = 'jabber:iq:version' 408 @@ -268,3 +273,331 @@ 159 409 The default is no timeout. 160 410 """ … … 163 413 + 164 414 + 165 +class TestableReceivingInitializer(object): 415 +class BaseReceivingInitializerTest(unittest.TestCase): 416 + """ 417 + Tests for L{generic.BaseReceivingInitializer}. 418 + """ 419 + 420 + def setUp(self): 421 + self.init = generic.BaseReceivingInitializer('init', None) 422 + 423 + 424 + def test_interface(self): 425 + verify.verifyObject(iwokkel.IReceivingInitializer, self.init) 426 + 427 + 428 + def test_getFeatures(self): 429 + self.assertRaises(NotImplementedError, self.init.getFeatures) 430 + 431 + 432 + def test_initialize(self): 433 + d = self.init.initialize() 434 + self.init.deferred.callback(None) 435 + return d 436 + 437 + 438 + 439 +class TestableReceivingInitializer(generic.BaseReceivingInitializer): 166 440 + """ 167 441 + Testable initializer for receiving entities. … … 172 446 + 173 447 + @ivar uri: Namespace of the stream feature. 174 + @ivar name: Element localname for the stream feature.448 + @ivar localname: Element localname for the stream feature. 175 449 + """ 176 450 + required = True 177 451 + 178 + def __init__(self, xs, uri,name):179 + self.xmlstream = xs452 + def __init__(self, name, xs, uri, localname): 453 + generic.BaseReceivingInitializer.__init__(self, name, xs) 180 454 + self.uri = uri 181 + self. name =name455 + self.localname = localname 182 456 + self.deferred = defer.Deferred() 183 457 + 184 458 + 185 459 + def getFeatures(self): 186 + return [domish.Element((self.uri, self.name))] 187 + 188 + 189 + def initialize(self): 190 + return self.deferred 460 + return [domish.Element((self.uri, self.localname))] 461 + 462 + 191 463 + 192 464 +class FeatureListenAuthenticatorTest(unittest.TestCase): … … 206 478 + self.onInitFailed) 207 479 + 208 + self.init = TestableReceivingInitializer(self.xmlstream, 'testns', 'test') 480 + self.init = TestableReceivingInitializer('init', self.xmlstream, 481 + 'testns', 'test') 209 482 + 210 483 + def getInitializers(): … … 220 493 + def onInitFailed(self, failure): 221 494 + self.initFailure = failure 495 + 496 + 497 + def test_getInitializers(self): 498 + """ 499 + Unoverridden getInitializers raises NotImplementedError. 500 + """ 501 + authenticator = generic.FeatureListenAuthenticator() 502 + self.assertRaises( 503 + NotImplementedError, 504 + authenticator.getInitializers) 222 505 + 223 506 + … … 231 514 + themselves up. 232 515 + """ 233 +234 516 + xs = self.xmlstream 235 517 + xs.makeConnection(proto_helpers.StringTransport()) … … 255 537 + 256 538 + 539 + def test_streamStartedStreamError(self): 540 + """ 541 + A stream error raised by the initializer is sent out. 542 + """ 543 + xs = self.xmlstream 544 + xs.makeConnection(proto_helpers.StringTransport()) 545 + xs.dataReceived("<stream:stream xmlns='jabber:server' " 546 + "xmlns:stream='http://etherx.jabber.org/streams' " 547 + "from='example.com' to='example.org' id='12345' " 548 + "version='1.0'>") 549 + 550 + self.assertTrue(xs.headerSent) 551 + 552 + xs.output = [] 553 + exc = error.StreamError('policy-violation') 554 + self.init.deferred.errback(exc) 555 + 556 + self.xmlstream.assertStreamError(self, exc=exc) 557 + self.assertFalse(xs.output) 558 + self.assertFalse(self.gotAuthenticated) 559 + 560 + 561 + def test_streamStartedOtherError(self): 562 + """ 563 + Initializer exceptions are logged and yield a internal-server-error. 564 + """ 565 + xs = self.xmlstream 566 + xs.makeConnection(proto_helpers.StringTransport()) 567 + xs.dataReceived("<stream:stream xmlns='jabber:server' " 568 + "xmlns:stream='http://etherx.jabber.org/streams' " 569 + "from='example.com' to='example.org' id='12345' " 570 + "version='1.0'>") 571 + 572 + self.assertTrue(xs.headerSent) 573 + 574 + xs.output = [] 575 + class Error(Exception): 576 + pass 577 + self.init.deferred.errback(Error()) 578 + 579 + self.xmlstream.assertStreamError(self, condition='internal-server-error') 580 + self.assertFalse(xs.output) 581 + self.assertFalse(self.gotAuthenticated) 582 + self.assertEqual(1, len(self.flushLoggedErrors(Error))) 583 + 584 + 257 585 + def test_streamStartedInitializerCompleted(self): 258 586 + """ … … 266 594 + "version='1.0'>") 267 595 + 596 + xs.output = [] 268 597 + self.init.deferred.callback(None) 269 + self.assertEqual([self.init], self.authenticator.completedInitializers) 598 + self.assertEqual(['init'], self.authenticator.completedInitializers) 599 + 600 + 601 + def test_streamStartedInitializerCompletedFeatures(self): 602 + """ 603 + After completing an initializer, stream features are sent again. 604 + 605 + In this case, with only one initializer, there are no more features. 606 + """ 607 + xs = self.xmlstream 608 + xs.makeConnection(proto_helpers.StringTransport()) 609 + xs.dataReceived("<stream:stream xmlns='jabber:server' " 610 + "xmlns:stream='http://etherx.jabber.org/streams' " 611 + "from='example.com' to='example.org' id='12345' " 612 + "version='1.0'>") 613 + 614 + xs.output = [] 615 + self.init.deferred.callback(None) 616 + 617 + self.assertEqual(1, len(xs.output)) 618 + features = xs.output[-1] 619 + self.assertEqual('features', features.name) 620 + self.assertEqual(xmlstream.NS_STREAMS, features.uri) 621 + self.assertFalse(features.children) 622 + 623 + 624 + def test_streamStartedInitializerCompletedReset(self): 625 + """ 626 + If an initializer completes with Reset, no features are sent. 627 + """ 628 + xs = self.xmlstream 629 + xs.makeConnection(proto_helpers.StringTransport()) 630 + xs.dataReceived("<stream:stream xmlns='jabber:server' " 631 + "xmlns:stream='http://etherx.jabber.org/streams' " 632 + "from='example.com' to='example.org' id='12345' " 633 + "version='1.0'>") 634 + 635 + xs.output = [] 636 + self.init.deferred.callback(xmlstream.Reset) 637 + 638 + self.assertEqual(0, len(xs.output)) 270 639 + 271 640 + … … 285 654 + "</iq>") 286 655 + 287 + self. assertEqual('not-authorized', xs.streamError.condition)656 + self.xmlstream.assertStreamError(self, condition='not-authorized') 288 657 + 289 658 + … … 304 673 + " <query xmlns='jabber:iq:version'/>" 305 674 + "</iq>") 306 +307 + self.assertIdentical(None, xs.streamError)308 675 + 309 676 + … … 325 692 + xs.dispatch(iq) 326 693 + 327 + self.assertIdentical(None, xs.streamError)328 +329 694 + 330 695 + def test_streamStartedNonXmlStanzasIgnored(self): … … 340 705 + 341 706 + xs.dataReceived("<test xmlns='myns'/>") 342 +343 + self.assertIdentical(None, xs.streamError)344 707 + 345 708 + … … 359 722 + "version='1.0'>") 360 723 + 361 + self. assertEqual('undefined-condition', xs.streamError.condition)724 + self.xmlstream.assertStreamError(self, condition='undefined-condition') 362 725 + self.assertFalse(xs.headerSent) 363 726 + … … 374 737 + "version='1.0'>") 375 738 + 376 + self. assertEqual('invalid-namespace', xs.streamError.condition)739 + self.xmlstream.assertStreamError(self, condition='invalid-namespace') -
series
r65 r66 1 1 roster_server.patch #+c2s 2 2 router_unknown.patch #+c2s 3 listening-authenticator-stream-features.patch #+c2s 3 4 4 listening-authenticator-stream-features.patch #+c2s5 5 client_listen_authenticator.patch #+c2s 6 6 7 c2s_server_factory.patch #+c2s -broken8 session_manager.patch #+c2s -broken9 c2s_stanza_handlers.patch #+c2s -broken7 c2s_server_factory.patch #+c2s 8 session_manager.patch #+c2s 9 c2s_stanza_handlers.patch #+c2s 10 10 11 11 version.patch -
session_manager.patch
r57 r66 1 1 # HG changeset patch 2 # Parent 4 e25d6deb8beeb732cadb38349ee820f0dc98b3a2 # Parent 4962500d52ed0bd4a71047c93818303accea55fd 3 3 4 diff -r 4e25d6deb8be wokkel/client.py 5 --- a/wokkel/client.py Wed Nov 30 09:32:01 2011 +0100 6 +++ b/wokkel/client.py Wed Nov 30 09:33:11 2011 +0100 7 @@ -13,15 +13,16 @@ 4 diff --git a/wokkel/client.py b/wokkel/client.py 5 --- a/wokkel/client.py 6 +++ b/wokkel/client.py 7 @@ -12,18 +12,21 @@ 8 8 9 import base64 9 10 11 +from zope.interface import implements 12 + 10 13 from twisted.application import service 11 -from twisted.internet import reactor 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 12 17 -from twisted.python import log 13 +from twisted.internet import defer, reactor14 18 +from twisted.python import log, randbytes 15 19 from twisted.names.srvconnect import SRVConnector … … 21 25 from wokkel import generic 22 26 from wokkel.compat import XmlStreamServerFactory 27 from wokkel.iwokkel import IUserSession 23 28 -from wokkel.subprotocols import StreamManager 24 29 +from wokkel.subprotocols import StreamManager, XMPPHandler … … 26 31 NS_CLIENT = 'jabber:client' 27 32 28 @@ -443,3 +444,105 @@ 29 else: 30 raise RecipientUnavailable(u"There is no connection for %s" % 31 recipient.full()) 32 + 33 + 34 +class Session(object): 33 @@ -501,3 +504,161 @@ 34 35 def onError(self, reason): 36 log.err(reason, "Stream Error") 37 + 38 + 39 + 40 +class UserSession(object): 41 + 42 + implements(IUserSession) 43 + 44 + realm = None 45 + mind = None 46 + 47 + connected = False 48 + interested = False 49 + presence = None 50 + 51 + clientStream = None 52 + 35 53 + def __init__(self, entity): 36 54 + self.entity = entity 55 + 56 + 57 + def loggedIn(self, realm, mind): 58 + self.realm = realm 59 + self.mind = mind 60 + 61 + 62 + def bindResource(self, resource): 63 + def cb(entity): 64 + self.entity = entity 65 + self.connected = True 66 + return entity 67 + 68 + d = self.realm.bindResource(self, resource) 69 + d.addCallback(cb) 70 + return d 71 + 72 + 73 + def logout(self): 37 74 + self.connected = False 38 + self.interested = False 39 + self.presence = None 75 + self.realm.unbindResource(self) 76 + 77 + 78 + def send(self, element): 79 + self.realm.onElement(element, self) 80 + 81 + 82 + def receive(self, element): 83 + self.mind.send(element) 40 84 + 41 85 + 42 86 + 43 87 +class SessionManager(XMPPHandler): 44 +45 88 + """ 46 89 + Session Manager. … … 53 96 + """ 54 97 + 98 + implements(portal.IRealm) 99 + 55 100 + def __init__(self, domain, accounts): 56 101 + XMPPHandler.__init__(self) … … 58 103 + self.accounts = accounts 59 104 + 60 + self.connectionManager = None61 105 + self.sessions = {} 62 106 + self.clientStream = utility.EventDispatcher() … … 64 108 + 65 109 + 66 + def bindResource(self, localpart, domain, resource): 67 + if domain != self.domain: 68 + raise Exception("I don't host this domain!") 110 + def requestAvatar(self, avatarId, mind, *interfaces): 111 + if IUserSession not in interfaces: 112 + raise NotImplementedError(self, interfaces) 113 + 114 + localpart = avatarId.decode('utf-8') 115 + entity = JID(tuple=(localpart, self.domain, None)) 116 + session = UserSession(entity) 117 + session.loggedIn(self, mind) 118 + return IUserSession, session, session.logout 119 + 120 + 121 + def bindResource(self, session, resource): 122 + localpart = session.entity.user 69 123 + 70 124 + try: … … 78 132 + resource = resource + ' ' + randbytes.secureRandom(8).encode('hex') 79 133 + 80 + entity = JID(tuple=(localpart, domain, resource)) 81 + session = Session(entity) 82 + session.connected = True 134 + entity = JID(tuple=(session.entity.user, session.entity.host, resource)) 83 135 + userSessions[resource] = session 84 136 + … … 86 138 + 87 139 + 88 + def unbindResource(self, localpart, domain, resource, reason=None): 89 + try: 90 + session = self.sessions[localpart][resource] 91 + except KeyError: 92 + pass 93 + else: 94 + session.connected = False 95 + del self.sessions[localpart][resource] 96 + if not self.sessions[localpart]: 97 + del self.sessions[localpart] 140 + def lookupSession(self, entity): 141 + localpart = entity.user 142 + resource = entity.resource 143 + 144 + userSessions = self.sessions[localpart] 145 + session = userSessions[resource] 146 + return session 147 + 148 + 149 + def unbindResource(self, session, reason=None): 150 + session.connected = False 151 + 152 + localpart = session.entity.user 153 + resource = session.entity.resource 154 + 155 + del self.sessions[localpart][resource] 156 + if not self.sessions[localpart]: 157 + del self.sessions[localpart] 98 158 + 99 159 + return defer.succeed(None) 100 160 + 101 161 + 102 + def onElement(self, element, se nder):162 + def onElement(self, element, session): 103 163 + # Make sure each stanza has a sender address 104 164 + if (element.name == 'presence' and 105 165 + element.getAttribute('type') in ('subscribe', 'subscribed', 106 166 + 'unsubscribe', 'unsubscribed')): 107 + element['from'] = se nder.userhost()167 + element['from'] = session.entity.userhost() 108 168 + else: 109 + element['from'] = se nder.full()169 + element['from'] = session.entity.full() 110 170 + 111 171 + self.clientStream.dispatch(element) … … 113 173 + 114 174 + def routeOrDeliver(self, element): 175 + """ 176 + Deliver a stanza locally or pass on for routing. 177 + """ 115 178 + if element.handled: 116 179 + return … … 124 187 + # This stanza is for remote routing 125 188 + log.msg("Routing remotely: %r" % element.toXml()) 126 + self.xmlstream.send(element)189 + XMPPHandler.send(self, element) 127 190 + 128 191 + 129 192 + def deliverStanza(self, element, recipient): 130 + if self.connectionManager: 131 + self.connectionManager.deliverStanza(element, recipient) 132 + else: 133 + raise Exception("No connection manager set") 193 + session = self.lookupSession(recipient) 194 + session.receive(element) 195 diff --git a/wokkel/test/test_client.py b/wokkel/test/test_client.py 196 --- a/wokkel/test/test_client.py 197 +++ b/wokkel/test/test_client.py 198 @@ -7,7 +7,7 @@ 199 200 from base64 import b64encode 201 202 -from zope.interface import implements 203 +from zope.interface import implements, verify 204 205 from twisted.cred.portal import IRealm, Portal 206 from twisted.cred.checkers import InMemoryUsernamePasswordDatabaseDontUse 207 @@ -601,6 +601,7 @@ 208 209 210 211 + 212 class XMPPClientListenAuthenticatorTest(unittest.TestCase): 213 """ 214 Tests for L{client.XMPPClientListenAuthenticator}. 215 @@ -670,3 +671,26 @@ 216 "to='example.com' " 217 "version='1.0'>") 218 self.xmlstream.assertStreamError(self, condition='host-unknown') 219 + 220 + 221 + 222 +class UserSessionTest(unittest.TestCase): 223 + 224 + def setUp(self): 225 + self.session = client.UserSession(JID('user@example.org')) 226 + 227 + 228 + def test_interface(self): 229 + verify.verifyObject(client.IUserSession, self.session) 230 + 231 + 232 + 233 +class SessionManagerTest(unittest.TestCase): 234 + 235 + def setUp(self): 236 + accounts = {'user': None} 237 + self.sessionManager = client.SessionManager('example.org', accounts) 238 + 239 + 240 + def test_interface(self): 241 + verify.verifyObject(IRealm, self.sessionManager)
Note: See TracChangeset
for help on using the changeset viewer.