source:
ralphm-patches/client_listen_authenticator.patch
@
71:7b5ddbb2b9bb
Last change on this file since 71:7b5ddbb2b9bb was 66:b713f442b222, checked in by Ralph Meijer <ralphm@…>, 10 years ago | |
---|---|
File size: 33.9 KB |
-
wokkel/client.py
# HG changeset patch # Parent a1648553ea06b7f0b38fec775db71ac03782e3d6 Add authenticator for accepting XMPP client connections. The new authenticator XMPPClientListenAuthenticator is to be used together with an `XmlStream` created for an incoming XMPP stream. It uses the new initializers for SASL (PLAIN only), resource binding and session establishement. This authenticator needs at least one Twisted Cred portal to hold the domain served. After authenticating, an avatar and a logout callback are returned. Upon binding a resource, the avatar's `bindResource` method is called with the desired resource name. Upon stream disconnect, the logout callback is called. diff --git a/wokkel/client.py b/wokkel/client.py
a b 10 10 that should probably eventually move there. 11 11 """ 12 12 13 import base64 14 13 15 from twisted.application import service 14 from twisted.internet import reactor 16 from twisted.cred import credentials, error as ecred 17 from twisted.internet import defer, reactor 18 from twisted.python import log 15 19 from twisted.names.srvconnect import SRVConnector 16 from twisted.words.protocols.jabber import client, sasl, xmlstream 20 from twisted.words.protocols.jabber import client, error, sasl, xmlstream 21 from twisted.words.xish import domish 17 22 18 23 from wokkel import generic 24 from wokkel.iwokkel import IUserSession 19 25 from wokkel.subprotocols import StreamManager 20 26 27 NS_CLIENT = 'jabber:client' 28 29 XPATH_AUTH = "/auth[@xmlns='%s']" % sasl.NS_XMPP_SASL 30 XPATH_BIND = "/iq[@type='set']/bind[@xmlns='%s']" % client.NS_XMPP_BIND 31 XPATH_SESSION = "/iq[@type='set']/session[@xmlns='%s']" % \ 32 client.NS_XMPP_SESSION 33 21 34 class CheckAuthInitializer(object): 22 35 """ 23 36 Check what authentication methods are available. … … 51 64 autentication. 52 65 """ 53 66 54 namespace = 'jabber:client'67 namespace = NS_CLIENT 55 68 56 69 def __init__(self, jid, password): 57 70 xmlstream.ConnectAuthenticator.__init__(self, jid.host) … … 186 199 c = XMPPClientConnector(reactor, domain, factory) 187 200 c.connect() 188 201 return factory.deferred 202 203 204 205 class InvalidMechanism(Exception): 206 """ 207 The requested SASL mechanism is invalid. 208 """ 209 210 211 212 class AuthorizationIdentifierNotSupported(Exception): 213 """ 214 Authorization Identifiers are not supported. 215 """ 216 217 218 219 class SASLReceivingInitializer(generic.BaseReceivingInitializer): 220 """ 221 Stream initializer for SASL authentication, receiving side. 222 """ 223 224 required = True 225 226 def __init__(self, name, xs, portal): 227 generic.BaseReceivingInitializer.__init__(self, name, xs) 228 self.portal = portal 229 self.failureGrace = 3 230 231 232 def getFeatures(self): 233 feature = domish.Element((sasl.NS_XMPP_SASL, 'mechanisms')) 234 feature.addElement('mechanism', content='PLAIN') 235 return [feature] 236 237 238 def initialize(self): 239 self.xmlstream.avatar = None 240 self.xmlstream.addObserver(XPATH_AUTH, self._onAuth) 241 return self.deferred 242 243 244 def _onAuth(self, auth): 245 """ 246 Called when the start of the SASL negotiation is received. 247 248 @type auth: L{domish.Element}. 249 """ 250 auth.handled = True 251 252 def cb(_): 253 response = domish.Element((sasl.NS_XMPP_SASL, 'success')) 254 self.xmlstream.send(response) 255 self.xmlstream.reset() 256 self.deferred.callback(xmlstream.Reset) 257 258 def eb(failure): 259 if failure.check(ecred.UnauthorizedLogin): 260 condition = 'not-authorized' 261 elif failure.check(InvalidMechanism): 262 condition = 'invalid-mechanism' 263 elif failure.check(AuthorizationIdentifierNotSupported): 264 condition = 'invalid-authz' 265 else: 266 log.err(failure) 267 condition = 'temporary-auth-failure' 268 269 response = domish.Element((sasl.NS_XMPP_SASL, 'failure')) 270 response.addElement(condition) 271 self.xmlstream.send(response) 272 273 # Close stream on too many failing authentication attempts 274 self.failureGrace -= 1 275 if self.failureGrace == 0: 276 self.deferred.errback(error.StreamError('policy-violation')) 277 else: 278 return 279 280 d = defer.maybeDeferred(self._doAuth, auth) 281 d.addCallbacks(cb, eb) 282 283 284 def _credentialsFromPlain(self, auth): 285 """ 286 Create credentials from the initial response for PLAIN. 287 """ 288 initialResponse = base64.b64decode(unicode(auth)) 289 authzid, authcid, passwd = initialResponse.split('\x00') 290 291 if authzid: 292 raise AuthorizationIdentifierNotSupported() 293 294 creds = credentials.UsernamePassword(username=authcid, 295 password=passwd) 296 return creds 297 298 299 def _doAuth(self, auth): 300 """ 301 Start authentication. 302 """ 303 if auth.getAttribute('mechanism') != 'PLAIN': 304 raise InvalidMechanism() 305 306 creds = self._credentialsFromPlain(auth) 307 308 def cb((iface, avatar, logout)): 309 self.xmlstream.avatar = avatar 310 self.xmlstream.addObserver(xmlstream.STREAM_END_EVENT, 311 lambda _: logout()) 312 313 d = self.portal.login(creds, self.xmlstream, IUserSession) 314 d.addCallback(cb) 315 return d 316 317 318 319 class BindReceivingInitializer(generic.BaseReceivingInitializer): 320 """ 321 Stream initializer for resource binding, receiving side. 322 """ 323 324 required = True 325 326 def getFeatures(self): 327 feature = domish.Element((client.NS_XMPP_BIND, 'bind')) 328 return [feature] 329 330 331 def initialize(self): 332 self.xmlstream.addOnetimeObserver(XPATH_BIND, self.onBind) 333 return self.deferred 334 335 336 def onBind(self, iq): 337 def cb(boundJID): 338 self.xmlstream.otherEntity = boundJID 339 340 response = xmlstream.toResponse(iq, 'result') 341 response.addElement((client.NS_XMPP_BIND, 'bind')) 342 response.bind.addElement((client.NS_XMPP_BIND, 'jid'), 343 content=boundJID.full()) 344 345 return response 346 347 iq.handled = True 348 resource = unicode(iq.bind) or None 349 d = self.xmlstream.avatar.bindResource(resource) 350 d.addCallback(cb) 351 d.addCallback(self.xmlstream.send) 352 d.chainDeferred(self.deferred) 353 354 355 356 class SessionReceivingInitializer(generic.BaseReceivingInitializer): 357 """ 358 Stream initializer for session establishment, receiving side. 359 360 This is mostly a no-op and just returns a result stanza. If resource 361 binding hasn't yet completed, this will return a stanza error with the 362 condition C{'forbidden'}. 363 364 Note that RFC 6120 deprecated the session establishment protocol. This 365 is provided for backwards compatibility. 366 """ 367 368 required = False 369 370 def getFeatures(self): 371 feature = domish.Element((client.NS_XMPP_SESSION, 'session')) 372 return [feature] 373 374 375 def initialize(self): 376 self.xmlstream.addOnetimeObserver(XPATH_SESSION, self.onSession, 1) 377 return self.deferred 378 379 380 def onSession(self, iq): 381 iq.handled = True 382 383 reply = domish.Element((None, 'iq')) 384 385 if self.xmlstream.otherEntity: 386 reply = xmlstream.toResponse(iq, 'result') 387 else: 388 reply = error.StanzaError('forbidden').toResponse(iq) 389 self.xmlstream.send(reply) 390 self.deferred.callback(None) 391 392 393 394 class XMPPClientListenAuthenticator(generic.FeatureListenAuthenticator): 395 """ 396 XML Stream authenticator for XMPP clients, server side. 397 398 @ivar portals: Mapping of server JIDs to Cred Portals. 399 @type portals: C{dict} of L{twisted.words.protocols.jabber.jid.JID} to 400 L{twisted.cred.portal.Portal}. 401 """ 402 403 namespace = NS_CLIENT 404 405 def __init__(self, portals): 406 generic.FeatureListenAuthenticator.__init__(self) 407 self.portals = portals 408 self.portal = None 409 410 411 def getInitializers(self): 412 """ 413 Return initializers based on previously completed initializers. 414 415 This has three stages: 1. SASL, 2. Resource binding and session 416 establishment. 3. Completed. Note that session establishment 417 is optional. 418 """ 419 if not self.completedInitializers: 420 return [SASLReceivingInitializer('sasl', self.xmlstream, self.portal)] 421 elif self.completedInitializers[-1] == 'sasl': 422 return [BindReceivingInitializer('bind', self.xmlstream), 423 SessionReceivingInitializer('session', self.xmlstream)] 424 else: 425 return [] 426 427 428 def checkStream(self): 429 """ 430 Check that the stream header has proper addressing. 431 432 The C{'to'} attribute must be present and there should have a matching 433 portal in L{portals}. 434 """ 435 generic.FeatureListenAuthenticator.checkStream(self) 436 437 if not self.xmlstream.thisEntity: 438 raise error.StreamError('improper-addressing') 439 440 # Check if we serve the domain and use the associated portal. 441 try: 442 self.portal = self.portals[self.xmlstream.thisEntity] 443 except KeyError: 444 raise error.StreamError('host-unknown') -
wokkel/generic.py
diff --git a/wokkel/generic.py b/wokkel/generic.py
a b 465 465 466 466 def __init__(self): 467 467 self.completedInitializers = [] 468 self._initialized = False 468 469 469 470 470 471 def _onElementFallback(self, element): … … 556 557 557 558 self.xmlstream.send(features) 558 559 559 if not required :560 if not required and not self._initialized: 560 561 # There are no required initializers anymore. This stream is 561 562 # now ready for the exchange of stanzas. 562 563 self.xmlstream.removeObserver(XPATH_ALL, self._onElementFallback) 563 564 self.xmlstream.dispatch(self.xmlstream, xmlstream.STREAM_AUTHD_EVENT) 565 self._initialized = True 564 566 565 567 if ds: 566 568 d = defer.DeferredList(ds, fireOnOneCallback=True, -
wokkel/iwokkel.py
diff --git a/wokkel/iwokkel.py b/wokkel/iwokkel.py
a b 985 985 986 986 987 987 988 class IUserSession(Interface): 989 def loggedIn(realm, mind): 990 """ 991 Called by the realm when login occurs. 992 993 @param realm: The realm though which login is occurring. 994 @param mind: The mind object. 995 """ 996 997 998 def bindResource(resource): 999 """ 1000 Bind a resource to this session. 1001 1002 @type resource: C{unicode}. 1003 """ 1004 1005 1006 def logout(): 1007 """ 1008 End this session. 1009 1010 This is called when the stream is disconnected. 1011 """ 1012 1013 1014 def send(element): 1015 """ 1016 Called when the client sends a stanza. 1017 """ 1018 1019 1020 def receive(element): 1021 """ 1022 Have the client receive a stanza. 1023 """ 1024 1025 1026 988 1027 class IReceivingInitializer(Interface): 989 1028 """ 990 1029 Interface for XMPP stream initializers for receiving entities. -
wokkel/test/test_client.py
diff --git a/wokkel/test/test_client.py b/wokkel/test/test_client.py
a b 5 5 Tests for L{wokkel.client}. 6 6 """ 7 7 8 from base64 import b64encode 9 10 from zope.interface import implements 11 12 from twisted.cred.portal import IRealm, Portal 13 from twisted.cred.checkers import InMemoryUsernamePasswordDatabaseDontUse 8 14 from twisted.internet import defer 15 from twisted.test import proto_helpers 9 16 from twisted.trial import unittest 10 from twisted.words.protocols.jabber import xmlstream 17 from twisted.words.protocols.jabber import error, xmlstream 18 from twisted.words.protocols.jabber.client import NS_XMPP_BIND 19 from twisted.words.protocols.jabber.client import NS_XMPP_SESSION 11 20 from twisted.words.protocols.jabber.client import XMPPAuthenticator 12 21 from twisted.words.protocols.jabber.jid import JID 22 from twisted.words.protocols.jabber.sasl import NS_XMPP_SASL 23 from twisted.words.protocols.jabber.xmlstream import INIT_FAILED_EVENT 24 from twisted.words.protocols.jabber.xmlstream import NS_STREAMS 13 25 from twisted.words.protocols.jabber.xmlstream import STREAM_AUTHD_EVENT 14 from twisted.words.protocols.jabber.xmlstream import INIT_FAILED_EVENT15 26 from twisted.words.protocols.jabber.xmlstream import XMPPHandler 27 from twisted.words.xish import xpath 16 28 17 from wokkel import client 29 from wokkel import client, iwokkel 30 from wokkel.generic import TestableXmlStream, FeatureListenAuthenticator 18 31 19 32 class XMPPClientTest(unittest.TestCase): 20 33 """ … … 155 168 self.assertEqual(factory.deferred, d2) 156 169 157 170 return d1 171 172 173 174 class TestSession(object): 175 implements(iwokkel.IUserSession) 176 177 def __init__(self, domain, user): 178 self.domain = domain 179 self.user = user 180 181 182 def bindResource(self, resource): 183 return defer.succeed(JID(tuple=(self.user, self.domain, resource))) 184 185 186 187 class TestRealm(object): 188 189 implements(IRealm) 190 191 logoutCalled = False 192 193 def __init__(self, domain): 194 self.domain = domain 195 196 197 def requestAvatar(self, avatarId, mind, *interfaces): 198 return (iwokkel.IUserSession, 199 TestSession(self.domain, avatarId.decode('utf-8')), 200 self.logout) 201 202 203 def logout(self): 204 self.logoutCalled = True 205 206 207 208 class TestableFeatureListenAuthenticator(FeatureListenAuthenticator): 209 namespace = 'jabber:client' 210 211 initialized = None 212 213 def __init__(self, getInitializers): 214 """ 215 Set up authenticator. 216 217 @param getInitializers: Function to override the getInitializers 218 method. It will receive C{self} as the only argument. 219 """ 220 FeatureListenAuthenticator.__init__(self) 221 222 import types 223 self.getInitializers = types.MethodType(getInitializers, self) 224 225 xs = TestableXmlStream(self) 226 xs.makeConnection(proto_helpers.StringTransport()) 227 228 229 def streamStarted(self, rootElement): 230 """ 231 Set up observers for authentication events. 232 """ 233 def authenticated(_): 234 self.initialized = True 235 236 self.xmlstream.addObserver(STREAM_AUTHD_EVENT, authenticated) 237 FeatureListenAuthenticator.streamStarted(self, rootElement) 238 239 240 241 class SASLReceivingInitializerTest(unittest.TestCase): 242 """ 243 Tests for L{client.SASLReceivingInitializer}. 244 """ 245 246 def setUp(self): 247 realm = TestRealm(u'example.org') 248 checker = InMemoryUsernamePasswordDatabaseDontUse(test='secret') 249 self.portal = portal = Portal(realm, (checker,)) 250 251 def getInitializers(self): 252 self.initializer = client.SASLReceivingInitializer('sasl', 253 self.xmlstream, 254 portal) 255 return [self.initializer] 256 257 self.authenticator = TestableFeatureListenAuthenticator(getInitializers) 258 self.xmlstream = self.authenticator.xmlstream 259 260 261 def test_getFeatures(self): 262 """ 263 The stream features list SASL with the PLAIN mechanism. 264 """ 265 xs = self.xmlstream 266 xs.dataReceived("<stream:stream xmlns='jabber:client' " 267 "xmlns:stream='http://etherx.jabber.org/streams' " 268 "to='example.org' " 269 "version='1.0'>") 270 271 self.assertTrue(xs.headerSent) 272 273 # Check SASL mechanisms 274 features = xs.output[-1] 275 self.assertTrue(xpath.matches("/features[@xmlns='%s']" 276 "/mechanisms[@xmlns='%s']" 277 "/mechanism[@xmlns='%s' and " 278 "text()='PLAIN']" % 279 (NS_STREAMS, NS_XMPP_SASL, NS_XMPP_SASL), 280 features)) 281 282 283 def test_auth(self): 284 """ 285 Authenticating causes an avatar to be set on the authenticator. 286 """ 287 xs = self.xmlstream 288 xs.dataReceived("<stream:stream xmlns='jabber:client' " 289 "xmlns:stream='http://etherx.jabber.org/streams' " 290 "to='example.org' " 291 "version='1.0'>") 292 xs.output = [] 293 response = b64encode('\x00'.join(['', 'test', 'secret'])) 294 xs.dataReceived("<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' " 295 "mechanism='PLAIN'>%s</auth>" % response) 296 self.assertTrue(iwokkel.IUserSession.providedBy(self.xmlstream.avatar)) 297 self.assertFalse(xs.headerSent) 298 self.assertEqual(1, len(xs.output)) 299 self.assertFalse(self.authenticator.initialized) 300 301 302 def test_authInvalidMechanism(self): 303 """ 304 Authenticating with an invalid SASL mechanism causes a streamError. 305 """ 306 xs = self.xmlstream 307 xs.dataReceived("<stream:stream xmlns='jabber:client' " 308 "xmlns:stream='http://etherx.jabber.org/streams' " 309 "to='example.org' " 310 "version='1.0'>") 311 xs.output = [] 312 xs.dataReceived("<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' " 313 "mechanism='unknown'/>") 314 self.assertTrue(xpath.matches(("/failure[@xmlns='%s']" 315 "/invalid-mechanism[@xmlns='%s']" % 316 (NS_XMPP_SASL, NS_XMPP_SASL)), 317 xs.output[-1])) 318 319 320 def test_authFail(self): 321 """ 322 Authenticating causes an avatar to be set on the authenticator. 323 """ 324 xs = self.xmlstream 325 xs.dataReceived("<stream:stream xmlns='jabber:client' " 326 "xmlns:stream='http://etherx.jabber.org/streams' " 327 "to='example.org' " 328 "version='1.0'>") 329 xs.output = [] 330 response = b64encode('\x00'.join(['', 'test', 'bad'])) 331 xs.dataReceived("<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' " 332 "mechanism='PLAIN'>%s</auth>" % response) 333 self.assertIdentical(None, self.xmlstream.avatar) 334 self.assertTrue(xs.headerSent) 335 336 self.assertTrue(xpath.matches(("/failure[@xmlns='%s']" 337 "/not-authorized[@xmlns='%s']" % 338 (NS_XMPP_SASL, NS_XMPP_SASL)), 339 xs.output[-1])) 340 341 self.assertFalse(self.authenticator.initialized) 342 343 344 def test_authFailMultiple(self): 345 """ 346 Authenticating causes an avatar to be set on the authenticator. 347 """ 348 xs = self.xmlstream 349 xs.dataReceived("<stream:stream xmlns='jabber:client' " 350 "xmlns:stream='http://etherx.jabber.org/streams' " 351 "to='example.org' " 352 "version='1.0'>") 353 354 xs.output = [] 355 response = b64encode('\x00'.join(['', 'test', 'bad'])) 356 357 attempts = self.authenticator.initializer.failureGrace 358 for attempt in xrange(attempts): 359 xs.dataReceived("<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' " 360 "mechanism='PLAIN'>%s</auth>" % response) 361 self.assertTrue(xpath.matches(("/failure[@xmlns='%s']" 362 "/not-authorized[@xmlns='%s']" % 363 (NS_XMPP_SASL, NS_XMPP_SASL)), 364 xs.output[-1])) 365 self.xmlstream.assertStreamError(self, condition='policy-violation') 366 self.assertFalse(self.authenticator.initialized) 367 368 369 def test_authException(self): 370 """ 371 Other authentication exceptions yield temporary-auth-failure. 372 """ 373 class Error(Exception): 374 pass 375 376 def login(credentials, mind, *interfaces): 377 raise Error() 378 379 self.portal.login = login 380 381 xs = self.xmlstream 382 xs.dataReceived("<stream:stream xmlns='jabber:client' " 383 "xmlns:stream='http://etherx.jabber.org/streams' " 384 "to='example.org' " 385 "version='1.0'>") 386 xs.output = [] 387 response = b64encode('\x00'.join(['', 'test', 'bad'])) 388 xs.dataReceived("<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' " 389 "mechanism='PLAIN'>%s</auth>" % response) 390 self.assertIdentical(None, self.xmlstream.avatar) 391 self.assertTrue(xs.headerSent) 392 393 self.assertTrue(xpath.matches(("/failure[@xmlns='%s']" 394 "/temporary-auth-failure[@xmlns='%s']" % 395 (NS_XMPP_SASL, NS_XMPP_SASL)), 396 xs.output[-1])) 397 self.assertFalse(self.authenticator.initialized) 398 self.assertEqual(1, len(self.flushLoggedErrors(Error))) 399 400 401 def test_authNonAsciiUsername(self): 402 """ 403 Authenticating causes an avatar to be set on the authenticator. 404 """ 405 xs = self.xmlstream 406 xs.dataReceived("<stream:stream xmlns='jabber:client' " 407 "xmlns:stream='http://etherx.jabber.org/streams' " 408 "to='example.org' " 409 "version='1.0'>") 410 xs.output = [] 411 response = b64encode('\x00'.join(['', 'test\xa1', 'secret'])) 412 xs.dataReceived("<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' " 413 "mechanism='PLAIN'>%s</auth>" % response) 414 self.assertIdentical(None, self.xmlstream.avatar) 415 self.assertTrue(xs.headerSent) 416 417 self.assertEqual(1, len(xs.output)) 418 failure = xs.output[-1] 419 condition = failure.elements().next() 420 self.assertEqual('not-authorized', condition.name) 421 422 423 def test_authAuthorizationIdentifier(self): 424 """ 425 Authorization Identifiers are not supported. 426 """ 427 xs = self.xmlstream 428 xs.dataReceived("<stream:stream xmlns='jabber:client' " 429 "xmlns:stream='http://etherx.jabber.org/streams' " 430 "to='example.org' " 431 "version='1.0'>") 432 xs.output = [] 433 response = b64encode('\x00'.join(['other', 'test', 'secret'])) 434 xs.dataReceived("<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' " 435 "mechanism='PLAIN'>%s</auth>" % response) 436 self.assertIdentical(None, self.xmlstream.avatar) 437 self.assertTrue(xs.headerSent) 438 439 self.assertEqual(1, len(xs.output)) 440 failure = xs.output[-1] 441 condition = failure.elements().next() 442 self.assertEqual('invalid-authz', condition.name) 443 444 445 446 class BindReceivingInitializerTest(unittest.TestCase): 447 """ 448 Tests for L{client.BindReceivingInitializer}. 449 """ 450 451 def setUp(self): 452 def getInitializers(self): 453 self.initializer = client.BindReceivingInitializer('bind', 454 self.xmlstream) 455 return [self.initializer] 456 457 self.authenticator = TestableFeatureListenAuthenticator(getInitializers) 458 self.xmlstream = self.authenticator.xmlstream 459 self.xmlstream.avatar = TestSession('example.org', 'test') 460 461 462 def test_getFeatures(self): 463 """ 464 The stream features include resource binding. 465 """ 466 xs = self.xmlstream 467 xs.dataReceived("<stream:stream xmlns='jabber:client' " 468 "xmlns:stream='http://etherx.jabber.org/streams' " 469 "to='example.org' " 470 "version='1.0'>") 471 472 features = xs.output[-1] 473 self.assertTrue(xpath.matches("/features[@xmlns='%s']" 474 "/bind[@xmlns='%s']" % 475 (NS_STREAMS, NS_XMPP_BIND), 476 features)) 477 478 479 def test_bind(self): 480 """ 481 To bind a resource, the avatar is requested one and a JID is returned. 482 """ 483 xs = self.xmlstream 484 xs.dataReceived("<stream:stream xmlns='jabber:client' " 485 "xmlns:stream='http://etherx.jabber.org/streams' " 486 "to='example.org' " 487 "version='1.0'>") 488 489 # This initializer is required. 490 self.assertFalse(self.authenticator.initialized) 491 492 xs.output = [] 493 xs.dataReceived("""<iq type='set'> 494 <bind xmlns='%s'>Home</bind> 495 </iq>""" % NS_XMPP_BIND) 496 497 self.assertTrue(xs.headerSent, "Unexpected stream restart") 498 499 # In response to the bind request, a result iq and the new stream 500 # features are sent 501 response = xs.output[-2] 502 self.assertTrue(xpath.matches("/iq[@type='result']" 503 "/bind[@xmlns='%s']" 504 "/jid[@xmlns='%s' and " 505 "text()='%s']" % 506 (NS_XMPP_BIND, 507 NS_XMPP_BIND, 508 'test@example.org/Home'), 509 response)) 510 511 self.assertTrue(self.authenticator.initialized) 512 513 514 515 class SessionReceivingInitializerTest(unittest.TestCase): 516 """ 517 Tests for L{client.SessionReceivingInitializer}. 518 """ 519 520 def setUp(self): 521 def getInitializers(self): 522 self.initializer = client.SessionReceivingInitializer('session', 523 self.xmlstream) 524 return [self.initializer] 525 526 self.authenticator = TestableFeatureListenAuthenticator(getInitializers) 527 self.xmlstream = self.authenticator.xmlstream 528 529 530 def test_getFeatures(self): 531 """ 532 The stream features include session establishment. 533 """ 534 xs = self.xmlstream 535 xs.dataReceived("<stream:stream xmlns='jabber:client' " 536 "xmlns:stream='http://etherx.jabber.org/streams' " 537 "to='example.org' " 538 "version='1.0'>") 539 540 features = xs.output[-1] 541 self.assertTrue(xpath.matches("/features[@xmlns='%s']" 542 "/session[@xmlns='%s']" % 543 (NS_STREAMS, NS_XMPP_SESSION), 544 features)) 545 546 def test_session(self): 547 """ 548 Session establishment is a no-op iq exchange. 549 """ 550 xs = self.xmlstream 551 xs.dataReceived("<stream:stream xmlns='jabber:client' " 552 "xmlns:stream='http://etherx.jabber.org/streams' " 553 "to='example.org' " 554 "version='1.0'>") 555 556 # This initializer is not required. 557 self.assertTrue(self.authenticator.initialized) 558 559 # If resource binding has completed, xs.otherEntity has been set. 560 xs.otherEntity = JID('test@example.org/Home') 561 562 xs.output = [] 563 xs.dataReceived("""<iq type='set'> 564 <session xmlns='%s'/> 565 </iq>""" % NS_XMPP_SESSION) 566 567 self.assertTrue(xs.headerSent, "Unexpected stream restart") 568 569 # In response to the session request, a result iq and the new stream 570 # features are sent 571 response = xs.output[-2] 572 self.assertTrue(xpath.matches("/iq[@type='result']", response)) 573 574 575 576 def test_sessionNoBind(self): 577 """ 578 Session establishment requires resource binding being completed. 579 """ 580 xs = self.xmlstream 581 xs.dataReceived("<stream:stream xmlns='jabber:client' " 582 "xmlns:stream='http://etherx.jabber.org/streams' " 583 "to='example.org' " 584 "version='1.0'>") 585 586 # This initializer is not required. 587 self.assertTrue(self.authenticator.initialized) 588 589 xs.output = [] 590 xs.dataReceived("""<iq type='set'> 591 <session xmlns='%s'/> 592 </iq>""" % NS_XMPP_SESSION) 593 594 self.assertTrue(xs.headerSent, "Unexpected stream restart") 595 596 # In response to the session request, a result iq and the new stream 597 # features are sent 598 response = xs.output[-2] 599 stanzaError = error.exceptionFromStanza(response) 600 self.assertEqual('forbidden', stanzaError.condition) 601 602 603 604 class XMPPClientListenAuthenticatorTest(unittest.TestCase): 605 """ 606 Tests for L{client.XMPPClientListenAuthenticator}. 607 """ 608 609 def setUp(self): 610 portals = {JID('example.org'): None} 611 self.authenticator = client.XMPPClientListenAuthenticator(portals) 612 self.xmlstream = TestableXmlStream(self.authenticator) 613 self.xmlstream.makeConnection(self) 614 615 616 def test_getInitializersStart(self): 617 """ 618 Upon the start of negotation, only the SASL initializer is available. 619 """ 620 inits = self.authenticator.getInitializers() 621 (init,) = inits 622 self.assertEqual('sasl', init.name) 623 self.assertIsInstance(init, client.SASLReceivingInitializer) 624 625 626 def test_getInitializersPostSASL(self): 627 """ 628 After SASL, the resource binding and session establishment initializers 629 are available. 630 """ 631 self.authenticator.completedInitializers = ['sasl'] 632 inits = self.authenticator.getInitializers() 633 (bind, session) = inits 634 self.assertEqual('bind', bind.name) 635 self.assertIsInstance(bind, client.BindReceivingInitializer) 636 self.assertEqual('session', session.name) 637 self.assertIsInstance(session, client.SessionReceivingInitializer) 638 639 640 def test_streamStartedWrongNamespace(self): 641 """ 642 An incorrect stream namespace causes a stream error. 643 """ 644 xs = self.xmlstream 645 xs.dataReceived("<stream:stream xmlns='jabber:server' " 646 "xmlns:stream='http://etherx.jabber.org/streams' " 647 "to='example.org' " 648 "version='1.0'>") 649 self.xmlstream.assertStreamError(self, condition='invalid-namespace') 650 651 652 def test_streamStartedNoTo(self): 653 """ 654 A missing 'to' attribute on the stream header causes a stream error. 655 """ 656 xs = self.xmlstream 657 xs.dataReceived("<stream:stream xmlns='jabber:client' " 658 "xmlns:stream='http://etherx.jabber.org/streams' " 659 "version='1.0'>") 660 self.xmlstream.assertStreamError(self, condition='improper-addressing') 661 662 663 def test_streamStartedUnknownHost(self): 664 """ 665 An unknown 'to' on the stream header causes a stream error. 666 """ 667 xs = self.xmlstream 668 xs.dataReceived("<stream:stream xmlns='jabber:client' " 669 "xmlns:stream='http://etherx.jabber.org/streams' " 670 "to='example.com' " 671 "version='1.0'>") 672 self.xmlstream.assertStreamError(self, condition='host-unknown') -
wokkel/test/test_generic.py
diff --git a/wokkel/test/test_generic.py b/wokkel/test/test_generic.py
a b 331 331 """ 332 332 333 333 def setUp(self): 334 self.gotAuthenticated = False 335 self.initFailure = None 334 self.gotAuthenticated = 0 336 335 self.authenticator = generic.FeatureListenAuthenticator() 337 336 self.authenticator.namespace = 'jabber:server' 338 337 self.xmlstream = generic.TestableXmlStream(self.authenticator) 339 338 self.xmlstream.addObserver('//event/stream/authd', 340 339 self.onAuthenticated) 341 self.xmlstream.addObserver('//event/xmpp/initfailed',342 self.onInitFailed)343 340 344 341 self.init = TestableReceivingInitializer('init', self.xmlstream, 345 342 'testns', 'test') … … 351 348 352 349 353 350 def onAuthenticated(self, obj): 354 self.gotAuthenticated = True 355 356 357 def onInitFailed(self, failure): 358 self.initFailure = failure 351 self.gotAuthenticated += 1 359 352 360 353 361 354 def test_getInitializers(self): … … 537 530 " <query xmlns='jabber:iq:version'/>" 538 531 "</iq>") 539 532 533 def test_streamStartedInitializerNotRequired(self): 534 """ 535 If no initializers are required, initialization is done. 536 """ 537 self.init.required = False 538 xs = self.xmlstream 539 xs.makeConnection(proto_helpers.StringTransport()) 540 xs.dataReceived("<stream:stream xmlns='jabber:server' " 541 "xmlns:stream='http://etherx.jabber.org/streams' " 542 "from='example.com' to='example.org' id='12345' " 543 "version='1.0'>") 544 545 self.assertEqual(1, self.gotAuthenticated) 546 547 548 def test_streamStartedInitializerNotRequiredDoneOnce(self): 549 """ 550 If no initializers are required, the authd event is not sent again. 551 """ 552 self.init.required = False 553 xs = self.xmlstream 554 xs.makeConnection(proto_helpers.StringTransport()) 555 xs.dataReceived("<stream:stream xmlns='jabber:server' " 556 "xmlns:stream='http://etherx.jabber.org/streams' " 557 "from='example.com' to='example.org' id='12345' " 558 "version='1.0'>") 559 560 self.assertEqual(1, self.gotAuthenticated) 561 xs.output = [] 562 self.init.deferred.callback(None) 563 self.assertEqual(1, self.gotAuthenticated) 564 540 565 541 566 def test_streamStartedXmlStanzasHandledIgnored(self): 542 567 """
Note: See TracBrowser
for help on using the repository browser.