Changeset 65:736d81819863 in ralphm-patches
- Timestamp:
- Aug 19, 2012, 11:19:55 PM (10 years ago)
- Branch:
- default
- Files:
-
- 1 deleted
- 3 edited
Legend:
- Unmodified
- Added
- Removed
-
client_listen_authenticator.patch
r57 r65 1 1 # HG changeset patch 2 # Parent 75701188facc278f61a0dfb4bcfcd2232ee771ca2 # Parent ad0f4165244b1c661023f03d8f7557fe5352337f 3 3 Add authenticator for accepting XMPP client connections. 4 4 … … 7 7 SASL PLAIN mechanism only. 8 8 9 This authenticator needs a backend service to hold the domain served and 10 relay established connections. Upon binding a resource, its `bindResource` 11 method is called with the local-part, domain and resource bound to the new 12 stream. 9 This authenticator needs at least one Twisted Cred portal to hold the 10 domain served. After authenticating, an avatar and a logout callback are 11 returned. Upon binding a resource, the avatar's `bindResource` method is 12 called with the desired resource name. Upon stream disconnect, the 13 logout callback is called. 13 14 14 15 TODO: … … 16 17 * Add tests. 17 18 * Add docstrings. 18 * Readd stream namespace check?19 * Host checks.20 * Password checks.21 * Support for multiple domains?22 19 23 diff - r 75701188faccwokkel/client.py24 --- a/wokkel/client.py Wed Nov 23 09:52:41 2011 +010025 +++ b/wokkel/client.py Wed Nov 30 09:31:07 2011 +010026 @@ -10,14 +10,2 6@@20 diff --git a/wokkel/client.py b/wokkel/client.py 21 --- a/wokkel/client.py 22 +++ b/wokkel/client.py 23 @@ -10,14 +10,28 @@ 27 24 that should probably eventually move there. 28 25 """ … … 30 27 +import base64 31 28 + 29 +from zope.interface import Interface 30 + 32 31 from twisted.application import service 33 from twisted.internet import reactor 32 -from twisted.internet import reactor 33 +from twisted.cred import credentials, error as ecred 34 +from twisted.internet import defer, reactor 34 35 +from twisted.python import log 35 36 from twisted.names.srvconnect import SRVConnector … … 43 44 +NS_CLIENT = 'jabber:client' 44 45 + 45 +XPATH_ALL = "/*"46 46 +XPATH_AUTH = "/auth[@xmlns='%s']" % sasl.NS_XMPP_SASL 47 47 +XPATH_BIND = "/iq[@type='set']/bind[@xmlns='%s']" % client.NS_XMPP_BIND … … 52 52 """ 53 53 Check what authentication methods are available. 54 @@ -51,7 +6 3,7 @@54 @@ -51,7 +65,7 @@ 55 55 autentication. 56 56 """ … … 61 61 def __init__(self, jid, password): 62 62 xmlstream.ConnectAuthenticator.__init__(self, jid.host) 63 @@ -186,3 + 198,152@@63 @@ -186,3 +200,210 @@ 64 64 c = XMPPClientConnector(reactor, domain, factory) 65 65 c.connect() … … 68 68 + 69 69 + 70 +class XMPPClientListenAuthenticator(xmlstream.ListenAuthenticator): 71 + namespace = NS_CLIENT 72 + 73 + def __init__(self, service): 74 + self.service = service 70 +class InvalidMechanism(Exception): 71 + """ 72 + The requested SASL mechanism is invalid. 73 + """ 74 + 75 + 76 +class IAccount(Interface): 77 + pass 78 + 79 + 80 + 81 +class SASLReceivingInitializer(object): 82 + required = True 83 + 84 + def __init__(self, xs, portal): 85 + self.xmlstream = xs 86 + self.portal = portal 87 + self.deferred = defer.Deferred() 75 88 + self.failureGrace = 3 76 + self.state = 'auth' 77 + 78 + 79 + def associateWithStream(self, xs): 80 + xmlstream.ListenAuthenticator.associateWithStream(self, xs) 81 + self.xmlstream.addObserver(XPATH_ALL, self.onElementFallback, -1) 82 + 83 + 84 + def onElementFallback(self, element): 85 + if element.handled: 86 + return 87 + 88 + exc = error.StreamError('not-authorized') 89 + self.xmlstream.sendStreamError(exc) 90 + 91 + 92 + def streamStarted(self, rootElement): 93 + xmlstream.ListenAuthenticator.streamStarted(self, rootElement) 94 + 95 + # check namespace 96 + #if self.xmlstream.namespace != self.namespace: 97 + # self.xmlstream.namespace = self.namespace 98 + # exc = error.StreamError('invalid-namespace') 99 + # self.xmlstream.sendStreamError(exc) 100 + # return 101 + 102 + # TODO: check domain (self.service.domain) 103 + 104 + self.xmlstream.sendHeader() 105 + 106 + try: 107 + stateHandlerName = 'streamStarted_' + self.state 108 + stateHandler = getattr(self, stateHandlerName) 109 + except AttributeError: 110 + log.msg('streamStarted handler for', self.state, 'not found') 111 + else: 112 + stateHandler() 113 + 114 + 115 + def toState(self, state): 116 + self.state = state 117 + if state == 'initialized': 118 + self.xmlstream.removeObserver(XPATH_ALL, self.onElementFallback) 119 + self.xmlstream.addOnetimeObserver(XPATH_SESSION, self.onSession, 1) 120 + self.xmlstream.dispatch(self.xmlstream, 121 + xmlstream.STREAM_AUTHD_EVENT) 122 + 123 + 124 + def streamStarted_auth(self): 125 + features = domish.Element((xmlstream.NS_STREAMS, 'features')) 126 + features.addElement((sasl.NS_XMPP_SASL, 'mechanisms')) 127 + features.mechanisms.addElement('mechanism', content='PLAIN') 128 + self.xmlstream.send(features) 129 + self.xmlstream.addOnetimeObserver(XPATH_AUTH, self.onAuth) 89 + 90 + 91 + def getFeatures(self): 92 + feature = domish.Element((sasl.NS_XMPP_SASL, 'mechanisms')) 93 + feature.addElement('mechanism', content='PLAIN') 94 + return [feature] 95 + 96 + 97 + def initialize(self): 98 + self.xmlstream.addObserver(XPATH_AUTH, self.onAuth) 99 + return self.deferred 130 100 + 131 101 + … … 133 103 + auth.handled = True 134 104 + 135 + if auth.getAttribute('mechanism') != 'PLAIN': 136 + failure = domish.Element((sasl.NS_XMPP_SASL, 'failure')) 137 + failure.addElement('invalid-mechanism') 138 + self.xmlstream.send(failure) 105 + def cb(_): 106 + response = domish.Element((sasl.NS_XMPP_SASL, 'success')) 107 + self.xmlstream.send(response) 108 + self.xmlstream.reset() 109 + 110 + def eb(failure): 111 + if failure.check(ecred.UnauthorizedLogin): 112 + condition = 'not-authorized' 113 + elif failure.check(InvalidMechanism): 114 + condition = 'invalid-mechanism' 115 + else: 116 + log.err(failure) 117 + condition = 'temporary-auth-failure' 118 + 119 + response = domish.Element((sasl.NS_XMPP_SASL, 'failure')) 120 + response.addElement(condition) 121 + self.xmlstream.send(response) 139 122 + 140 123 + # Close stream on too many failing authentication attempts 141 124 + self.failureGrace -= 1 142 125 + if self.failureGrace == 0: 143 + self.xmlstream.sendFooter() 144 + else: 145 + self.xmlstream.addOnetimeObserver(XPATH_AUTH, self.onAuth) 146 + 147 + return 148 + 126 + raise error.StreamError('policy-violation') 127 + #self.xmlstream.sendStreamError(exc) 128 + 129 + d = defer.maybeDeferred(self.doAuth, auth) 130 + d.addCallbacks(cb, eb) 131 + d.chainDeferred(self.deferred) 132 + 133 + 134 + def credentialsFromPlain(self, auth): 149 135 + initialResponse = base64.b64decode(unicode(auth)) 150 136 + authzid, authcid, passwd = initialResponse.split('\x00') 151 137 + 152 + # TODO: check passwd 153 + 154 + # authenticated 155 + 156 + self.username = authcid 157 + 158 + success = domish.Element((sasl.NS_XMPP_SASL, 'success')) 159 + self.xmlstream.send(success) 160 + self.xmlstream.reset() 161 + 162 + self.toState('bind') 163 + 164 + 165 + def streamStarted_bind(self): 166 + features = domish.Element((xmlstream.NS_STREAMS, 'features')) 167 + features.addElement((client.NS_XMPP_BIND, 'bind')) 168 + features.addElement((client.NS_XMPP_SESSION, 'session')) 169 + self.xmlstream.send(features) 138 + # FIXME: bail if authzid is set 139 + 140 + creds = credentials.UsernamePassword(username=authcid.encode('utf-8'), 141 + password=passwd) 142 + return creds 143 + 144 + 145 + def doAuth(self, auth): 146 + 147 + if auth.getAttribute('mechanism') != 'PLAIN': 148 + raise InvalidMechanism() 149 + 150 + creds = self.credentialsFromPlain(auth) 151 + 152 + def cb((iface, avatar, logout)): 153 + self.xmlstream.avatar = avatar 154 + self.xmlstream.addObserver(xmlstream.STREAM_END_EVENT, 155 + lambda _: logout()) 156 + 157 + d = self.portal.login(creds, None, IAccount) 158 + d.addCallback(cb) 159 + return d 160 + 161 + 162 + 163 +class BindReceivingInitializer(object): 164 + required = True 165 + 166 + def __init__(self, xs): 167 + self.xmlstream = xs 168 + self.deferred = defer.Deferred() 169 + 170 + 171 + def getFeatures(self): 172 + feature = domish.Element((client.NS_XMPP_BIND, 'bind')) 173 + return [feature] 174 + 175 + 176 + def initialize(self): 170 177 + self.xmlstream.addOnetimeObserver(XPATH_BIND, self.onBind) 178 + return self.deferred 171 179 + 172 180 + … … 174 182 + def cb(boundJID): 175 183 + self.xmlstream.otherEntity = boundJID 176 + self.toState('initialized')177 184 + 178 185 + response = xmlstream.toResponse(iq, 'result') … … 194 201 + iq.handled = True 195 202 + resource = unicode(iq.bind) or None 196 + d = self.service.bindResource(self.username, 197 + self.service.domain, 198 + resource) 203 + d = self.xmlstream.avatar.bindResource(resource) 199 204 + d.addCallback(cb) 200 205 + d.addErrback(eb) 201 206 + d.addCallback(self.xmlstream.send) 207 + d.chainDeferred(self.deferred) 208 + 209 + 210 + 211 +class SessionReceivingInitializer(object): 212 + required = False 213 + 214 + def __init__(self, xs): 215 + self.xmlstream = xs 216 + self.deferred = defer.Deferred() 217 + 218 + 219 + def getFeatures(self): 220 + feature = domish.Element((client.NS_XMPP_SESSION, 'session')) 221 + return [feature] 222 + 223 + 224 + def initialize(self): 225 + self.xmlstream.addOnetimeObserver(XPATH_SESSION, self.onSession, 1) 226 + return self.deferred 202 227 + 203 228 + … … 206 231 + 207 232 + reply = domish.Element((None, 'iq')) 208 + reply['type'] = 'result' 209 + if iq.getAttribute('id'): 210 + reply['id'] = iq['id'] 211 + reply.addElement((client.NS_XMPP_SESSION, 'session')) 233 + 234 + if self.xmlstream.otherEntity: 235 + reply = xmlstream.toResponse(iq) 236 + else: 237 + reply = error.StanzaError('forbidden').toResponse(iq) 212 238 + self.xmlstream.send(reply) 213 + 214 + 215 + 239 + self.deferred.callback(None) 240 + 241 + 242 + 243 +class XMPPClientListenAuthenticator(generic.FeatureListenAuthenticator): 244 + namespace = NS_CLIENT 245 + 246 + def __init__(self, portals): 247 + generic.FeatureListenAuthenticator.__init__(self) 248 + self.portals = portals 249 + self.portal = None 250 + 251 + 252 + def getInitializers(self): 253 + 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)] 259 + else: 260 + return [] 261 + 262 + 263 + def checkStream(self): 264 + generic.FeatureListenAuthenticator.checkStream(self) 265 + 266 + if not self.xmlstream.thisEntity: 267 + raise error.StreamError('improper-addressing') 268 + 269 + # Check if we serve the domain and use the associated portal. 270 + try: 271 + self.portal = self.portals[self.xmlstream.thisEntity] 272 + except KeyError: 273 + raise error.StreamError('host-unknown') 274 diff --git a/wokkel/test/test_client.py b/wokkel/test/test_client.py 275 --- a/wokkel/test/test_client.py 276 +++ b/wokkel/test/test_client.py 277 @@ -5,14 +5,21 @@ 278 Tests for L{wokkel.client}. 279 """ 280 281 +from zope.interface import implements 282 + 283 +from twisted.cred.portal import IRealm, Portal 284 +from twisted.cred.checkers import InMemoryUsernamePasswordDatabaseDontUse 285 from twisted.internet import defer 286 from twisted.trial import unittest 287 from twisted.words.protocols.jabber import xmlstream 288 from twisted.words.protocols.jabber.client import XMPPAuthenticator 289 from twisted.words.protocols.jabber.jid import JID 290 +from twisted.words.protocols.jabber.sasl import NS_XMPP_SASL 291 +from twisted.words.protocols.jabber.xmlstream import INIT_FAILED_EVENT 292 +from twisted.words.protocols.jabber.xmlstream import NS_STREAMS 293 from twisted.words.protocols.jabber.xmlstream import STREAM_AUTHD_EVENT 294 -from twisted.words.protocols.jabber.xmlstream import INIT_FAILED_EVENT 295 from twisted.words.protocols.jabber.xmlstream import XMPPHandler 296 +from twisted.words.xish import xpath 297 298 from wokkel import client 299 300 @@ -155,3 +162,167 @@ 301 self.assertEqual(factory.deferred, d2) 302 303 return d1 304 + 305 + 306 +class TestAccount(object): 307 + implements(client.IAccount) 308 + 309 + 310 +class TestRealm(object): 311 + 312 + implements(IRealm) 313 + 314 + logoutCalled = False 315 + 316 + def requestAvatar(self, avatarId, mind, *interfaces): 317 + return (client.IAccount, TestAccount(), self.logout) 318 + 319 + 320 + def logout(self): 321 + self.logoutCalled = True 322 + 323 + 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) 353 + 354 + 355 +class XMPPClientListenAuthenticatorTest(unittest.TestCase): 356 + """ 357 + Tests for L{client.XMPPClientListenAuthenticator}. 358 + """ 359 + 360 + 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} 366 + self.authenticator = client.XMPPClientListenAuthenticator(portals) 367 + self.xmlstream = TestableXmlStream(self.authenticator) 368 + self.xmlstream.makeConnection(self) 369 + 370 + 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) 397 + 398 + 399 + def test_streamStartedWrongNamespace(self): 400 + """ 401 + An incorrect stream namespace causes a stream error. 402 + """ 403 + xs = self.xmlstream 404 + xs.dataReceived("<stream:stream xmlns='jabber:server' " 405 + "xmlns:stream='http://etherx.jabber.org/streams' " 406 + "to='example.org' " 407 + "version='1.0'>") 408 + streamError = xs.streamErrors[-1] 409 + self.assertEquals('invalid-namespace', streamError.condition) 410 + 411 + 412 + def test_streamStartedNoTo(self): 413 + """ 414 + A missing 'to' attribute on the stream header causes a stream error. 415 + """ 416 + xs = self.xmlstream 417 + xs.dataReceived("<stream:stream xmlns='jabber:client' " 418 + "xmlns:stream='http://etherx.jabber.org/streams' " 419 + "version='1.0'>") 420 + streamError = xs.streamErrors[-1] 421 + self.assertEquals('improper-addressing', streamError.condition) 422 + 423 + 424 + def test_streamStartedUnknownHost(self): 425 + """ 426 + An unknown 'to' on the stream header causes a stream error. 427 + """ 428 + xs = self.xmlstream 429 + xs.dataReceived("<stream:stream xmlns='jabber:client' " 430 + "xmlns:stream='http://etherx.jabber.org/streams' " 431 + "to='example.com' " 432 + "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]) -
listening-authenticator-stream-features.patch
r64 r65 1 1 # HG changeset patch 2 # Parent 4f4ccf34fee8357fdf4fbf1a741e4c5113553d5d 2 # Parent 8b17590769db3088336f1fa65710c48e5ad5dcc1 3 Add FeatureListeningAuthenticator. 4 5 This new authenticator is for incoming streams and uses initializers 6 for stream negotiation, similar to inializers for clients. 7 8 TODO: 9 10 * Add docstrings. 11 * Add support for stream restarts. 3 12 4 13 diff --git a/wokkel/generic.py b/wokkel/generic.py 5 14 --- a/wokkel/generic.py 6 15 +++ b/wokkel/generic.py 7 @@ -327,3 +327,74 @@ 16 @@ -25,6 +25,8 @@ 17 NS_VERSION = 'jabber:iq:version' 18 VERSION = IQ_GET + '/query[@xmlns="' + NS_VERSION + '"]' 19 20 +XPATH_ALL = "/*" 21 + 22 def parseXml(string): 23 """ 24 Parse serialized XML into a DOM structure. 25 @@ -327,3 +329,116 @@ 8 26 9 27 def clientConnectionFailed(self, connector, reason): … … 18 36 + self.headerSent = False 19 37 + self.footerSent = False 20 + self.streamError s = []38 + self.streamError = None 21 39 + self.output = [] 22 40 + … … 36 54 + 37 55 + def sendStreamError(self, streamError): 38 + self.streamError s.append(streamError)56 + self.streamError = streamError 39 57 + 40 58 + … … 48 66 + Authenticator for receiving entities with support for initializers. 49 67 + """ 68 + 69 + def __init__(self): 70 + self.completedInitializers = [] 71 + 72 + 73 + def _onElementFallback(self, element): 74 + """ 75 + Fallback observer that rejects XML Stanzas. 76 + 77 + This observer is active while stream feature negotiation has not yet 78 + completed. 79 + """ 80 + # ignore elements that are not XML Stanzas 81 + if (element.uri not in (self.namespace) or 82 + element.name not in ('iq', 'message', 'presence')): 83 + return 84 + 85 + # ignore elements that have already been handled 86 + if element.handled: 87 + return 88 + 89 + exc = error.StreamError('not-authorized') 90 + self.xmlstream.sendStreamError(exc) 91 + 50 92 + 51 93 + def _initializeStream(self): 52 94 + def cb(result): 53 95 + result, index = result 54 + del self.initializers[index] 96 + self.completedInitializers.append(self._initializers[index]) 97 + del self._initializers[index] 55 98 + self._initializeStream() 56 99 + … … 58 101 + ds = [] 59 102 + required = False 60 + for initializer in self. initializers:103 + for initializer in self._initializers: 61 104 + required = required or initializer.required 62 105 + for feature in initializer.getFeatures(): … … 66 109 + self.xmlstream.send(features) 67 110 + if not required: 111 + self.xmlstream.removeObserver(XPATH_ALL, self._onElementFallback) 68 112 + self.xmlstream.dispatch(self.xmlstream, xmlstream.STREAM_AUTHD_EVENT) 69 113 + if ds: … … 71 115 + d.addCallback(cb) 72 116 + 117 + def getInitializers(self): 118 + return [] 119 + 120 + 121 + def checkStream(self): 122 + # check namespace 123 + if self.xmlstream.namespace != self.namespace: 124 + self.xmlstream.namespace = self.namespace 125 + raise error.StreamError('invalid-namespace') 73 126 + 74 127 + 75 128 + def streamStarted(self, rootElement): 76 129 + xmlstream.ListenAuthenticator.streamStarted(self, rootElement) 77 + # check headers? 130 + 131 + try: 132 + self.checkStream() 133 + except error.StreamError, exc: 134 + self.xmlstream.sendStreamError(exc) 135 + return 136 + 137 + self.xmlstream.addObserver(XPATH_ALL, self._onElementFallback, -1) 78 138 + self.xmlstream.sendHeader() 79 + self.initializers = self.getInitializers() 139 + 140 + self._initializers = self.getInitializers() 80 141 + self._initializeStream() 81 +82 142 diff --git a/wokkel/test/test_generic.py b/wokkel/test/test_generic.py 83 143 --- a/wokkel/test/test_generic.py … … 92 152 from twisted.words.xish import domish 93 153 from twisted.words.protocols.jabber.jid import JID 94 +from twisted.words.protocols.jabber import xmlstream154 +from twisted.words.protocols.jabber import error, xmlstream 95 155 96 156 from wokkel import generic 97 157 from wokkel.test.helpers import XmlStreamStub 98 @@ -268,3 +271, 79@@158 @@ -268,3 +271,218 @@ 99 159 The default is no timeout. 100 160 """ … … 102 162 + 103 163 + 164 + 165 +class TestableReceivingInitializer(object): 166 + """ 167 + Testable initializer for receiving entities. 168 + 169 + This initializer advertises support for a stream feature denoted by 170 + C{uri} and C{name}. Its C{deferred} should be fired to complete 171 + initialization for this initializer. 172 + 173 + @ivar uri: Namespace of the stream feature. 174 + @ivar name: Element localname for the stream feature. 175 + """ 176 + required = True 177 + 178 + def __init__(self, xs, uri, name): 179 + self.xmlstream = xs 180 + self.uri = uri 181 + self.name = name 182 + self.deferred = defer.Deferred() 183 + 184 + 185 + def getFeatures(self): 186 + return [domish.Element((self.uri, self.name))] 187 + 188 + 189 + def initialize(self): 190 + return self.deferred 104 191 + 105 192 +class FeatureListenAuthenticatorTest(unittest.TestCase): … … 112 199 + self.initFailure = None 113 200 + self.authenticator = generic.FeatureListenAuthenticator() 201 + self.authenticator.namespace = 'jabber:server' 114 202 + self.xmlstream = generic.TestableXmlStream(self.authenticator) 115 203 + self.xmlstream.addObserver('//event/stream/authd', … … 118 206 + self.onInitFailed) 119 207 + 208 + self.init = TestableReceivingInitializer(self.xmlstream, 'testns', 'test') 209 + 210 + def getInitializers(): 211 + return [self.init] 212 + 213 + self.authenticator.getInitializers = getInitializers 214 + 120 215 + 121 216 + def onAuthenticated(self, obj): … … 133 228 + The method getInitializers will determine the available stream 134 229 + initializers given the current state of stream initialization. 135 + Then, each of the returned initializers will be called to set230 + hen, each of the returned initializers will be called to set 136 231 + themselves up. 137 232 + """ 138 + class Initializer(object):139 + required = True140 + def __init__(self, xs):141 + self.xmlstream = xs142 + self.deferred = defer.Deferred()143 + def getFeatures(self):144 + return [domish.Element(('testns', 'test'))]145 + def initialize(self):146 + return self.deferred147 +148 + init = Initializer(self.xmlstream)149 +150 + def getInitializers():151 + return [init]152 +153 + self.authenticator.getInitializers = getInitializers154 233 + 155 234 + xs = self.xmlstream … … 164 243 + # Check if features were sent 165 244 + features = xs.output[-1] 166 + print features.toXml()167 245 + self.assertEquals(xmlstream.NS_STREAMS, features.uri) 168 246 + self.assertEquals('features', features.name) 169 247 + feature = features.elements().next() 170 + print feature.toXml()171 248 + self.assertEqual('testns', feature.uri) 172 249 + self.assertEqual('test', feature.name) … … 174 251 + self.assertFalse(self.gotAuthenticated) 175 252 + 176 + init.deferred.callback(None)253 + self.init.deferred.callback(None) 177 254 + self.assertTrue(self.gotAuthenticated) 255 + 256 + 257 + def test_streamStartedInitializerCompleted(self): 258 + """ 259 + Succesfully finished initializers are recorded. 260 + """ 261 + xs = self.xmlstream 262 + xs.makeConnection(proto_helpers.StringTransport()) 263 + xs.dataReceived("<stream:stream xmlns='jabber:server' " 264 + "xmlns:stream='http://etherx.jabber.org/streams' " 265 + "from='example.com' to='example.org' id='12345' " 266 + "version='1.0'>") 267 + 268 + self.init.deferred.callback(None) 269 + self.assertEqual([self.init], self.authenticator.completedInitializers) 270 + 271 + 272 + def test_streamStartedXmlStanzasRejected(self): 273 + """ 274 + XML Stanzas may not be sent before feature negotiation has completed. 275 + """ 276 + xs = self.xmlstream 277 + xs.makeConnection(proto_helpers.StringTransport()) 278 + xs.dataReceived("<stream:stream xmlns='jabber:server' " 279 + "xmlns:stream='http://etherx.jabber.org/streams' " 280 + "from='example.com' to='example.org' id='12345' " 281 + "version='1.0'>") 282 + 283 + xs.dataReceived("<iq to='example.org' from='example.com' type='set'>" 284 + " <query xmlns='jabber:iq:version'/>" 285 + "</iq>") 286 + 287 + self.assertEqual('not-authorized', xs.streamError.condition) 288 + 289 + 290 + def test_streamStartedCompleteXmlStanzasAllowed(self): 291 + """ 292 + XML Stanzas may sent after feature negotiation has completed. 293 + """ 294 + xs = self.xmlstream 295 + xs.makeConnection(proto_helpers.StringTransport()) 296 + xs.dataReceived("<stream:stream xmlns='jabber:server' " 297 + "xmlns:stream='http://etherx.jabber.org/streams' " 298 + "from='example.com' to='example.org' id='12345' " 299 + "version='1.0'>") 300 + 301 + self.init.deferred.callback(None) 302 + 303 + xs.dataReceived("<iq to='example.org' from='example.com' type='set'>" 304 + " <query xmlns='jabber:iq:version'/>" 305 + "</iq>") 306 + 307 + self.assertIdentical(None, xs.streamError) 308 + 309 + 310 + def test_streamStartedXmlStanzasHandledIgnored(self): 311 + """ 312 + XML Stanzas that have already been handled are ignored. 313 + """ 314 + xs = self.xmlstream 315 + xs.makeConnection(proto_helpers.StringTransport()) 316 + xs.dataReceived("<stream:stream xmlns='jabber:server' " 317 + "xmlns:stream='http://etherx.jabber.org/streams' " 318 + "from='example.com' to='example.org' id='12345' " 319 + "version='1.0'>") 320 + 321 + iq = generic.parseXml("<iq to='example.org' from='example.com' type='set'>" 322 + " <query xmlns='jabber:iq:version'/>" 323 + "</iq>") 324 + iq.handled = True 325 + xs.dispatch(iq) 326 + 327 + self.assertIdentical(None, xs.streamError) 328 + 329 + 330 + def test_streamStartedNonXmlStanzasIgnored(self): 331 + """ 332 + Elements that are not XML Stranzas are not rejected. 333 + """ 334 + xs = self.xmlstream 335 + xs.makeConnection(proto_helpers.StringTransport()) 336 + xs.dataReceived("<stream:stream xmlns='jabber:server' " 337 + "xmlns:stream='http://etherx.jabber.org/streams' " 338 + "from='example.com' to='example.org' id='12345' " 339 + "version='1.0'>") 340 + 341 + xs.dataReceived("<test xmlns='myns'/>") 342 + 343 + self.assertIdentical(None, xs.streamError) 344 + 345 + 346 + def test_streamStartedCheckStream(self): 347 + """ 348 + Stream errors raised by checkStream are sent out. 349 + """ 350 + def checkStream(): 351 + raise error.StreamError('undefined-condition') 352 + 353 + self.authenticator.checkStream = checkStream 354 + xs = self.xmlstream 355 + xs.makeConnection(proto_helpers.StringTransport()) 356 + xs.dataReceived("<stream:stream xmlns='jabber:server' " 357 + "xmlns:stream='http://etherx.jabber.org/streams' " 358 + "from='example.com' to='example.org' id='12345' " 359 + "version='1.0'>") 360 + 361 + self.assertEqual('undefined-condition', xs.streamError.condition) 362 + self.assertFalse(xs.headerSent) 363 + 364 + 365 + def test_checkStreamNamespace(self): 366 + """ 367 + The stream namespace must match the pre-defined stream namespace. 368 + """ 369 + xs = self.xmlstream 370 + xs.makeConnection(proto_helpers.StringTransport()) 371 + xs.dataReceived("<stream:stream xmlns='jabber:client' " 372 + "xmlns:stream='http://etherx.jabber.org/streams' " 373 + "from='example.com' to='example.org' id='12345' " 374 + "version='1.0'>") 375 + 376 + self.assertEqual('invalid-namespace', xs.streamError.condition) -
series
r64 r65 1 1 roster_server.patch #+c2s 2 2 router_unknown.patch #+c2s 3 4 listening-authenticator-stream-features.patch #+c2s 3 5 client_listen_authenticator.patch #+c2s 4 c2s-cred.patch #+c2s5 listening-authenticator-stream-features.patch6 6 7 c2s_server_factory.patch #+c2s 8 session_manager.patch #+c2s 9 c2s_stanza_handlers.patch #+c2s 7 c2s_server_factory.patch #+c2s-broken 8 session_manager.patch #+c2s-broken 9 c2s_stanza_handlers.patch #+c2s-broken 10 10 11 11 version.patch
Note: See TracChangeset
for help on using the changeset viewer.