[64] | 1 | # HG changeset patch |
---|
[72] | 2 | # Parent 840b96390047670c5209195300f902689c18b12f |
---|
[79] | 3 | # Parent 146ff13d66e54c535445adf4a19a3ef07a0a6f57 |
---|
[65] | 4 | Add FeatureListeningAuthenticator. |
---|
| 5 | |
---|
| 6 | This new authenticator is for incoming streams and uses initializers |
---|
| 7 | for stream negotiation, similar to inializers for clients. |
---|
| 8 | |
---|
[64] | 9 | diff --git a/wokkel/generic.py b/wokkel/generic.py |
---|
| 10 | --- a/wokkel/generic.py |
---|
| 11 | +++ b/wokkel/generic.py |
---|
[79] | 12 | @@ -14,6 +14,7 @@ |
---|
[66] | 13 | from zope.interface import implements |
---|
| 14 | |
---|
| 15 | from twisted.internet import defer, protocol |
---|
[79] | 16 | +from twisted.python import log |
---|
[72] | 17 | from twisted.python.deprecate import deprecated |
---|
| 18 | from twisted.python.versions import Version |
---|
[79] | 19 | from twisted.words.protocols.jabber import error, xmlstream |
---|
| 20 | @@ -21,7 +22,7 @@ |
---|
[66] | 21 | from twisted.words.xish import domish, utility |
---|
| 22 | from twisted.words.xish.xmlstream import BootstrapMixin |
---|
| 23 | |
---|
| 24 | -from wokkel.iwokkel import IDisco |
---|
| 25 | +from wokkel.iwokkel import IDisco, IReceivingInitializer |
---|
[79] | 26 | from wokkel.stanza import Stanza, ErrorStanza, Request |
---|
[66] | 27 | from wokkel.subprotocols import XMPPHandler |
---|
| 28 | |
---|
[79] | 29 | @@ -31,6 +32,8 @@ |
---|
[65] | 30 | NS_VERSION = 'jabber:iq:version' |
---|
| 31 | VERSION = IQ_GET + '/query[@xmlns="' + NS_VERSION + '"]' |
---|
| 32 | |
---|
| 33 | +XPATH_ALL = "/*" |
---|
| 34 | + |
---|
| 35 | def parseXml(string): |
---|
| 36 | """ |
---|
| 37 | Parse serialized XML into a DOM structure. |
---|
[79] | 38 | @@ -180,6 +183,290 @@ |
---|
[64] | 39 | |
---|
[72] | 40 | |
---|
| 41 | |
---|
[64] | 42 | +class TestableXmlStream(xmlstream.XmlStream): |
---|
[66] | 43 | + """ |
---|
| 44 | + XML Stream that buffers outgoing data and catches special events. |
---|
| 45 | + |
---|
| 46 | + This implementation overrides relevant methods to prevent any data |
---|
| 47 | + to be sent out on a transport. Instead it buffers all outgoing stanzas, |
---|
| 48 | + sets flags instead of sending the stream header and footer and logs stream |
---|
| 49 | + errors so it can be caught by a logging observer. |
---|
| 50 | + |
---|
| 51 | + @ivar output: Sequence of objects sent out using L{send}. Usually these are |
---|
| 52 | + L{domish.Element} instances. |
---|
| 53 | + @type output: C{list} |
---|
| 54 | + |
---|
| 55 | + @ivar headerSent: Flag set when a stream header would have been sent. When |
---|
| 56 | + a stream restart occurs through L{reset}, this flag is reset as well. |
---|
| 57 | + @type headerSent: C{bool} |
---|
| 58 | + |
---|
| 59 | + @ivar footerSent: Flag set when a stream footer would have been sent |
---|
| 60 | + explicitly. Note that it is not set when a stream error is sent |
---|
| 61 | + using L{sendStreamError}. |
---|
| 62 | + @type footerSent: C{bool} |
---|
| 63 | + """ |
---|
| 64 | + |
---|
[64] | 65 | + |
---|
| 66 | + def __init__(self, authenticator): |
---|
| 67 | + xmlstream.XmlStream.__init__(self, authenticator) |
---|
| 68 | + self.headerSent = False |
---|
| 69 | + self.footerSent = False |
---|
| 70 | + self.output = [] |
---|
| 71 | + |
---|
| 72 | + |
---|
| 73 | + def reset(self): |
---|
| 74 | + xmlstream.XmlStream.reset(self) |
---|
| 75 | + self.headerSent = False |
---|
| 76 | + |
---|
| 77 | + |
---|
| 78 | + def sendHeader(self): |
---|
| 79 | + self.headerSent = True |
---|
| 80 | + |
---|
| 81 | + |
---|
| 82 | + def sendFooter(self): |
---|
| 83 | + self.footerSent = True |
---|
| 84 | + |
---|
| 85 | + |
---|
| 86 | + def sendStreamError(self, streamError): |
---|
[66] | 87 | + """ |
---|
| 88 | + Log a stream error. |
---|
| 89 | + |
---|
| 90 | + If this is called from a Twisted Trial test case, the stream error |
---|
| 91 | + will be observed by the Trial logging observer. If it is not explicitly |
---|
| 92 | + tested for (i.e. flushed), this will cause the test case to |
---|
| 93 | + automatically fail. See L{assertStreamError} for a convenience method |
---|
| 94 | + to test for stream errors. |
---|
| 95 | + |
---|
| 96 | + @type streamError: L{error.StreamError} |
---|
| 97 | + """ |
---|
| 98 | + log.err(streamError) |
---|
| 99 | + |
---|
| 100 | + |
---|
| 101 | + @staticmethod |
---|
| 102 | + def assertStreamError(testcase, condition=None, exc=None): |
---|
| 103 | + """ |
---|
| 104 | + Check if a stream error was sent out. |
---|
| 105 | + |
---|
| 106 | + To check for stream errors sent out by L{sendStreamError}, this method |
---|
| 107 | + will flush logged stream errors and inspect the last one. If |
---|
| 108 | + C{condition} was passed, the logged error is asserted to match that |
---|
| 109 | + condition. If C{exc} was passed, the logged error is asserted to be |
---|
| 110 | + identical to it. |
---|
| 111 | + |
---|
| 112 | + Note that this is takes the calling test case as the first argument, to |
---|
| 113 | + be able to hook into its methods for flushing errors and making |
---|
| 114 | + assertions. |
---|
| 115 | + |
---|
| 116 | + @param testcase: The test case instance that is calling this method. |
---|
| 117 | + @type testcase: {twisted.trial.unittest.TestCase} |
---|
| 118 | + |
---|
| 119 | + @param condition: The optional stream error condition to match against. |
---|
| 120 | + @type condition: C{unicode}. |
---|
| 121 | + |
---|
| 122 | + @param exc: The optional stream error to check identity against. |
---|
| 123 | + @type exc: L{error.StreamError} |
---|
| 124 | + """ |
---|
| 125 | + |
---|
| 126 | + loggedErrors = testcase.flushLoggedErrors(error.StreamError) |
---|
| 127 | + testcase.assertTrue(loggedErrors, "No stream error was sent") |
---|
| 128 | + streamError = loggedErrors[-1].value |
---|
| 129 | + if condition: |
---|
| 130 | + testcase.assertEqual(condition, streamError.condition) |
---|
| 131 | + elif exc: |
---|
| 132 | + testcase.assertIdentical(exc, streamError) |
---|
[64] | 133 | + |
---|
| 134 | + |
---|
| 135 | + def send(self, obj): |
---|
[66] | 136 | + """ |
---|
| 137 | + Buffer all outgoing stanzas. |
---|
| 138 | + |
---|
| 139 | + @type obj: L{domish.Element} |
---|
| 140 | + """ |
---|
[64] | 141 | + self.output.append(obj) |
---|
| 142 | + |
---|
| 143 | + |
---|
| 144 | + |
---|
[66] | 145 | +class BaseReceivingInitializer(object): |
---|
| 146 | + """ |
---|
| 147 | + Base stream initializer for receiving entities. |
---|
| 148 | + """ |
---|
| 149 | + implements(IReceivingInitializer) |
---|
| 150 | + |
---|
| 151 | + required = False |
---|
| 152 | + |
---|
| 153 | + def __init__(self, name, xs): |
---|
| 154 | + self.name = name |
---|
| 155 | + self.xmlstream = xs |
---|
| 156 | + self.deferred = defer.Deferred() |
---|
| 157 | + |
---|
| 158 | + |
---|
| 159 | + def getFeatures(self): |
---|
| 160 | + raise NotImplementedError() |
---|
| 161 | + |
---|
| 162 | + |
---|
| 163 | + def initialize(self): |
---|
| 164 | + return self.deferred |
---|
| 165 | + |
---|
| 166 | + |
---|
| 167 | + |
---|
[64] | 168 | +class FeatureListenAuthenticator(xmlstream.ListenAuthenticator): |
---|
| 169 | + """ |
---|
| 170 | + Authenticator for receiving entities with support for initializers. |
---|
| 171 | + """ |
---|
| 172 | + |
---|
[65] | 173 | + def __init__(self): |
---|
| 174 | + self.completedInitializers = [] |
---|
| 175 | + |
---|
| 176 | + |
---|
| 177 | + def _onElementFallback(self, element): |
---|
| 178 | + """ |
---|
| 179 | + Fallback observer that rejects XML Stanzas. |
---|
| 180 | + |
---|
| 181 | + This observer is active while stream feature negotiation has not yet |
---|
| 182 | + completed. |
---|
| 183 | + """ |
---|
| 184 | + # ignore elements that are not XML Stanzas |
---|
| 185 | + if (element.uri not in (self.namespace) or |
---|
| 186 | + element.name not in ('iq', 'message', 'presence')): |
---|
| 187 | + return |
---|
| 188 | + |
---|
| 189 | + # ignore elements that have already been handled |
---|
| 190 | + if element.handled: |
---|
| 191 | + return |
---|
| 192 | + |
---|
| 193 | + exc = error.StreamError('not-authorized') |
---|
| 194 | + self.xmlstream.sendStreamError(exc) |
---|
| 195 | + |
---|
| 196 | + |
---|
[66] | 197 | + def connectionMade(self): |
---|
| 198 | + """ |
---|
| 199 | + Called when the connection has been made. |
---|
| 200 | + |
---|
| 201 | + Adds an observer to reject XML Stanzas until stream feature negotiation |
---|
| 202 | + has completed. |
---|
| 203 | + """ |
---|
| 204 | + xmlstream.ListenAuthenticator.connectionMade(self) |
---|
| 205 | + self.xmlstream.addObserver(XPATH_ALL, self._onElementFallback, -1) |
---|
| 206 | + |
---|
| 207 | + |
---|
| 208 | + def _cbInit(self, result): |
---|
| 209 | + """ |
---|
| 210 | + Mark the initializer as completed and continue to the next. |
---|
| 211 | + """ |
---|
| 212 | + result, index = result |
---|
| 213 | + self.completedInitializers.append(self._initializers[index].name) |
---|
| 214 | + del self._initializers[index] |
---|
| 215 | + |
---|
| 216 | + if result is xmlstream.Reset: |
---|
| 217 | + # The initializer initiated a stream restart, bail. |
---|
| 218 | + return |
---|
| 219 | + else: |
---|
[64] | 220 | + self._initializeStream() |
---|
| 221 | + |
---|
[66] | 222 | + |
---|
| 223 | + def _ebInit(self, failure): |
---|
| 224 | + """ |
---|
| 225 | + Called when an initializer raises an exception. |
---|
| 226 | + |
---|
| 227 | + If the exception is a L{error.StreamError} it is sent out, otherwise |
---|
| 228 | + the error is logged and a stream error with condition |
---|
| 229 | + C{'internal-server-error'} is sent out instead. |
---|
| 230 | + """ |
---|
| 231 | + firstError = failure.value |
---|
| 232 | + subFailure = firstError.subFailure |
---|
| 233 | + if subFailure.check(error.StreamError): |
---|
| 234 | + exc = subFailure.value |
---|
| 235 | + else: |
---|
| 236 | + log.err(subFailure, index=firstError.index) |
---|
| 237 | + exc = error.StreamError('internal-server-error') |
---|
| 238 | + |
---|
| 239 | + self.xmlstream.sendStreamError(exc) |
---|
| 240 | + |
---|
| 241 | + |
---|
| 242 | + def _initializeStream(self): |
---|
| 243 | + """ |
---|
| 244 | + Initialize the stream. |
---|
| 245 | + |
---|
| 246 | + This walks all initializers to retrieve their features and determine |
---|
| 247 | + if there is at least one required initializer. If not, the stream is |
---|
| 248 | + ready for the exchange of stanzas. The features are sent out and each |
---|
| 249 | + initializer will have its C{initialize} method called. |
---|
| 250 | + |
---|
| 251 | + If negation has completed, L{xmlstream.STREAM_AUTHD_EVENT} is |
---|
| 252 | + dispatched and the observer rejecting incoming stanzas is removed. |
---|
| 253 | + """ |
---|
[64] | 254 | + features = domish.Element((xmlstream.NS_STREAMS, 'features')) |
---|
| 255 | + ds = [] |
---|
| 256 | + required = False |
---|
[65] | 257 | + for initializer in self._initializers: |
---|
[64] | 258 | + required = required or initializer.required |
---|
| 259 | + for feature in initializer.getFeatures(): |
---|
| 260 | + features.addChild(feature) |
---|
| 261 | + d = initializer.initialize() |
---|
| 262 | + ds.append(d) |
---|
[66] | 263 | + |
---|
[64] | 264 | + self.xmlstream.send(features) |
---|
[66] | 265 | + |
---|
[64] | 266 | + if not required: |
---|
[66] | 267 | + # There are no required initializers anymore. This stream is |
---|
| 268 | + # now ready for the exchange of stanzas. |
---|
[65] | 269 | + self.xmlstream.removeObserver(XPATH_ALL, self._onElementFallback) |
---|
[64] | 270 | + self.xmlstream.dispatch(self.xmlstream, xmlstream.STREAM_AUTHD_EVENT) |
---|
[66] | 271 | + |
---|
[64] | 272 | + if ds: |
---|
[66] | 273 | + d = defer.DeferredList(ds, fireOnOneCallback=True, |
---|
| 274 | + fireOnOneErrback=True, |
---|
| 275 | + consumeErrors=True) |
---|
| 276 | + d.addCallbacks(self._cbInit, self._ebInit) |
---|
| 277 | + |
---|
[64] | 278 | + |
---|
[65] | 279 | + def getInitializers(self): |
---|
[66] | 280 | + """ |
---|
| 281 | + Get the initializers for the current stage of stream negotiation. |
---|
| 282 | + |
---|
| 283 | + This will be called at the start of each stream start to retrieve |
---|
| 284 | + the initializers for which the features are advertised and initiated. |
---|
| 285 | + |
---|
| 286 | + @rtype: C{list} of C{IReceivingInitializer} instances. |
---|
| 287 | + """ |
---|
| 288 | + raise NotImplementedError() |
---|
[65] | 289 | + |
---|
| 290 | + |
---|
| 291 | + def checkStream(self): |
---|
[66] | 292 | + """ |
---|
| 293 | + Check the stream before sending out a stream header and initialization. |
---|
| 294 | + |
---|
| 295 | + This is the place to inspect the stream properties and raise a relevant |
---|
| 296 | + L{error.StreamError} if needed. |
---|
| 297 | + """ |
---|
| 298 | + # Check stream namespace |
---|
[65] | 299 | + if self.xmlstream.namespace != self.namespace: |
---|
| 300 | + self.xmlstream.namespace = self.namespace |
---|
| 301 | + raise error.StreamError('invalid-namespace') |
---|
[64] | 302 | + |
---|
| 303 | + |
---|
| 304 | + def streamStarted(self, rootElement): |
---|
[66] | 305 | + """ |
---|
| 306 | + Called when the stream header has been received. |
---|
| 307 | + |
---|
| 308 | + Check the stream properties through L{checkStream}, send out |
---|
| 309 | + a stream header, and retrieve and initialize the stream initializers. |
---|
| 310 | + """ |
---|
[64] | 311 | + xmlstream.ListenAuthenticator.streamStarted(self, rootElement) |
---|
[65] | 312 | + |
---|
| 313 | + try: |
---|
| 314 | + self.checkStream() |
---|
| 315 | + except error.StreamError, exc: |
---|
| 316 | + self.xmlstream.sendStreamError(exc) |
---|
| 317 | + return |
---|
| 318 | + |
---|
[64] | 319 | + self.xmlstream.sendHeader() |
---|
[65] | 320 | + |
---|
| 321 | + self._initializers = self.getInitializers() |
---|
[64] | 322 | + self._initializeStream() |
---|
[72] | 323 | + |
---|
| 324 | + |
---|
| 325 | + |
---|
| 326 | @deprecated(Version("Wokkel", 0, 8, 0), "unicode.encode('idna')") |
---|
| 327 | def prepareIDNName(name): |
---|
| 328 | """ |
---|
[66] | 329 | diff --git a/wokkel/iwokkel.py b/wokkel/iwokkel.py |
---|
| 330 | --- a/wokkel/iwokkel.py |
---|
| 331 | +++ b/wokkel/iwokkel.py |
---|
| 332 | @@ -11,7 +11,7 @@ |
---|
| 333 | 'IPubSubClient', 'IPubSubService', 'IPubSubResource', |
---|
| 334 | 'IMUCClient', 'IMUCStatuses'] |
---|
| 335 | |
---|
| 336 | -from zope.interface import Interface |
---|
| 337 | +from zope.interface import Attribute, Interface |
---|
| 338 | from twisted.python.deprecate import deprecatedModuleAttribute |
---|
| 339 | from twisted.python.versions import Version |
---|
| 340 | from twisted.words.protocols.jabber.ijabber import IXMPPHandler |
---|
| 341 | @@ -982,3 +982,50 @@ |
---|
| 342 | """ |
---|
| 343 | Return the number of status conditions. |
---|
| 344 | """ |
---|
| 345 | + |
---|
| 346 | + |
---|
| 347 | + |
---|
| 348 | +class IReceivingInitializer(Interface): |
---|
| 349 | + """ |
---|
| 350 | + Interface for XMPP stream initializers for receiving entities. |
---|
| 351 | + """ |
---|
| 352 | + |
---|
| 353 | + required = Attribute( |
---|
| 354 | + """ |
---|
| 355 | + This initializer is required to complete feature negotiation. |
---|
| 356 | + """) |
---|
| 357 | + name = Attribute( |
---|
| 358 | + """ |
---|
| 359 | + Identifier for this initializer. |
---|
| 360 | + |
---|
| 361 | + This identifier is included in |
---|
| 362 | + L{wokkel.generic.FeatureListenAuthenticator} when an initializer has |
---|
| 363 | + completed. |
---|
| 364 | + """) |
---|
| 365 | + xmlstream = Attribute( |
---|
| 366 | + """ |
---|
| 367 | + The XML Stream. |
---|
| 368 | + """) |
---|
| 369 | + deferred = Attribute( |
---|
| 370 | + """ |
---|
| 371 | + The deferred returned from initialize. |
---|
| 372 | + """) |
---|
| 373 | + |
---|
| 374 | + |
---|
| 375 | + def getFeatures(): |
---|
| 376 | + """ |
---|
| 377 | + Get stream features for this initializer. |
---|
| 378 | + |
---|
| 379 | + @rtype: C{list} of L{twisted.words.xish.domish.Element} |
---|
| 380 | + """ |
---|
| 381 | + |
---|
| 382 | + |
---|
| 383 | + def initialize(): |
---|
| 384 | + """ |
---|
| 385 | + Initialize the initializer. |
---|
| 386 | + |
---|
| 387 | + This is where observers for feature negotiation are set up. When |
---|
| 388 | + the returned deferred fires, it is assumed to have completed. |
---|
| 389 | + |
---|
| 390 | + @rtype: L{twisted.internet.defer.Deferred} |
---|
| 391 | + """ |
---|
[64] | 392 | diff --git a/wokkel/test/test_generic.py b/wokkel/test/test_generic.py |
---|
| 393 | --- a/wokkel/test/test_generic.py |
---|
| 394 | +++ b/wokkel/test/test_generic.py |
---|
[72] | 395 | @@ -7,14 +7,19 @@ |
---|
| 396 | |
---|
| 397 | import re |
---|
[64] | 398 | |
---|
[66] | 399 | +from zope.interface import verify |
---|
| 400 | + |
---|
[64] | 401 | +from twisted.internet import defer |
---|
[72] | 402 | from twisted.python import deprecate |
---|
| 403 | from twisted.python.versions import Version |
---|
[64] | 404 | +from twisted.test import proto_helpers |
---|
| 405 | from twisted.trial import unittest |
---|
[72] | 406 | from twisted.trial.util import suppress as SUPPRESS |
---|
[64] | 407 | from twisted.words.xish import domish |
---|
| 408 | from twisted.words.protocols.jabber.jid import JID |
---|
[65] | 409 | +from twisted.words.protocols.jabber import error, xmlstream |
---|
[64] | 410 | |
---|
[66] | 411 | -from wokkel import generic |
---|
| 412 | +from wokkel import generic, iwokkel |
---|
[64] | 413 | from wokkel.test.helpers import XmlStreamStub |
---|
[66] | 414 | |
---|
| 415 | NS_VERSION = 'jabber:iq:version' |
---|
[79] | 416 | @@ -300,6 +305,334 @@ |
---|
[72] | 417 | |
---|
| 418 | |
---|
| 419 | |
---|
[66] | 420 | +class BaseReceivingInitializerTest(unittest.TestCase): |
---|
| 421 | + """ |
---|
| 422 | + Tests for L{generic.BaseReceivingInitializer}. |
---|
| 423 | + """ |
---|
| 424 | + |
---|
| 425 | + def setUp(self): |
---|
| 426 | + self.init = generic.BaseReceivingInitializer('init', None) |
---|
| 427 | + |
---|
| 428 | + |
---|
| 429 | + def test_interface(self): |
---|
| 430 | + verify.verifyObject(iwokkel.IReceivingInitializer, self.init) |
---|
| 431 | + |
---|
| 432 | + |
---|
| 433 | + def test_getFeatures(self): |
---|
| 434 | + self.assertRaises(NotImplementedError, self.init.getFeatures) |
---|
| 435 | + |
---|
| 436 | + |
---|
| 437 | + def test_initialize(self): |
---|
| 438 | + d = self.init.initialize() |
---|
| 439 | + self.init.deferred.callback(None) |
---|
| 440 | + return d |
---|
| 441 | + |
---|
| 442 | + |
---|
| 443 | + |
---|
| 444 | +class TestableReceivingInitializer(generic.BaseReceivingInitializer): |
---|
[65] | 445 | + """ |
---|
| 446 | + Testable initializer for receiving entities. |
---|
| 447 | + |
---|
| 448 | + This initializer advertises support for a stream feature denoted by |
---|
| 449 | + C{uri} and C{name}. Its C{deferred} should be fired to complete |
---|
| 450 | + initialization for this initializer. |
---|
| 451 | + |
---|
| 452 | + @ivar uri: Namespace of the stream feature. |
---|
[66] | 453 | + @ivar localname: Element localname for the stream feature. |
---|
[65] | 454 | + """ |
---|
| 455 | + required = True |
---|
| 456 | + |
---|
[66] | 457 | + def __init__(self, name, xs, uri, localname): |
---|
| 458 | + generic.BaseReceivingInitializer.__init__(self, name, xs) |
---|
[65] | 459 | + self.uri = uri |
---|
[66] | 460 | + self.localname = localname |
---|
[65] | 461 | + self.deferred = defer.Deferred() |
---|
| 462 | + |
---|
| 463 | + |
---|
| 464 | + def getFeatures(self): |
---|
[66] | 465 | + return [domish.Element((self.uri, self.localname))] |
---|
[65] | 466 | + |
---|
| 467 | + |
---|
| 468 | + |
---|
[64] | 469 | +class FeatureListenAuthenticatorTest(unittest.TestCase): |
---|
| 470 | + """ |
---|
| 471 | + Tests for L{generic.FeatureListenAuthenticator}. |
---|
| 472 | + """ |
---|
| 473 | + |
---|
| 474 | + def setUp(self): |
---|
| 475 | + self.gotAuthenticated = False |
---|
| 476 | + self.initFailure = None |
---|
| 477 | + self.authenticator = generic.FeatureListenAuthenticator() |
---|
[65] | 478 | + self.authenticator.namespace = 'jabber:server' |
---|
[64] | 479 | + self.xmlstream = generic.TestableXmlStream(self.authenticator) |
---|
| 480 | + self.xmlstream.addObserver('//event/stream/authd', |
---|
| 481 | + self.onAuthenticated) |
---|
| 482 | + self.xmlstream.addObserver('//event/xmpp/initfailed', |
---|
| 483 | + self.onInitFailed) |
---|
| 484 | + |
---|
[66] | 485 | + self.init = TestableReceivingInitializer('init', self.xmlstream, |
---|
| 486 | + 'testns', 'test') |
---|
[65] | 487 | + |
---|
| 488 | + def getInitializers(): |
---|
| 489 | + return [self.init] |
---|
| 490 | + |
---|
| 491 | + self.authenticator.getInitializers = getInitializers |
---|
| 492 | + |
---|
[64] | 493 | + |
---|
| 494 | + def onAuthenticated(self, obj): |
---|
| 495 | + self.gotAuthenticated = True |
---|
| 496 | + |
---|
| 497 | + |
---|
| 498 | + def onInitFailed(self, failure): |
---|
| 499 | + self.initFailure = failure |
---|
| 500 | + |
---|
| 501 | + |
---|
[66] | 502 | + def test_getInitializers(self): |
---|
| 503 | + """ |
---|
| 504 | + Unoverridden getInitializers raises NotImplementedError. |
---|
| 505 | + """ |
---|
| 506 | + authenticator = generic.FeatureListenAuthenticator() |
---|
| 507 | + self.assertRaises( |
---|
| 508 | + NotImplementedError, |
---|
| 509 | + authenticator.getInitializers) |
---|
| 510 | + |
---|
| 511 | + |
---|
[64] | 512 | + def test_streamStarted(self): |
---|
| 513 | + """ |
---|
| 514 | + Upon stream start, stream initializers are set up. |
---|
| 515 | + |
---|
| 516 | + The method getInitializers will determine the available stream |
---|
| 517 | + initializers given the current state of stream initialization. |
---|
[65] | 518 | + hen, each of the returned initializers will be called to set |
---|
[64] | 519 | + themselves up. |
---|
| 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 | + self.assertTrue(xs.headerSent) |
---|
| 529 | + |
---|
| 530 | + # Check if features were sent |
---|
| 531 | + features = xs.output[-1] |
---|
| 532 | + self.assertEquals(xmlstream.NS_STREAMS, features.uri) |
---|
| 533 | + self.assertEquals('features', features.name) |
---|
| 534 | + feature = features.elements().next() |
---|
| 535 | + self.assertEqual('testns', feature.uri) |
---|
| 536 | + self.assertEqual('test', feature.name) |
---|
| 537 | + |
---|
| 538 | + self.assertFalse(self.gotAuthenticated) |
---|
| 539 | + |
---|
[65] | 540 | + self.init.deferred.callback(None) |
---|
[64] | 541 | + self.assertTrue(self.gotAuthenticated) |
---|
[65] | 542 | + |
---|
| 543 | + |
---|
[66] | 544 | + def test_streamStartedStreamError(self): |
---|
| 545 | + """ |
---|
| 546 | + A stream error raised by the initializer is sent out. |
---|
| 547 | + """ |
---|
| 548 | + xs = self.xmlstream |
---|
| 549 | + xs.makeConnection(proto_helpers.StringTransport()) |
---|
| 550 | + xs.dataReceived("<stream:stream xmlns='jabber:server' " |
---|
| 551 | + "xmlns:stream='http://etherx.jabber.org/streams' " |
---|
| 552 | + "from='example.com' to='example.org' id='12345' " |
---|
| 553 | + "version='1.0'>") |
---|
| 554 | + |
---|
| 555 | + self.assertTrue(xs.headerSent) |
---|
| 556 | + |
---|
| 557 | + xs.output = [] |
---|
| 558 | + exc = error.StreamError('policy-violation') |
---|
| 559 | + self.init.deferred.errback(exc) |
---|
| 560 | + |
---|
| 561 | + self.xmlstream.assertStreamError(self, exc=exc) |
---|
| 562 | + self.assertFalse(xs.output) |
---|
| 563 | + self.assertFalse(self.gotAuthenticated) |
---|
| 564 | + |
---|
| 565 | + |
---|
| 566 | + def test_streamStartedOtherError(self): |
---|
| 567 | + """ |
---|
| 568 | + Initializer exceptions are logged and yield a internal-server-error. |
---|
| 569 | + """ |
---|
| 570 | + xs = self.xmlstream |
---|
| 571 | + xs.makeConnection(proto_helpers.StringTransport()) |
---|
| 572 | + xs.dataReceived("<stream:stream xmlns='jabber:server' " |
---|
| 573 | + "xmlns:stream='http://etherx.jabber.org/streams' " |
---|
| 574 | + "from='example.com' to='example.org' id='12345' " |
---|
| 575 | + "version='1.0'>") |
---|
| 576 | + |
---|
| 577 | + self.assertTrue(xs.headerSent) |
---|
| 578 | + |
---|
| 579 | + xs.output = [] |
---|
| 580 | + class Error(Exception): |
---|
| 581 | + pass |
---|
| 582 | + self.init.deferred.errback(Error()) |
---|
| 583 | + |
---|
| 584 | + self.xmlstream.assertStreamError(self, condition='internal-server-error') |
---|
| 585 | + self.assertFalse(xs.output) |
---|
| 586 | + self.assertFalse(self.gotAuthenticated) |
---|
| 587 | + self.assertEqual(1, len(self.flushLoggedErrors(Error))) |
---|
| 588 | + |
---|
| 589 | + |
---|
[65] | 590 | + def test_streamStartedInitializerCompleted(self): |
---|
| 591 | + """ |
---|
| 592 | + Succesfully finished initializers are recorded. |
---|
| 593 | + """ |
---|
| 594 | + xs = self.xmlstream |
---|
| 595 | + xs.makeConnection(proto_helpers.StringTransport()) |
---|
| 596 | + xs.dataReceived("<stream:stream xmlns='jabber:server' " |
---|
| 597 | + "xmlns:stream='http://etherx.jabber.org/streams' " |
---|
| 598 | + "from='example.com' to='example.org' id='12345' " |
---|
| 599 | + "version='1.0'>") |
---|
| 600 | + |
---|
[66] | 601 | + xs.output = [] |
---|
[65] | 602 | + self.init.deferred.callback(None) |
---|
[66] | 603 | + self.assertEqual(['init'], self.authenticator.completedInitializers) |
---|
| 604 | + |
---|
| 605 | + |
---|
| 606 | + def test_streamStartedInitializerCompletedFeatures(self): |
---|
| 607 | + """ |
---|
| 608 | + After completing an initializer, stream features are sent again. |
---|
| 609 | + |
---|
| 610 | + In this case, with only one initializer, there are no more features. |
---|
| 611 | + """ |
---|
| 612 | + xs = self.xmlstream |
---|
| 613 | + xs.makeConnection(proto_helpers.StringTransport()) |
---|
| 614 | + xs.dataReceived("<stream:stream xmlns='jabber:server' " |
---|
| 615 | + "xmlns:stream='http://etherx.jabber.org/streams' " |
---|
| 616 | + "from='example.com' to='example.org' id='12345' " |
---|
| 617 | + "version='1.0'>") |
---|
| 618 | + |
---|
| 619 | + xs.output = [] |
---|
| 620 | + self.init.deferred.callback(None) |
---|
| 621 | + |
---|
| 622 | + self.assertEqual(1, len(xs.output)) |
---|
| 623 | + features = xs.output[-1] |
---|
| 624 | + self.assertEqual('features', features.name) |
---|
| 625 | + self.assertEqual(xmlstream.NS_STREAMS, features.uri) |
---|
| 626 | + self.assertFalse(features.children) |
---|
| 627 | + |
---|
| 628 | + |
---|
| 629 | + def test_streamStartedInitializerCompletedReset(self): |
---|
| 630 | + """ |
---|
| 631 | + If an initializer completes with Reset, no features are sent. |
---|
| 632 | + """ |
---|
| 633 | + xs = self.xmlstream |
---|
| 634 | + xs.makeConnection(proto_helpers.StringTransport()) |
---|
| 635 | + xs.dataReceived("<stream:stream xmlns='jabber:server' " |
---|
| 636 | + "xmlns:stream='http://etherx.jabber.org/streams' " |
---|
| 637 | + "from='example.com' to='example.org' id='12345' " |
---|
| 638 | + "version='1.0'>") |
---|
| 639 | + |
---|
| 640 | + xs.output = [] |
---|
| 641 | + self.init.deferred.callback(xmlstream.Reset) |
---|
| 642 | + |
---|
| 643 | + self.assertEqual(0, len(xs.output)) |
---|
[65] | 644 | + |
---|
| 645 | + |
---|
| 646 | + def test_streamStartedXmlStanzasRejected(self): |
---|
| 647 | + """ |
---|
| 648 | + XML Stanzas may not be sent before feature negotiation has completed. |
---|
| 649 | + """ |
---|
| 650 | + xs = self.xmlstream |
---|
| 651 | + xs.makeConnection(proto_helpers.StringTransport()) |
---|
| 652 | + xs.dataReceived("<stream:stream xmlns='jabber:server' " |
---|
| 653 | + "xmlns:stream='http://etherx.jabber.org/streams' " |
---|
| 654 | + "from='example.com' to='example.org' id='12345' " |
---|
| 655 | + "version='1.0'>") |
---|
| 656 | + |
---|
| 657 | + xs.dataReceived("<iq to='example.org' from='example.com' type='set'>" |
---|
| 658 | + " <query xmlns='jabber:iq:version'/>" |
---|
| 659 | + "</iq>") |
---|
| 660 | + |
---|
[66] | 661 | + self.xmlstream.assertStreamError(self, condition='not-authorized') |
---|
[65] | 662 | + |
---|
| 663 | + |
---|
| 664 | + def test_streamStartedCompleteXmlStanzasAllowed(self): |
---|
| 665 | + """ |
---|
| 666 | + XML Stanzas may sent after feature negotiation has completed. |
---|
| 667 | + """ |
---|
| 668 | + xs = self.xmlstream |
---|
| 669 | + xs.makeConnection(proto_helpers.StringTransport()) |
---|
| 670 | + xs.dataReceived("<stream:stream xmlns='jabber:server' " |
---|
| 671 | + "xmlns:stream='http://etherx.jabber.org/streams' " |
---|
| 672 | + "from='example.com' to='example.org' id='12345' " |
---|
| 673 | + "version='1.0'>") |
---|
| 674 | + |
---|
| 675 | + self.init.deferred.callback(None) |
---|
| 676 | + |
---|
| 677 | + xs.dataReceived("<iq to='example.org' from='example.com' type='set'>" |
---|
| 678 | + " <query xmlns='jabber:iq:version'/>" |
---|
| 679 | + "</iq>") |
---|
| 680 | + |
---|
| 681 | + |
---|
| 682 | + def test_streamStartedXmlStanzasHandledIgnored(self): |
---|
| 683 | + """ |
---|
| 684 | + XML Stanzas that have already been handled are ignored. |
---|
| 685 | + """ |
---|
| 686 | + xs = self.xmlstream |
---|
| 687 | + xs.makeConnection(proto_helpers.StringTransport()) |
---|
| 688 | + xs.dataReceived("<stream:stream xmlns='jabber:server' " |
---|
| 689 | + "xmlns:stream='http://etherx.jabber.org/streams' " |
---|
| 690 | + "from='example.com' to='example.org' id='12345' " |
---|
| 691 | + "version='1.0'>") |
---|
| 692 | + |
---|
| 693 | + iq = generic.parseXml("<iq to='example.org' from='example.com' type='set'>" |
---|
| 694 | + " <query xmlns='jabber:iq:version'/>" |
---|
| 695 | + "</iq>") |
---|
| 696 | + iq.handled = True |
---|
| 697 | + xs.dispatch(iq) |
---|
| 698 | + |
---|
| 699 | + |
---|
| 700 | + def test_streamStartedNonXmlStanzasIgnored(self): |
---|
| 701 | + """ |
---|
| 702 | + Elements that are not XML Stranzas are not rejected. |
---|
| 703 | + """ |
---|
| 704 | + xs = self.xmlstream |
---|
| 705 | + xs.makeConnection(proto_helpers.StringTransport()) |
---|
| 706 | + xs.dataReceived("<stream:stream xmlns='jabber:server' " |
---|
| 707 | + "xmlns:stream='http://etherx.jabber.org/streams' " |
---|
| 708 | + "from='example.com' to='example.org' id='12345' " |
---|
| 709 | + "version='1.0'>") |
---|
| 710 | + |
---|
| 711 | + xs.dataReceived("<test xmlns='myns'/>") |
---|
| 712 | + |
---|
| 713 | + |
---|
| 714 | + def test_streamStartedCheckStream(self): |
---|
| 715 | + """ |
---|
| 716 | + Stream errors raised by checkStream are sent out. |
---|
| 717 | + """ |
---|
| 718 | + def checkStream(): |
---|
| 719 | + raise error.StreamError('undefined-condition') |
---|
| 720 | + |
---|
| 721 | + self.authenticator.checkStream = checkStream |
---|
| 722 | + xs = self.xmlstream |
---|
| 723 | + xs.makeConnection(proto_helpers.StringTransport()) |
---|
| 724 | + xs.dataReceived("<stream:stream xmlns='jabber:server' " |
---|
| 725 | + "xmlns:stream='http://etherx.jabber.org/streams' " |
---|
| 726 | + "from='example.com' to='example.org' id='12345' " |
---|
| 727 | + "version='1.0'>") |
---|
| 728 | + |
---|
[66] | 729 | + self.xmlstream.assertStreamError(self, condition='undefined-condition') |
---|
[65] | 730 | + self.assertFalse(xs.headerSent) |
---|
| 731 | + |
---|
| 732 | + |
---|
| 733 | + def test_checkStreamNamespace(self): |
---|
| 734 | + """ |
---|
| 735 | + The stream namespace must match the pre-defined stream namespace. |
---|
| 736 | + """ |
---|
| 737 | + xs = self.xmlstream |
---|
| 738 | + xs.makeConnection(proto_helpers.StringTransport()) |
---|
| 739 | + xs.dataReceived("<stream:stream xmlns='jabber:client' " |
---|
| 740 | + "xmlns:stream='http://etherx.jabber.org/streams' " |
---|
| 741 | + "from='example.com' to='example.org' id='12345' " |
---|
| 742 | + "version='1.0'>") |
---|
| 743 | + |
---|
[66] | 744 | + self.xmlstream.assertStreamError(self, condition='invalid-namespace') |
---|
[72] | 745 | + |
---|
| 746 | + |
---|
| 747 | + |
---|
| 748 | class PrepareIDNNameTests(unittest.TestCase): |
---|
| 749 | """ |
---|
| 750 | Tests for L{wokkel.generic.prepareIDNName}. |
---|