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