source:
ralphm-patches/listening-authenticator-stream-features.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: 24.7 KB |
-
wokkel/generic.py
# HG changeset patch # Parent 840b96390047670c5209195300f902689c18b12f Add FeatureListeningAuthenticator. This new authenticator is for incoming streams and uses initializers for stream negotiation, similar to inializers for clients. diff --git a/wokkel/generic.py b/wokkel/generic.py
a b 10 10 from zope.interface import implements 11 11 12 12 from twisted.internet import defer, protocol 13 from twisted.python import reflect13 from twisted.python import log, reflect 14 14 from twisted.python.deprecate import deprecated 15 15 from twisted.python.versions import Version 16 16 from twisted.words.protocols.jabber import error, jid, xmlstream … … 18 18 from twisted.words.xish import domish, utility 19 19 from twisted.words.xish.xmlstream import BootstrapMixin 20 20 21 from wokkel.iwokkel import IDisco 21 from wokkel.iwokkel import IDisco, IReceivingInitializer 22 22 from wokkel.subprotocols import XMPPHandler 23 23 24 24 IQ_GET = '/iq[@type="get"]' … … 27 27 NS_VERSION = 'jabber:iq:version' 28 28 VERSION = IQ_GET + '/query[@xmlns="' + NS_VERSION + '"]' 29 29 30 XPATH_ALL = "/*" 31 30 32 def parseXml(string): 31 33 """ 32 34 Parse serialized XML into a DOM structure. … … 332 334 333 335 334 336 337 class TestableXmlStream(xmlstream.XmlStream): 338 """ 339 XML Stream that buffers outgoing data and catches special events. 340 341 This implementation overrides relevant methods to prevent any data 342 to be sent out on a transport. Instead it buffers all outgoing stanzas, 343 sets flags instead of sending the stream header and footer and logs stream 344 errors so it can be caught by a logging observer. 345 346 @ivar output: Sequence of objects sent out using L{send}. Usually these are 347 L{domish.Element} instances. 348 @type output: C{list} 349 350 @ivar headerSent: Flag set when a stream header would have been sent. When 351 a stream restart occurs through L{reset}, this flag is reset as well. 352 @type headerSent: C{bool} 353 354 @ivar footerSent: Flag set when a stream footer would have been sent 355 explicitly. Note that it is not set when a stream error is sent 356 using L{sendStreamError}. 357 @type footerSent: C{bool} 358 """ 359 360 361 def __init__(self, authenticator): 362 xmlstream.XmlStream.__init__(self, authenticator) 363 self.headerSent = False 364 self.footerSent = False 365 self.output = [] 366 367 368 def reset(self): 369 xmlstream.XmlStream.reset(self) 370 self.headerSent = False 371 372 373 def sendHeader(self): 374 self.headerSent = True 375 376 377 def sendFooter(self): 378 self.footerSent = True 379 380 381 def sendStreamError(self, streamError): 382 """ 383 Log a stream error. 384 385 If this is called from a Twisted Trial test case, the stream error 386 will be observed by the Trial logging observer. If it is not explicitly 387 tested for (i.e. flushed), this will cause the test case to 388 automatically fail. See L{assertStreamError} for a convenience method 389 to test for stream errors. 390 391 @type streamError: L{error.StreamError} 392 """ 393 log.err(streamError) 394 395 396 @staticmethod 397 def assertStreamError(testcase, condition=None, exc=None): 398 """ 399 Check if a stream error was sent out. 400 401 To check for stream errors sent out by L{sendStreamError}, this method 402 will flush logged stream errors and inspect the last one. If 403 C{condition} was passed, the logged error is asserted to match that 404 condition. If C{exc} was passed, the logged error is asserted to be 405 identical to it. 406 407 Note that this is takes the calling test case as the first argument, to 408 be able to hook into its methods for flushing errors and making 409 assertions. 410 411 @param testcase: The test case instance that is calling this method. 412 @type testcase: {twisted.trial.unittest.TestCase} 413 414 @param condition: The optional stream error condition to match against. 415 @type condition: C{unicode}. 416 417 @param exc: The optional stream error to check identity against. 418 @type exc: L{error.StreamError} 419 """ 420 421 loggedErrors = testcase.flushLoggedErrors(error.StreamError) 422 testcase.assertTrue(loggedErrors, "No stream error was sent") 423 streamError = loggedErrors[-1].value 424 if condition: 425 testcase.assertEqual(condition, streamError.condition) 426 elif exc: 427 testcase.assertIdentical(exc, streamError) 428 429 430 def send(self, obj): 431 """ 432 Buffer all outgoing stanzas. 433 434 @type obj: L{domish.Element} 435 """ 436 self.output.append(obj) 437 438 439 440 class BaseReceivingInitializer(object): 441 """ 442 Base stream initializer for receiving entities. 443 """ 444 implements(IReceivingInitializer) 445 446 required = False 447 448 def __init__(self, name, xs): 449 self.name = name 450 self.xmlstream = xs 451 self.deferred = defer.Deferred() 452 453 454 def getFeatures(self): 455 raise NotImplementedError() 456 457 458 def initialize(self): 459 return self.deferred 460 461 462 463 class FeatureListenAuthenticator(xmlstream.ListenAuthenticator): 464 """ 465 Authenticator for receiving entities with support for initializers. 466 """ 467 468 def __init__(self): 469 self.completedInitializers = [] 470 471 472 def _onElementFallback(self, element): 473 """ 474 Fallback observer that rejects XML Stanzas. 475 476 This observer is active while stream feature negotiation has not yet 477 completed. 478 """ 479 # ignore elements that are not XML Stanzas 480 if (element.uri not in (self.namespace) or 481 element.name not in ('iq', 'message', 'presence')): 482 return 483 484 # ignore elements that have already been handled 485 if element.handled: 486 return 487 488 exc = error.StreamError('not-authorized') 489 self.xmlstream.sendStreamError(exc) 490 491 492 def connectionMade(self): 493 """ 494 Called when the connection has been made. 495 496 Adds an observer to reject XML Stanzas until stream feature negotiation 497 has completed. 498 """ 499 xmlstream.ListenAuthenticator.connectionMade(self) 500 self.xmlstream.addObserver(XPATH_ALL, self._onElementFallback, -1) 501 502 503 def _cbInit(self, result): 504 """ 505 Mark the initializer as completed and continue to the next. 506 """ 507 result, index = result 508 self.completedInitializers.append(self._initializers[index].name) 509 del self._initializers[index] 510 511 if result is xmlstream.Reset: 512 # The initializer initiated a stream restart, bail. 513 return 514 else: 515 self._initializeStream() 516 517 518 def _ebInit(self, failure): 519 """ 520 Called when an initializer raises an exception. 521 522 If the exception is a L{error.StreamError} it is sent out, otherwise 523 the error is logged and a stream error with condition 524 C{'internal-server-error'} is sent out instead. 525 """ 526 firstError = failure.value 527 subFailure = firstError.subFailure 528 if subFailure.check(error.StreamError): 529 exc = subFailure.value 530 else: 531 log.err(subFailure, index=firstError.index) 532 exc = error.StreamError('internal-server-error') 533 534 self.xmlstream.sendStreamError(exc) 535 536 537 def _initializeStream(self): 538 """ 539 Initialize the stream. 540 541 This walks all initializers to retrieve their features and determine 542 if there is at least one required initializer. If not, the stream is 543 ready for the exchange of stanzas. The features are sent out and each 544 initializer will have its C{initialize} method called. 545 546 If negation has completed, L{xmlstream.STREAM_AUTHD_EVENT} is 547 dispatched and the observer rejecting incoming stanzas is removed. 548 """ 549 features = domish.Element((xmlstream.NS_STREAMS, 'features')) 550 ds = [] 551 required = False 552 for initializer in self._initializers: 553 required = required or initializer.required 554 for feature in initializer.getFeatures(): 555 features.addChild(feature) 556 d = initializer.initialize() 557 ds.append(d) 558 559 self.xmlstream.send(features) 560 561 if not required: 562 # There are no required initializers anymore. This stream is 563 # now ready for the exchange of stanzas. 564 self.xmlstream.removeObserver(XPATH_ALL, self._onElementFallback) 565 self.xmlstream.dispatch(self.xmlstream, xmlstream.STREAM_AUTHD_EVENT) 566 567 if ds: 568 d = defer.DeferredList(ds, fireOnOneCallback=True, 569 fireOnOneErrback=True, 570 consumeErrors=True) 571 d.addCallbacks(self._cbInit, self._ebInit) 572 573 574 def getInitializers(self): 575 """ 576 Get the initializers for the current stage of stream negotiation. 577 578 This will be called at the start of each stream start to retrieve 579 the initializers for which the features are advertised and initiated. 580 581 @rtype: C{list} of C{IReceivingInitializer} instances. 582 """ 583 raise NotImplementedError() 584 585 586 def checkStream(self): 587 """ 588 Check the stream before sending out a stream header and initialization. 589 590 This is the place to inspect the stream properties and raise a relevant 591 L{error.StreamError} if needed. 592 """ 593 # Check stream namespace 594 if self.xmlstream.namespace != self.namespace: 595 self.xmlstream.namespace = self.namespace 596 raise error.StreamError('invalid-namespace') 597 598 599 def streamStarted(self, rootElement): 600 """ 601 Called when the stream header has been received. 602 603 Check the stream properties through L{checkStream}, send out 604 a stream header, and retrieve and initialize the stream initializers. 605 """ 606 xmlstream.ListenAuthenticator.streamStarted(self, rootElement) 607 608 try: 609 self.checkStream() 610 except error.StreamError, exc: 611 self.xmlstream.sendStreamError(exc) 612 return 613 614 self.xmlstream.sendHeader() 615 616 self._initializers = self.getInitializers() 617 self._initializeStream() 618 619 620 335 621 @deprecated(Version("Wokkel", 0, 8, 0), "unicode.encode('idna')") 336 622 def prepareIDNName(name): 337 623 """ -
wokkel/iwokkel.py
diff --git a/wokkel/iwokkel.py b/wokkel/iwokkel.py
a b 11 11 'IPubSubClient', 'IPubSubService', 'IPubSubResource', 12 12 'IMUCClient', 'IMUCStatuses'] 13 13 14 from zope.interface import Interface14 from zope.interface import Attribute, Interface 15 15 from twisted.python.deprecate import deprecatedModuleAttribute 16 16 from twisted.python.versions import Version 17 17 from twisted.words.protocols.jabber.ijabber import IXMPPHandler … … 982 982 """ 983 983 Return the number of status conditions. 984 984 """ 985 986 987 988 class IReceivingInitializer(Interface): 989 """ 990 Interface for XMPP stream initializers for receiving entities. 991 """ 992 993 required = Attribute( 994 """ 995 This initializer is required to complete feature negotiation. 996 """) 997 name = Attribute( 998 """ 999 Identifier for this initializer. 1000 1001 This identifier is included in 1002 L{wokkel.generic.FeatureListenAuthenticator} when an initializer has 1003 completed. 1004 """) 1005 xmlstream = Attribute( 1006 """ 1007 The XML Stream. 1008 """) 1009 deferred = Attribute( 1010 """ 1011 The deferred returned from initialize. 1012 """) 1013 1014 1015 def getFeatures(): 1016 """ 1017 Get stream features for this initializer. 1018 1019 @rtype: C{list} of L{twisted.words.xish.domish.Element} 1020 """ 1021 1022 1023 def initialize(): 1024 """ 1025 Initialize the initializer. 1026 1027 This is where observers for feature negotiation are set up. When 1028 the returned deferred fires, it is assumed to have completed. 1029 1030 @rtype: L{twisted.internet.defer.Deferred} 1031 """ -
wokkel/test/test_generic.py
diff --git a/wokkel/test/test_generic.py b/wokkel/test/test_generic.py
a b 7 7 8 8 import re 9 9 10 from zope.interface import verify 11 12 from twisted.internet import defer 10 13 from twisted.python import deprecate 11 14 from twisted.python.versions import Version 15 from twisted.test import proto_helpers 12 16 from twisted.trial import unittest 13 17 from twisted.trial.util import suppress as SUPPRESS 14 18 from twisted.words.xish import domish 15 19 from twisted.words.protocols.jabber.jid import JID 20 from twisted.words.protocols.jabber import error, xmlstream 16 21 17 from wokkel import generic 22 from wokkel import generic, iwokkel 18 23 from wokkel.test.helpers import XmlStreamStub 19 24 20 25 NS_VERSION = 'jabber:iq:version' … … 276 281 277 282 278 283 284 class BaseReceivingInitializerTest(unittest.TestCase): 285 """ 286 Tests for L{generic.BaseReceivingInitializer}. 287 """ 288 289 def setUp(self): 290 self.init = generic.BaseReceivingInitializer('init', None) 291 292 293 def test_interface(self): 294 verify.verifyObject(iwokkel.IReceivingInitializer, self.init) 295 296 297 def test_getFeatures(self): 298 self.assertRaises(NotImplementedError, self.init.getFeatures) 299 300 301 def test_initialize(self): 302 d = self.init.initialize() 303 self.init.deferred.callback(None) 304 return d 305 306 307 308 class TestableReceivingInitializer(generic.BaseReceivingInitializer): 309 """ 310 Testable initializer for receiving entities. 311 312 This initializer advertises support for a stream feature denoted by 313 C{uri} and C{name}. Its C{deferred} should be fired to complete 314 initialization for this initializer. 315 316 @ivar uri: Namespace of the stream feature. 317 @ivar localname: Element localname for the stream feature. 318 """ 319 required = True 320 321 def __init__(self, name, xs, uri, localname): 322 generic.BaseReceivingInitializer.__init__(self, name, xs) 323 self.uri = uri 324 self.localname = localname 325 self.deferred = defer.Deferred() 326 327 328 def getFeatures(self): 329 return [domish.Element((self.uri, self.localname))] 330 331 332 333 class FeatureListenAuthenticatorTest(unittest.TestCase): 334 """ 335 Tests for L{generic.FeatureListenAuthenticator}. 336 """ 337 338 def setUp(self): 339 self.gotAuthenticated = False 340 self.initFailure = None 341 self.authenticator = generic.FeatureListenAuthenticator() 342 self.authenticator.namespace = 'jabber:server' 343 self.xmlstream = generic.TestableXmlStream(self.authenticator) 344 self.xmlstream.addObserver('//event/stream/authd', 345 self.onAuthenticated) 346 self.xmlstream.addObserver('//event/xmpp/initfailed', 347 self.onInitFailed) 348 349 self.init = TestableReceivingInitializer('init', self.xmlstream, 350 'testns', 'test') 351 352 def getInitializers(): 353 return [self.init] 354 355 self.authenticator.getInitializers = getInitializers 356 357 358 def onAuthenticated(self, obj): 359 self.gotAuthenticated = True 360 361 362 def onInitFailed(self, failure): 363 self.initFailure = failure 364 365 366 def test_getInitializers(self): 367 """ 368 Unoverridden getInitializers raises NotImplementedError. 369 """ 370 authenticator = generic.FeatureListenAuthenticator() 371 self.assertRaises( 372 NotImplementedError, 373 authenticator.getInitializers) 374 375 376 def test_streamStarted(self): 377 """ 378 Upon stream start, stream initializers are set up. 379 380 The method getInitializers will determine the available stream 381 initializers given the current state of stream initialization. 382 hen, each of the returned initializers will be called to set 383 themselves up. 384 """ 385 xs = self.xmlstream 386 xs.makeConnection(proto_helpers.StringTransport()) 387 xs.dataReceived("<stream:stream xmlns='jabber:server' " 388 "xmlns:stream='http://etherx.jabber.org/streams' " 389 "from='example.com' to='example.org' id='12345' " 390 "version='1.0'>") 391 392 self.assertTrue(xs.headerSent) 393 394 # Check if features were sent 395 features = xs.output[-1] 396 self.assertEquals(xmlstream.NS_STREAMS, features.uri) 397 self.assertEquals('features', features.name) 398 feature = features.elements().next() 399 self.assertEqual('testns', feature.uri) 400 self.assertEqual('test', feature.name) 401 402 self.assertFalse(self.gotAuthenticated) 403 404 self.init.deferred.callback(None) 405 self.assertTrue(self.gotAuthenticated) 406 407 408 def test_streamStartedStreamError(self): 409 """ 410 A stream error raised by the initializer is sent out. 411 """ 412 xs = self.xmlstream 413 xs.makeConnection(proto_helpers.StringTransport()) 414 xs.dataReceived("<stream:stream xmlns='jabber:server' " 415 "xmlns:stream='http://etherx.jabber.org/streams' " 416 "from='example.com' to='example.org' id='12345' " 417 "version='1.0'>") 418 419 self.assertTrue(xs.headerSent) 420 421 xs.output = [] 422 exc = error.StreamError('policy-violation') 423 self.init.deferred.errback(exc) 424 425 self.xmlstream.assertStreamError(self, exc=exc) 426 self.assertFalse(xs.output) 427 self.assertFalse(self.gotAuthenticated) 428 429 430 def test_streamStartedOtherError(self): 431 """ 432 Initializer exceptions are logged and yield a internal-server-error. 433 """ 434 xs = self.xmlstream 435 xs.makeConnection(proto_helpers.StringTransport()) 436 xs.dataReceived("<stream:stream xmlns='jabber:server' " 437 "xmlns:stream='http://etherx.jabber.org/streams' " 438 "from='example.com' to='example.org' id='12345' " 439 "version='1.0'>") 440 441 self.assertTrue(xs.headerSent) 442 443 xs.output = [] 444 class Error(Exception): 445 pass 446 self.init.deferred.errback(Error()) 447 448 self.xmlstream.assertStreamError(self, condition='internal-server-error') 449 self.assertFalse(xs.output) 450 self.assertFalse(self.gotAuthenticated) 451 self.assertEqual(1, len(self.flushLoggedErrors(Error))) 452 453 454 def test_streamStartedInitializerCompleted(self): 455 """ 456 Succesfully finished initializers are recorded. 457 """ 458 xs = self.xmlstream 459 xs.makeConnection(proto_helpers.StringTransport()) 460 xs.dataReceived("<stream:stream xmlns='jabber:server' " 461 "xmlns:stream='http://etherx.jabber.org/streams' " 462 "from='example.com' to='example.org' id='12345' " 463 "version='1.0'>") 464 465 xs.output = [] 466 self.init.deferred.callback(None) 467 self.assertEqual(['init'], self.authenticator.completedInitializers) 468 469 470 def test_streamStartedInitializerCompletedFeatures(self): 471 """ 472 After completing an initializer, stream features are sent again. 473 474 In this case, with only one initializer, there are no more features. 475 """ 476 xs = self.xmlstream 477 xs.makeConnection(proto_helpers.StringTransport()) 478 xs.dataReceived("<stream:stream xmlns='jabber:server' " 479 "xmlns:stream='http://etherx.jabber.org/streams' " 480 "from='example.com' to='example.org' id='12345' " 481 "version='1.0'>") 482 483 xs.output = [] 484 self.init.deferred.callback(None) 485 486 self.assertEqual(1, len(xs.output)) 487 features = xs.output[-1] 488 self.assertEqual('features', features.name) 489 self.assertEqual(xmlstream.NS_STREAMS, features.uri) 490 self.assertFalse(features.children) 491 492 493 def test_streamStartedInitializerCompletedReset(self): 494 """ 495 If an initializer completes with Reset, no features are sent. 496 """ 497 xs = self.xmlstream 498 xs.makeConnection(proto_helpers.StringTransport()) 499 xs.dataReceived("<stream:stream xmlns='jabber:server' " 500 "xmlns:stream='http://etherx.jabber.org/streams' " 501 "from='example.com' to='example.org' id='12345' " 502 "version='1.0'>") 503 504 xs.output = [] 505 self.init.deferred.callback(xmlstream.Reset) 506 507 self.assertEqual(0, len(xs.output)) 508 509 510 def test_streamStartedXmlStanzasRejected(self): 511 """ 512 XML Stanzas may not be sent before feature negotiation has completed. 513 """ 514 xs = self.xmlstream 515 xs.makeConnection(proto_helpers.StringTransport()) 516 xs.dataReceived("<stream:stream xmlns='jabber:server' " 517 "xmlns:stream='http://etherx.jabber.org/streams' " 518 "from='example.com' to='example.org' id='12345' " 519 "version='1.0'>") 520 521 xs.dataReceived("<iq to='example.org' from='example.com' type='set'>" 522 " <query xmlns='jabber:iq:version'/>" 523 "</iq>") 524 525 self.xmlstream.assertStreamError(self, condition='not-authorized') 526 527 528 def test_streamStartedCompleteXmlStanzasAllowed(self): 529 """ 530 XML Stanzas may sent after feature negotiation has completed. 531 """ 532 xs = self.xmlstream 533 xs.makeConnection(proto_helpers.StringTransport()) 534 xs.dataReceived("<stream:stream xmlns='jabber:server' " 535 "xmlns:stream='http://etherx.jabber.org/streams' " 536 "from='example.com' to='example.org' id='12345' " 537 "version='1.0'>") 538 539 self.init.deferred.callback(None) 540 541 xs.dataReceived("<iq to='example.org' from='example.com' type='set'>" 542 " <query xmlns='jabber:iq:version'/>" 543 "</iq>") 544 545 546 def test_streamStartedXmlStanzasHandledIgnored(self): 547 """ 548 XML Stanzas that have already been handled are ignored. 549 """ 550 xs = self.xmlstream 551 xs.makeConnection(proto_helpers.StringTransport()) 552 xs.dataReceived("<stream:stream xmlns='jabber:server' " 553 "xmlns:stream='http://etherx.jabber.org/streams' " 554 "from='example.com' to='example.org' id='12345' " 555 "version='1.0'>") 556 557 iq = generic.parseXml("<iq to='example.org' from='example.com' type='set'>" 558 " <query xmlns='jabber:iq:version'/>" 559 "</iq>") 560 iq.handled = True 561 xs.dispatch(iq) 562 563 564 def test_streamStartedNonXmlStanzasIgnored(self): 565 """ 566 Elements that are not XML Stranzas are not rejected. 567 """ 568 xs = self.xmlstream 569 xs.makeConnection(proto_helpers.StringTransport()) 570 xs.dataReceived("<stream:stream xmlns='jabber:server' " 571 "xmlns:stream='http://etherx.jabber.org/streams' " 572 "from='example.com' to='example.org' id='12345' " 573 "version='1.0'>") 574 575 xs.dataReceived("<test xmlns='myns'/>") 576 577 578 def test_streamStartedCheckStream(self): 579 """ 580 Stream errors raised by checkStream are sent out. 581 """ 582 def checkStream(): 583 raise error.StreamError('undefined-condition') 584 585 self.authenticator.checkStream = checkStream 586 xs = self.xmlstream 587 xs.makeConnection(proto_helpers.StringTransport()) 588 xs.dataReceived("<stream:stream xmlns='jabber:server' " 589 "xmlns:stream='http://etherx.jabber.org/streams' " 590 "from='example.com' to='example.org' id='12345' " 591 "version='1.0'>") 592 593 self.xmlstream.assertStreamError(self, condition='undefined-condition') 594 self.assertFalse(xs.headerSent) 595 596 597 def test_checkStreamNamespace(self): 598 """ 599 The stream namespace must match the pre-defined stream namespace. 600 """ 601 xs = self.xmlstream 602 xs.makeConnection(proto_helpers.StringTransport()) 603 xs.dataReceived("<stream:stream xmlns='jabber:client' " 604 "xmlns:stream='http://etherx.jabber.org/streams' " 605 "from='example.com' to='example.org' id='12345' " 606 "version='1.0'>") 607 608 self.xmlstream.assertStreamError(self, condition='invalid-namespace') 609 610 611 279 612 class PrepareIDNNameTests(unittest.TestCase): 280 613 """ 281 614 Tests for L{wokkel.generic.prepareIDNName}.
Note: See TracBrowser
for help on using the repository browser.