source:
ralphm-patches/listening-authenticator-stream-features.patch
Last change on this file was 79:0752a1cca356, checked in by Ralph Meijer <ralphm@…>, 6 years ago | |
---|---|
File size: 24.7 KB |
-
wokkel/generic.py
# HG changeset patch # Parent 840b96390047670c5209195300f902689c18b12f # Parent 146ff13d66e54c535445adf4a19a3ef07a0a6f57 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 14 14 from zope.interface import implements 15 15 16 16 from twisted.internet import defer, protocol 17 from twisted.python import log 17 18 from twisted.python.deprecate import deprecated 18 19 from twisted.python.versions import Version 19 20 from twisted.words.protocols.jabber import error, xmlstream … … 21 22 from twisted.words.xish import domish, utility 22 23 from twisted.words.xish.xmlstream import BootstrapMixin 23 24 24 from wokkel.iwokkel import IDisco 25 from wokkel.iwokkel import IDisco, IReceivingInitializer 25 26 from wokkel.stanza import Stanza, ErrorStanza, Request 26 27 from wokkel.subprotocols import XMPPHandler 27 28 … … 31 32 NS_VERSION = 'jabber:iq:version' 32 33 VERSION = IQ_GET + '/query[@xmlns="' + NS_VERSION + '"]' 33 34 35 XPATH_ALL = "/*" 36 34 37 def parseXml(string): 35 38 """ 36 39 Parse serialized XML into a DOM structure. … … 180 183 181 184 182 185 186 class TestableXmlStream(xmlstream.XmlStream): 187 """ 188 XML Stream that buffers outgoing data and catches special events. 189 190 This implementation overrides relevant methods to prevent any data 191 to be sent out on a transport. Instead it buffers all outgoing stanzas, 192 sets flags instead of sending the stream header and footer and logs stream 193 errors so it can be caught by a logging observer. 194 195 @ivar output: Sequence of objects sent out using L{send}. Usually these are 196 L{domish.Element} instances. 197 @type output: C{list} 198 199 @ivar headerSent: Flag set when a stream header would have been sent. When 200 a stream restart occurs through L{reset}, this flag is reset as well. 201 @type headerSent: C{bool} 202 203 @ivar footerSent: Flag set when a stream footer would have been sent 204 explicitly. Note that it is not set when a stream error is sent 205 using L{sendStreamError}. 206 @type footerSent: C{bool} 207 """ 208 209 210 def __init__(self, authenticator): 211 xmlstream.XmlStream.__init__(self, authenticator) 212 self.headerSent = False 213 self.footerSent = False 214 self.output = [] 215 216 217 def reset(self): 218 xmlstream.XmlStream.reset(self) 219 self.headerSent = False 220 221 222 def sendHeader(self): 223 self.headerSent = True 224 225 226 def sendFooter(self): 227 self.footerSent = True 228 229 230 def sendStreamError(self, streamError): 231 """ 232 Log a stream error. 233 234 If this is called from a Twisted Trial test case, the stream error 235 will be observed by the Trial logging observer. If it is not explicitly 236 tested for (i.e. flushed), this will cause the test case to 237 automatically fail. See L{assertStreamError} for a convenience method 238 to test for stream errors. 239 240 @type streamError: L{error.StreamError} 241 """ 242 log.err(streamError) 243 244 245 @staticmethod 246 def assertStreamError(testcase, condition=None, exc=None): 247 """ 248 Check if a stream error was sent out. 249 250 To check for stream errors sent out by L{sendStreamError}, this method 251 will flush logged stream errors and inspect the last one. If 252 C{condition} was passed, the logged error is asserted to match that 253 condition. If C{exc} was passed, the logged error is asserted to be 254 identical to it. 255 256 Note that this is takes the calling test case as the first argument, to 257 be able to hook into its methods for flushing errors and making 258 assertions. 259 260 @param testcase: The test case instance that is calling this method. 261 @type testcase: {twisted.trial.unittest.TestCase} 262 263 @param condition: The optional stream error condition to match against. 264 @type condition: C{unicode}. 265 266 @param exc: The optional stream error to check identity against. 267 @type exc: L{error.StreamError} 268 """ 269 270 loggedErrors = testcase.flushLoggedErrors(error.StreamError) 271 testcase.assertTrue(loggedErrors, "No stream error was sent") 272 streamError = loggedErrors[-1].value 273 if condition: 274 testcase.assertEqual(condition, streamError.condition) 275 elif exc: 276 testcase.assertIdentical(exc, streamError) 277 278 279 def send(self, obj): 280 """ 281 Buffer all outgoing stanzas. 282 283 @type obj: L{domish.Element} 284 """ 285 self.output.append(obj) 286 287 288 289 class BaseReceivingInitializer(object): 290 """ 291 Base stream initializer for receiving entities. 292 """ 293 implements(IReceivingInitializer) 294 295 required = False 296 297 def __init__(self, name, xs): 298 self.name = name 299 self.xmlstream = xs 300 self.deferred = defer.Deferred() 301 302 303 def getFeatures(self): 304 raise NotImplementedError() 305 306 307 def initialize(self): 308 return self.deferred 309 310 311 312 class FeatureListenAuthenticator(xmlstream.ListenAuthenticator): 313 """ 314 Authenticator for receiving entities with support for initializers. 315 """ 316 317 def __init__(self): 318 self.completedInitializers = [] 319 320 321 def _onElementFallback(self, element): 322 """ 323 Fallback observer that rejects XML Stanzas. 324 325 This observer is active while stream feature negotiation has not yet 326 completed. 327 """ 328 # ignore elements that are not XML Stanzas 329 if (element.uri not in (self.namespace) or 330 element.name not in ('iq', 'message', 'presence')): 331 return 332 333 # ignore elements that have already been handled 334 if element.handled: 335 return 336 337 exc = error.StreamError('not-authorized') 338 self.xmlstream.sendStreamError(exc) 339 340 341 def connectionMade(self): 342 """ 343 Called when the connection has been made. 344 345 Adds an observer to reject XML Stanzas until stream feature negotiation 346 has completed. 347 """ 348 xmlstream.ListenAuthenticator.connectionMade(self) 349 self.xmlstream.addObserver(XPATH_ALL, self._onElementFallback, -1) 350 351 352 def _cbInit(self, result): 353 """ 354 Mark the initializer as completed and continue to the next. 355 """ 356 result, index = result 357 self.completedInitializers.append(self._initializers[index].name) 358 del self._initializers[index] 359 360 if result is xmlstream.Reset: 361 # The initializer initiated a stream restart, bail. 362 return 363 else: 364 self._initializeStream() 365 366 367 def _ebInit(self, failure): 368 """ 369 Called when an initializer raises an exception. 370 371 If the exception is a L{error.StreamError} it is sent out, otherwise 372 the error is logged and a stream error with condition 373 C{'internal-server-error'} is sent out instead. 374 """ 375 firstError = failure.value 376 subFailure = firstError.subFailure 377 if subFailure.check(error.StreamError): 378 exc = subFailure.value 379 else: 380 log.err(subFailure, index=firstError.index) 381 exc = error.StreamError('internal-server-error') 382 383 self.xmlstream.sendStreamError(exc) 384 385 386 def _initializeStream(self): 387 """ 388 Initialize the stream. 389 390 This walks all initializers to retrieve their features and determine 391 if there is at least one required initializer. If not, the stream is 392 ready for the exchange of stanzas. The features are sent out and each 393 initializer will have its C{initialize} method called. 394 395 If negation has completed, L{xmlstream.STREAM_AUTHD_EVENT} is 396 dispatched and the observer rejecting incoming stanzas is removed. 397 """ 398 features = domish.Element((xmlstream.NS_STREAMS, 'features')) 399 ds = [] 400 required = False 401 for initializer in self._initializers: 402 required = required or initializer.required 403 for feature in initializer.getFeatures(): 404 features.addChild(feature) 405 d = initializer.initialize() 406 ds.append(d) 407 408 self.xmlstream.send(features) 409 410 if not required: 411 # There are no required initializers anymore. This stream is 412 # now ready for the exchange of stanzas. 413 self.xmlstream.removeObserver(XPATH_ALL, self._onElementFallback) 414 self.xmlstream.dispatch(self.xmlstream, xmlstream.STREAM_AUTHD_EVENT) 415 416 if ds: 417 d = defer.DeferredList(ds, fireOnOneCallback=True, 418 fireOnOneErrback=True, 419 consumeErrors=True) 420 d.addCallbacks(self._cbInit, self._ebInit) 421 422 423 def getInitializers(self): 424 """ 425 Get the initializers for the current stage of stream negotiation. 426 427 This will be called at the start of each stream start to retrieve 428 the initializers for which the features are advertised and initiated. 429 430 @rtype: C{list} of C{IReceivingInitializer} instances. 431 """ 432 raise NotImplementedError() 433 434 435 def checkStream(self): 436 """ 437 Check the stream before sending out a stream header and initialization. 438 439 This is the place to inspect the stream properties and raise a relevant 440 L{error.StreamError} if needed. 441 """ 442 # Check stream namespace 443 if self.xmlstream.namespace != self.namespace: 444 self.xmlstream.namespace = self.namespace 445 raise error.StreamError('invalid-namespace') 446 447 448 def streamStarted(self, rootElement): 449 """ 450 Called when the stream header has been received. 451 452 Check the stream properties through L{checkStream}, send out 453 a stream header, and retrieve and initialize the stream initializers. 454 """ 455 xmlstream.ListenAuthenticator.streamStarted(self, rootElement) 456 457 try: 458 self.checkStream() 459 except error.StreamError, exc: 460 self.xmlstream.sendStreamError(exc) 461 return 462 463 self.xmlstream.sendHeader() 464 465 self._initializers = self.getInitializers() 466 self._initializeStream() 467 468 469 183 470 @deprecated(Version("Wokkel", 0, 8, 0), "unicode.encode('idna')") 184 471 def prepareIDNName(name): 185 472 """ -
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' … … 300 305 301 306 302 307 308 class BaseReceivingInitializerTest(unittest.TestCase): 309 """ 310 Tests for L{generic.BaseReceivingInitializer}. 311 """ 312 313 def setUp(self): 314 self.init = generic.BaseReceivingInitializer('init', None) 315 316 317 def test_interface(self): 318 verify.verifyObject(iwokkel.IReceivingInitializer, self.init) 319 320 321 def test_getFeatures(self): 322 self.assertRaises(NotImplementedError, self.init.getFeatures) 323 324 325 def test_initialize(self): 326 d = self.init.initialize() 327 self.init.deferred.callback(None) 328 return d 329 330 331 332 class TestableReceivingInitializer(generic.BaseReceivingInitializer): 333 """ 334 Testable initializer for receiving entities. 335 336 This initializer advertises support for a stream feature denoted by 337 C{uri} and C{name}. Its C{deferred} should be fired to complete 338 initialization for this initializer. 339 340 @ivar uri: Namespace of the stream feature. 341 @ivar localname: Element localname for the stream feature. 342 """ 343 required = True 344 345 def __init__(self, name, xs, uri, localname): 346 generic.BaseReceivingInitializer.__init__(self, name, xs) 347 self.uri = uri 348 self.localname = localname 349 self.deferred = defer.Deferred() 350 351 352 def getFeatures(self): 353 return [domish.Element((self.uri, self.localname))] 354 355 356 357 class FeatureListenAuthenticatorTest(unittest.TestCase): 358 """ 359 Tests for L{generic.FeatureListenAuthenticator}. 360 """ 361 362 def setUp(self): 363 self.gotAuthenticated = False 364 self.initFailure = None 365 self.authenticator = generic.FeatureListenAuthenticator() 366 self.authenticator.namespace = 'jabber:server' 367 self.xmlstream = generic.TestableXmlStream(self.authenticator) 368 self.xmlstream.addObserver('//event/stream/authd', 369 self.onAuthenticated) 370 self.xmlstream.addObserver('//event/xmpp/initfailed', 371 self.onInitFailed) 372 373 self.init = TestableReceivingInitializer('init', self.xmlstream, 374 'testns', 'test') 375 376 def getInitializers(): 377 return [self.init] 378 379 self.authenticator.getInitializers = getInitializers 380 381 382 def onAuthenticated(self, obj): 383 self.gotAuthenticated = True 384 385 386 def onInitFailed(self, failure): 387 self.initFailure = failure 388 389 390 def test_getInitializers(self): 391 """ 392 Unoverridden getInitializers raises NotImplementedError. 393 """ 394 authenticator = generic.FeatureListenAuthenticator() 395 self.assertRaises( 396 NotImplementedError, 397 authenticator.getInitializers) 398 399 400 def test_streamStarted(self): 401 """ 402 Upon stream start, stream initializers are set up. 403 404 The method getInitializers will determine the available stream 405 initializers given the current state of stream initialization. 406 hen, each of the returned initializers will be called to set 407 themselves up. 408 """ 409 xs = self.xmlstream 410 xs.makeConnection(proto_helpers.StringTransport()) 411 xs.dataReceived("<stream:stream xmlns='jabber:server' " 412 "xmlns:stream='http://etherx.jabber.org/streams' " 413 "from='example.com' to='example.org' id='12345' " 414 "version='1.0'>") 415 416 self.assertTrue(xs.headerSent) 417 418 # Check if features were sent 419 features = xs.output[-1] 420 self.assertEquals(xmlstream.NS_STREAMS, features.uri) 421 self.assertEquals('features', features.name) 422 feature = features.elements().next() 423 self.assertEqual('testns', feature.uri) 424 self.assertEqual('test', feature.name) 425 426 self.assertFalse(self.gotAuthenticated) 427 428 self.init.deferred.callback(None) 429 self.assertTrue(self.gotAuthenticated) 430 431 432 def test_streamStartedStreamError(self): 433 """ 434 A stream error raised by the initializer is sent out. 435 """ 436 xs = self.xmlstream 437 xs.makeConnection(proto_helpers.StringTransport()) 438 xs.dataReceived("<stream:stream xmlns='jabber:server' " 439 "xmlns:stream='http://etherx.jabber.org/streams' " 440 "from='example.com' to='example.org' id='12345' " 441 "version='1.0'>") 442 443 self.assertTrue(xs.headerSent) 444 445 xs.output = [] 446 exc = error.StreamError('policy-violation') 447 self.init.deferred.errback(exc) 448 449 self.xmlstream.assertStreamError(self, exc=exc) 450 self.assertFalse(xs.output) 451 self.assertFalse(self.gotAuthenticated) 452 453 454 def test_streamStartedOtherError(self): 455 """ 456 Initializer exceptions are logged and yield a internal-server-error. 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 self.assertTrue(xs.headerSent) 466 467 xs.output = [] 468 class Error(Exception): 469 pass 470 self.init.deferred.errback(Error()) 471 472 self.xmlstream.assertStreamError(self, condition='internal-server-error') 473 self.assertFalse(xs.output) 474 self.assertFalse(self.gotAuthenticated) 475 self.assertEqual(1, len(self.flushLoggedErrors(Error))) 476 477 478 def test_streamStartedInitializerCompleted(self): 479 """ 480 Succesfully finished initializers are recorded. 481 """ 482 xs = self.xmlstream 483 xs.makeConnection(proto_helpers.StringTransport()) 484 xs.dataReceived("<stream:stream xmlns='jabber:server' " 485 "xmlns:stream='http://etherx.jabber.org/streams' " 486 "from='example.com' to='example.org' id='12345' " 487 "version='1.0'>") 488 489 xs.output = [] 490 self.init.deferred.callback(None) 491 self.assertEqual(['init'], self.authenticator.completedInitializers) 492 493 494 def test_streamStartedInitializerCompletedFeatures(self): 495 """ 496 After completing an initializer, stream features are sent again. 497 498 In this case, with only one initializer, there are no more features. 499 """ 500 xs = self.xmlstream 501 xs.makeConnection(proto_helpers.StringTransport()) 502 xs.dataReceived("<stream:stream xmlns='jabber:server' " 503 "xmlns:stream='http://etherx.jabber.org/streams' " 504 "from='example.com' to='example.org' id='12345' " 505 "version='1.0'>") 506 507 xs.output = [] 508 self.init.deferred.callback(None) 509 510 self.assertEqual(1, len(xs.output)) 511 features = xs.output[-1] 512 self.assertEqual('features', features.name) 513 self.assertEqual(xmlstream.NS_STREAMS, features.uri) 514 self.assertFalse(features.children) 515 516 517 def test_streamStartedInitializerCompletedReset(self): 518 """ 519 If an initializer completes with Reset, no features are sent. 520 """ 521 xs = self.xmlstream 522 xs.makeConnection(proto_helpers.StringTransport()) 523 xs.dataReceived("<stream:stream xmlns='jabber:server' " 524 "xmlns:stream='http://etherx.jabber.org/streams' " 525 "from='example.com' to='example.org' id='12345' " 526 "version='1.0'>") 527 528 xs.output = [] 529 self.init.deferred.callback(xmlstream.Reset) 530 531 self.assertEqual(0, len(xs.output)) 532 533 534 def test_streamStartedXmlStanzasRejected(self): 535 """ 536 XML Stanzas may not be sent before feature negotiation has completed. 537 """ 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 xs.dataReceived("<iq to='example.org' from='example.com' type='set'>" 546 " <query xmlns='jabber:iq:version'/>" 547 "</iq>") 548 549 self.xmlstream.assertStreamError(self, condition='not-authorized') 550 551 552 def test_streamStartedCompleteXmlStanzasAllowed(self): 553 """ 554 XML Stanzas may sent after feature negotiation has completed. 555 """ 556 xs = self.xmlstream 557 xs.makeConnection(proto_helpers.StringTransport()) 558 xs.dataReceived("<stream:stream xmlns='jabber:server' " 559 "xmlns:stream='http://etherx.jabber.org/streams' " 560 "from='example.com' to='example.org' id='12345' " 561 "version='1.0'>") 562 563 self.init.deferred.callback(None) 564 565 xs.dataReceived("<iq to='example.org' from='example.com' type='set'>" 566 " <query xmlns='jabber:iq:version'/>" 567 "</iq>") 568 569 570 def test_streamStartedXmlStanzasHandledIgnored(self): 571 """ 572 XML Stanzas that have already been handled are ignored. 573 """ 574 xs = self.xmlstream 575 xs.makeConnection(proto_helpers.StringTransport()) 576 xs.dataReceived("<stream:stream xmlns='jabber:server' " 577 "xmlns:stream='http://etherx.jabber.org/streams' " 578 "from='example.com' to='example.org' id='12345' " 579 "version='1.0'>") 580 581 iq = generic.parseXml("<iq to='example.org' from='example.com' type='set'>" 582 " <query xmlns='jabber:iq:version'/>" 583 "</iq>") 584 iq.handled = True 585 xs.dispatch(iq) 586 587 588 def test_streamStartedNonXmlStanzasIgnored(self): 589 """ 590 Elements that are not XML Stranzas are not rejected. 591 """ 592 xs = self.xmlstream 593 xs.makeConnection(proto_helpers.StringTransport()) 594 xs.dataReceived("<stream:stream xmlns='jabber:server' " 595 "xmlns:stream='http://etherx.jabber.org/streams' " 596 "from='example.com' to='example.org' id='12345' " 597 "version='1.0'>") 598 599 xs.dataReceived("<test xmlns='myns'/>") 600 601 602 def test_streamStartedCheckStream(self): 603 """ 604 Stream errors raised by checkStream are sent out. 605 """ 606 def checkStream(): 607 raise error.StreamError('undefined-condition') 608 609 self.authenticator.checkStream = checkStream 610 xs = self.xmlstream 611 xs.makeConnection(proto_helpers.StringTransport()) 612 xs.dataReceived("<stream:stream xmlns='jabber:server' " 613 "xmlns:stream='http://etherx.jabber.org/streams' " 614 "from='example.com' to='example.org' id='12345' " 615 "version='1.0'>") 616 617 self.xmlstream.assertStreamError(self, condition='undefined-condition') 618 self.assertFalse(xs.headerSent) 619 620 621 def test_checkStreamNamespace(self): 622 """ 623 The stream namespace must match the pre-defined stream namespace. 624 """ 625 xs = self.xmlstream 626 xs.makeConnection(proto_helpers.StringTransport()) 627 xs.dataReceived("<stream:stream xmlns='jabber:client' " 628 "xmlns:stream='http://etherx.jabber.org/streams' " 629 "from='example.com' to='example.org' id='12345' " 630 "version='1.0'>") 631 632 self.xmlstream.assertStreamError(self, condition='invalid-namespace') 633 634 635 303 636 class PrepareIDNNameTests(unittest.TestCase): 304 637 """ 305 638 Tests for L{wokkel.generic.prepareIDNName}.
Note: See TracBrowser
for help on using the repository browser.