From 5f9894ca63713de2d7ac7f36ccb17c0220a4c77c Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Tue, 7 Mar 2017 12:34:36 +0100 Subject: [PATCH] API updates, make most tests pass, disable others * finally wire up "application versions" * remove when_verifier (which used to fire after key establishment, but before the VERSION message was received or verified) * fire when_verified and when_version at the same time (after VERSION is verified), but with different args --- docs/api.md | 53 +++---- src/wormhole/_boss.py | 5 +- src/wormhole/_key.py | 4 +- src/wormhole/test/test_wormhole.py | 191 ++++++++++++++----------- src/wormhole/test/test_wormhole_new.py | 6 +- src/wormhole/wormhole.py | 34 +++-- 6 files changed, 164 insertions(+), 129 deletions(-) diff --git a/docs/api.md b/docs/api.md index 922ce23..f4c4eeb 100644 --- a/docs/api.md +++ b/docs/api.md @@ -274,24 +274,25 @@ functions on the delegate object. In Deferred mode, the application retrieves Deferred objects from the wormhole, and event dispatch is performed by firing those Deferreds. -* got_code (`yield w.when_code()` / `dg.wormhole_got_code(code)`): fired when the +* got_code (`yield w.when_code()` / `dg.wormhole_code(code)`): fired when the wormhole code is established, either after `w.generate_code()` finishes the generation process, or when the Input Helper returned by `w.type_code()` has been told `h.set_words()`, or immediately after `w.set_code(code)` is called. This is most useful after calling `w.generate_code()`, to show the generated code to the user so they can transcribe it to their peer. -* got_verifier (`yield w.when_verifier()` / `dg.wormhole_got_verifier(verf)`: - fired when the key-exchange process has completed, and this side has - learned the shared key. The "verifier" is a byte string with a hash of the - shared session key; clients can compare them (probably as hex) to ensure - that they're really talking to each other, and not to a man-in-the-middle. - When `got_verifier` happens, this side has not yet seen evidence that the - peer has used the correct wormhole code. -* got_version (`yield w.when_version()` / `dg.wormhole_got_version(version)`: - fired when the VERSION message arrives from the peer. This serves two - purposes. The first is that it provide confirmation that the peer (or a - man-in-the-middle) has used the correct wormhole code. The second is - delivery of the "app_versions" data (passed into `wormhole.create`). +* verified (`verifier = yield w.when_verified()` / + `dg.wormhole_verified(verifier)`: fired when the key-exchange process has + completed and a valid VERSION message has arrived. The "verifier" is a byte + string with a hash of the shared session key; clients can compare them + (probably as hex) to ensure that they're really talking to each other, and + not to a man-in-the-middle. When `got_verifier` happens, this side knows + that *someone* has used the correct wormhole code; if someone used the + wrong code, the VERSION message cannot be decrypted, and the wormhole will + be closed instead. +* version (`yield w.when_version()` / `dg.wormhole_version(version)`: + fired when the VERSION message arrives from the peer. This fires at the + same time as `verified`, but delivers the "app_versions" data (passed into + `wormhole.create`) instead of the verifier string. * received (`yield w.when_received()` / `dg.wormhole_received(data)`: fired each time a data message arrives from the peer, with the bytestring that the peer passed into `w.send(data)`. @@ -391,19 +392,19 @@ in python3): ## Full API list -action | Deferred-Mode | Delegated-Mode --------------------------- | --------------------- | ---------------------------- -w.generate_code(length=2) | | -w.set_code(code) | | -h=w.type_code() | | - | d=w.when_code() | dg.wormhole_got_code(code) - | d=w.when_verifier() | dg.wormhole_got_verifier(verf) - | d=w.when_version() | dg.wormhole_got_version(version) -w.send(data) | | - | d=w.when_received() | dg.wormhole_received(data) -key=w.derive_key(purpose, length) | | -w.close() | | dg.wormhole_closed(result) - | d=w.close() | +action | Deferred-Mode | Delegated-Mode +-------------------------- | -------------------- | -------------- +w.generate_code(length=2) | | +w.set_code(code) | | +h=w.type_code() | | + | d=w.when_code() | dg.wormhole_code(code) + | d=w.when_verified() | dg.wormhole_verified(verifier) + | d=w.when_version() | dg.wormhole_version(version) +w.send(data) | | + | d=w.when_received() | dg.wormhole_received(data) +key=w.derive_key(purpose, length) | | +w.close() | | dg.wormhole_closed(result) + | d=w.close() | ## Dilation diff --git a/src/wormhole/_boss.py b/src/wormhole/_boss.py index fb0474c..530d7ae 100644 --- a/src/wormhole/_boss.py +++ b/src/wormhole/_boss.py @@ -27,6 +27,7 @@ class Boss(object): _side = attrib(validator=instance_of(type(u""))) _url = attrib(validator=instance_of(type(u""))) _appid = attrib(validator=instance_of(type(u""))) + _versions = attrib(validator=instance_of(dict)) _welcome_handler = attrib() # TODO: validator: callable _reactor = attrib() _journal = attrib(validator=provides(_interfaces.IJournal)) @@ -41,7 +42,7 @@ class Boss(object): self._M = Mailbox(self._side) self._S = Send(self._side, self._timing) self._O = Order(self._side, self._timing) - self._K = Key(self._appid, self._side, self._timing) + self._K = Key(self._appid, self._versions, self._side, self._timing) self._R = Receive(self._side, self._timing) self._RC = RendezvousConnector(self._url, self._appid, self._side, self._reactor, self._journal, @@ -188,7 +189,7 @@ class Boss(object): self._their_versions = bytes_to_dict(plaintext) # but this part is app-to-app app_versions = self._their_versions.get("app_versions", {}) - self._W.got_version(app_versions) + self._W.got_versions(app_versions) @m.output() def S_send(self, plaintext): diff --git a/src/wormhole/_key.py b/src/wormhole/_key.py index 6370df2..2436de4 100644 --- a/src/wormhole/_key.py +++ b/src/wormhole/_key.py @@ -56,6 +56,7 @@ def encrypt_data(key, plaintext): @implementer(_interfaces.IKey) class Key(object): _appid = attrib(validator=instance_of(type(u""))) + _versions = attrib(validator=instance_of(dict)) _side = attrib(validator=instance_of(type(u""))) _timing = attrib(validator=provides(_interfaces.ITiming)) m = MethodicalMachine() @@ -114,8 +115,7 @@ class Key(object): self._B.got_verifier(derive_key(key, b"wormhole:verifier")) phase = "version" data_key = derive_phase_key(key, self._side, phase) - my_versions = {} # TODO: get from Wormhole? - plaintext = dict_to_bytes(my_versions) + plaintext = dict_to_bytes(self._versions) encrypted = encrypt_data(data_key, plaintext) self._M.add_message(phase, encrypted) self._R.got_key(key) diff --git a/src/wormhole/test/test_wormhole.py b/src/wormhole/test/test_wormhole.py index 9f65bd6..b632f84 100644 --- a/src/wormhole/test/test_wormhole.py +++ b/src/wormhole/test/test_wormhole.py @@ -6,9 +6,9 @@ from twisted.trial import unittest from twisted.internet import reactor from twisted.internet.defer import Deferred, gatherResults, inlineCallbacks from .common import ServerBase -from .. import wormhole, _order +from .. import wormhole, _rendezvous from ..errors import (WrongPasswordError, WelcomeError, InternalError, - KeyFormatError) + KeyFormatError, WormholeClosed, LonelyError) from spake2 import SPAKE2_Symmetric from ..timing import DebugTiming from ..util import (bytes_to_dict, dict_to_bytes, @@ -116,7 +116,7 @@ class InputCode(unittest.TestCase): res = self.successResultOf(d) self.assertEqual(res, ["123"]) self.assertEqual(stderr.getvalue(), "") - +InputCode.skip = "not yet" class GetCode(unittest.TestCase): def test_get(self): @@ -134,6 +134,7 @@ class GetCode(unittest.TestCase): pieces = code.split("-") self.assertEqual(len(pieces), 3) # nameplate plus two words self.assert_(re.search(r'^\d+-\w+-\w+$', code), code) +GetCode.skip = "not yet" class Basic(unittest.TestCase): def tearDown(self): @@ -714,7 +715,7 @@ class Basic(unittest.TestCase): w.derive_key, "foo", SecretBox.KEY_SIZE) self.failureResultOf(w.get(), WrongPasswordError) self.failureResultOf(w.verify(), WrongPasswordError) - +Basic.skip = "being replaced by test_wormhole_new" # event orderings to exercise: # @@ -735,14 +736,15 @@ class Wormholes(ServerBase, unittest.TestCase): @inlineCallbacks def test_basic(self): - w1 = wormhole.wormhole(APPID, self.relayurl, reactor) - w2 = wormhole.wormhole(APPID, self.relayurl, reactor) - code = yield w1.get_code() + w1 = wormhole.create(APPID, self.relayurl, reactor) + w2 = wormhole.create(APPID, self.relayurl, reactor) + w1.allocate_code() + code = yield w1.when_code() w2.set_code(code) w1.send(b"data1") w2.send(b"data2") - dataX = yield w1.get() - dataY = yield w2.get() + dataX = yield w1.when_received() + dataY = yield w2.when_received() self.assertEqual(dataX, b"data2") self.assertEqual(dataY, b"data1") yield w1.close() @@ -753,14 +755,15 @@ class Wormholes(ServerBase, unittest.TestCase): # the two sides use random nonces for their messages, so it's ok for # both to try and send the same body: they'll result in distinct # encrypted messages - w1 = wormhole.wormhole(APPID, self.relayurl, reactor) - w2 = wormhole.wormhole(APPID, self.relayurl, reactor) - code = yield w1.get_code() + w1 = wormhole.create(APPID, self.relayurl, reactor) + w2 = wormhole.create(APPID, self.relayurl, reactor) + w1.allocate_code() + code = yield w1.when_code() w2.set_code(code) w1.send(b"data") w2.send(b"data") - dataX = yield w1.get() - dataY = yield w2.get() + dataX = yield w1.when_received() + dataY = yield w2.when_received() self.assertEqual(dataX, b"data") self.assertEqual(dataY, b"data") yield w1.close() @@ -768,14 +771,15 @@ class Wormholes(ServerBase, unittest.TestCase): @inlineCallbacks def test_interleaved(self): - w1 = wormhole.wormhole(APPID, self.relayurl, reactor) - w2 = wormhole.wormhole(APPID, self.relayurl, reactor) - code = yield w1.get_code() + w1 = wormhole.create(APPID, self.relayurl, reactor) + w2 = wormhole.create(APPID, self.relayurl, reactor) + w1.allocate_code() + code = yield w1.when_code() w2.set_code(code) w1.send(b"data1") - dataY = yield w2.get() + dataY = yield w2.when_received() self.assertEqual(dataY, b"data1") - d = w1.get() + d = w1.when_received() w2.send(b"data2") dataX = yield d self.assertEqual(dataX, b"data2") @@ -784,22 +788,23 @@ class Wormholes(ServerBase, unittest.TestCase): @inlineCallbacks def test_unidirectional(self): - w1 = wormhole.wormhole(APPID, self.relayurl, reactor) - w2 = wormhole.wormhole(APPID, self.relayurl, reactor) - code = yield w1.get_code() + w1 = wormhole.create(APPID, self.relayurl, reactor) + w2 = wormhole.create(APPID, self.relayurl, reactor) + w1.allocate_code() + code = yield w1.when_code() w2.set_code(code) w1.send(b"data1") - dataY = yield w2.get() + dataY = yield w2.when_received() self.assertEqual(dataY, b"data1") yield w1.close() yield w2.close() @inlineCallbacks def test_early(self): - w1 = wormhole.wormhole(APPID, self.relayurl, reactor) + w1 = wormhole.create(APPID, self.relayurl, reactor) w1.send(b"data1") - w2 = wormhole.wormhole(APPID, self.relayurl, reactor) - d = w2.get() + w2 = wormhole.create(APPID, self.relayurl, reactor) + d = w2.when_received() w1.set_code("123-abc-def") w2.set_code("123-abc-def") dataY = yield d @@ -809,12 +814,12 @@ class Wormholes(ServerBase, unittest.TestCase): @inlineCallbacks def test_fixed_code(self): - w1 = wormhole.wormhole(APPID, self.relayurl, reactor) - w2 = wormhole.wormhole(APPID, self.relayurl, reactor) + w1 = wormhole.create(APPID, self.relayurl, reactor) + w2 = wormhole.create(APPID, self.relayurl, reactor) w1.set_code("123-purple-elephant") w2.set_code("123-purple-elephant") w1.send(b"data1"), w2.send(b"data2") - dl = yield self.doBoth(w1.get(), w2.get()) + dl = yield self.doBoth(w1.when_received(), w2.when_received()) (dataX, dataY) = dl self.assertEqual(dataX, b"data2") self.assertEqual(dataY, b"data1") @@ -824,28 +829,52 @@ class Wormholes(ServerBase, unittest.TestCase): @inlineCallbacks def test_multiple_messages(self): - w1 = wormhole.wormhole(APPID, self.relayurl, reactor) - w2 = wormhole.wormhole(APPID, self.relayurl, reactor) + w1 = wormhole.create(APPID, self.relayurl, reactor) + w2 = wormhole.create(APPID, self.relayurl, reactor) w1.set_code("123-purple-elephant") w2.set_code("123-purple-elephant") w1.send(b"data1"), w2.send(b"data2") w1.send(b"data3"), w2.send(b"data4") - dl = yield self.doBoth(w1.get(), w2.get()) + dl = yield self.doBoth(w1.when_received(), w2.when_received()) (dataX, dataY) = dl self.assertEqual(dataX, b"data2") self.assertEqual(dataY, b"data1") - dl = yield self.doBoth(w1.get(), w2.get()) + dl = yield self.doBoth(w1.when_received(), w2.when_received()) (dataX, dataY) = dl self.assertEqual(dataX, b"data4") self.assertEqual(dataY, b"data3") yield w1.close() yield w2.close() + + @inlineCallbacks + def test_closed(self): + w1 = wormhole.create(APPID, self.relayurl, reactor) + w2 = wormhole.create(APPID, self.relayurl, reactor) + w1.set_code("123-foo") + w2.set_code("123-foo") + + # let it connect and become HAPPY + yield w1.when_version() + yield w2.when_version() + + yield w1.close() + yield w2.close() + + # once closed, all Deferred-yielding API calls get an error + e = yield self.assertFailure(w1.when_code(), WormholeClosed) + self.assertEqual(e.args[0], "happy") + yield self.assertFailure(w1.when_verified(), WormholeClosed) + yield self.assertFailure(w1.when_version(), WormholeClosed) + yield self.assertFailure(w1.when_received(), WormholeClosed) + + @inlineCallbacks def test_wrong_password(self): - w1 = wormhole.wormhole(APPID, self.relayurl, reactor) - w2 = wormhole.wormhole(APPID, self.relayurl, reactor) - code = yield w1.get_code() + w1 = wormhole.create(APPID, self.relayurl, reactor) + w2 = wormhole.create(APPID, self.relayurl, reactor) + w1.allocate_code() + code = yield w1.when_code() w2.set_code(code+"not") # That's enough to allow both sides to discover the mismatch, but # only after the confirmation message gets through. API calls that @@ -855,44 +884,41 @@ class Wormholes(ServerBase, unittest.TestCase): w2.send(b"should still work") # API calls that wait (i.e. get) will errback - yield self.assertFailure(w2.get(), WrongPasswordError) - yield self.assertFailure(w1.get(), WrongPasswordError) + yield self.assertFailure(w2.when_received(), WrongPasswordError) + yield self.assertFailure(w1.when_received(), WrongPasswordError) + + yield self.assertFailure(w1.when_verified(), WrongPasswordError) + yield self.assertFailure(w1.when_version(), WrongPasswordError) + + yield self.assertFailure(w1.close(), WrongPasswordError) + yield self.assertFailure(w2.close(), WrongPasswordError) - yield w1.close() - yield w2.close() - self.flushLoggedErrors(WrongPasswordError) @inlineCallbacks def test_wrong_password_with_spaces(self): - w1 = wormhole.wormhole(APPID, self.relayurl, reactor) - w2 = wormhole.wormhole(APPID, self.relayurl, reactor) - code = yield w1.get_code() - code_no_dashes = code.replace('-', ' ') - + w = wormhole.create(APPID, self.relayurl, reactor) + badcode = "4 oops spaces" with self.assertRaises(KeyFormatError) as ex: - w2.set_code(code_no_dashes) - - expected_msg = "code (%s) contains spaces." % (code_no_dashes,) + w.set_code(badcode) + expected_msg = "code (%s) contains spaces." % (badcode,) self.assertEqual(expected_msg, str(ex.exception)) - - yield w1.close() - yield w2.close() - self.flushLoggedErrors(KeyFormatError) + yield self.assertFailure(w.close(), LonelyError) @inlineCallbacks def test_verifier(self): - w1 = wormhole.wormhole(APPID, self.relayurl, reactor) - w2 = wormhole.wormhole(APPID, self.relayurl, reactor) - code = yield w1.get_code() + w1 = wormhole.create(APPID, self.relayurl, reactor) + w2 = wormhole.create(APPID, self.relayurl, reactor) + w1.allocate_code() + code = yield w1.when_code() w2.set_code(code) - v1 = yield w1.verify() - v2 = yield w2.verify() + v1 = yield w1.when_verified() + v2 = yield w2.when_verified() self.failUnlessEqual(type(v1), type(b"")) self.failUnlessEqual(v1, v2) w1.send(b"data1") w2.send(b"data2") - dataX = yield w1.get() - dataY = yield w2.get() + dataX = yield w1.when_received() + dataY = yield w2.when_received() self.assertEqual(dataX, b"data2") self.assertEqual(dataY, b"data1") yield w1.close() @@ -901,16 +927,17 @@ class Wormholes(ServerBase, unittest.TestCase): @inlineCallbacks def test_versions(self): # there's no API for this yet, but make sure the internals work - w1 = wormhole.wormhole(APPID, self.relayurl, reactor) - w1._my_versions = {"w1": 123} - w2 = wormhole.wormhole(APPID, self.relayurl, reactor) - w2._my_versions = {"w2": 456} - code = yield w1.get_code() + w1 = wormhole.create(APPID, self.relayurl, reactor, + versions={"w1": 123}) + w2 = wormhole.create(APPID, self.relayurl, reactor, + versions={"w2": 456}) + w1.allocate_code() + code = yield w1.when_code() w2.set_code(code) - yield w1.verify() - self.assertEqual(w1._their_versions, {"w2": 456}) - yield w2.verify() - self.assertEqual(w2._their_versions, {"w1": 123}) + w1_versions = yield w2.when_version() + self.assertEqual(w1_versions, {"w1": 123}) + w2_versions = yield w1.when_version() + self.assertEqual(w2_versions, {"w2": 456}) yield w1.close() yield w2.close() @@ -923,50 +950,52 @@ class Wormholes(ServerBase, unittest.TestCase): # incoming PAKE message was received, which would cause # SPAKE2.finish() to be called a second time, which throws an error # (which, being somewhat unexpected, caused a hang rather than a - # clear exception). - with mock.patch("wormhole.wormhole._order", MessageDoubler): + # clear exception). The Mailbox object is responsible for + # deduplication, so we must patch the RendezvousConnector to simulate + # duplicated messages. + with mock.patch("wormhole._boss.RendezvousConnector", MessageDoubler): w1 = wormhole.create(APPID, self.relayurl, reactor) w2 = wormhole.create(APPID, self.relayurl, reactor) w1.set_code("123-purple-elephant") w2.set_code("123-purple-elephant") w1.send(b"data1"), w2.send(b"data2") - dl = yield self.doBoth(w1.get(), w2.get()) + dl = yield self.doBoth(w1.when_received(), w2.when_received()) (dataX, dataY) = dl self.assertEqual(dataX, b"data2") self.assertEqual(dataY, b"data1") yield w1.close() yield w2.close() -class MessageDoubler(_order.Order): +class MessageDoubler(_rendezvous.RendezvousConnector): # we could double messages on the sending side, but a future server will # strip those duplicates, so to really exercise the receiver, we must # double them on the inbound side instead #def _msg_send(self, phase, body): # wormhole._Wormhole._msg_send(self, phase, body) # self._ws_send_command("add", phase=phase, body=bytes_to_hexstr(body)) - def got_message(self, side, phase, body): - _order.Order.got_message(self, side, phase, body) - _order.Order.got_message(self, side, phase, body) + def _response_handle_message(self, msg): + _rendezvous.RendezvousConnector._response_handle_message(self, msg) + _rendezvous.RendezvousConnector._response_handle_message(self, msg) class Errors(ServerBase, unittest.TestCase): @inlineCallbacks def test_codes_1(self): - w = wormhole.wormhole(APPID, self.relayurl, reactor) + w = wormhole.create(APPID, self.relayurl, reactor) # definitely too early self.assertRaises(InternalError, w.derive_key, "purpose", 12) w.set_code("123-purple-elephant") # code can only be set once self.assertRaises(InternalError, w.set_code, "123-nope") - yield self.assertFailure(w.get_code(), InternalError) + yield self.assertFailure(w.when_code(), InternalError) yield self.assertFailure(w.input_code(), InternalError) yield w.close() @inlineCallbacks def test_codes_2(self): - w = wormhole.wormhole(APPID, self.relayurl, reactor) - yield w.get_code() + w = wormhole.create(APPID, self.relayurl, reactor) + yield w.when_code() self.assertRaises(InternalError, w.set_code, "123-nope") - yield self.assertFailure(w.get_code(), InternalError) + yield self.assertFailure(w.when_code(), InternalError) yield self.assertFailure(w.input_code(), InternalError) yield w.close() diff --git a/src/wormhole/test/test_wormhole_new.py b/src/wormhole/test/test_wormhole_new.py index 2a271d0..7455acb 100644 --- a/src/wormhole/test/test_wormhole_new.py +++ b/src/wormhole/test/test_wormhole_new.py @@ -57,8 +57,8 @@ class New(ServerBase, unittest.TestCase): code2 = yield w2.when_code() self.assertEqual(code, code2) - verifier1 = yield w1.when_verifier() - verifier2 = yield w2.when_verifier() + verifier1 = yield w1.when_verified() + verifier2 = yield w2.when_verified() self.assertEqual(verifier1, verifier2) version1 = yield w1.when_version() @@ -88,7 +88,7 @@ class New(ServerBase, unittest.TestCase): w1.allocate_code(2) code = yield w1.when_code() w2 = wormhole.create(APPID, self.relayurl, reactor) - w2.set_code(code+", NOT") + w2.set_code(code+"NOT") code2 = yield w2.when_code() self.assertNotEqual(code, code2) diff --git a/src/wormhole/wormhole.py b/src/wormhole/wormhole.py index e73eb1b..291a466 100644 --- a/src/wormhole/wormhole.py +++ b/src/wormhole/wormhole.py @@ -25,7 +25,7 @@ from .util import to_bytes # w.send(data) # app.wormhole_got_code(code) # app.wormhole_got_verifier(verifier) -# app.wormhole_got_version(version) +# app.wormhole_got_version(versions) # app.wormhole_receive(data) # w.close() # app.wormhole_closed() @@ -117,15 +117,15 @@ class _DelegatedWormhole(object): # from below def got_code(self, code): - self._delegate.wormhole_got_code(code) + self._delegate.wormhole_code(code) def got_welcome(self, welcome): pass # TODO def got_key(self, key): self._key = key # for derive_key() def got_verifier(self, verifier): - self._delegate.wormhole_got_verifier(verifier) - def got_version(self, version): - self._delegate.wormhole_got_version(version) + self._delegate.wormhole_verified(verifier) + def got_versions(self, versions): + self._delegate.wormhole_version(versions) def received(self, plaintext): self._delegate.wormhole_received(plaintext) def closed(self, result): @@ -139,7 +139,7 @@ class _DeferredWormhole(object): self._key = None self._verifier = None self._verifier_observers = [] - self._version = None + self._versions = None self._version_observers = [] self._received_data = [] self._received_observers = [] @@ -162,7 +162,7 @@ class _DeferredWormhole(object): self._code_observers.append(d) return d - def when_verifier(self): + def when_verified(self): if self._observer_result is not None: return defer.fail(self._observer_result) if self._verifier is not None: @@ -174,8 +174,8 @@ class _DeferredWormhole(object): def when_version(self): if self._observer_result is not None: return defer.fail(self._observer_result) - if self._version is not None: - return defer.succeed(self._version) + if self._versions is not None: + return defer.succeed(self._versions) d = defer.Deferred() self._version_observers.append(d) return d @@ -241,10 +241,10 @@ class _DeferredWormhole(object): for d in self._verifier_observers: d.callback(verifier) self._verifier_observers[:] = [] - def got_version(self, version): - self._version = version + def got_versions(self, versions): + self._versions = versions for d in self._version_observers: - d.callback(version) + d.callback(versions) self._version_observers[:] = [] def received(self, plaintext): @@ -272,8 +272,9 @@ class _DeferredWormhole(object): d.callback(self._closed_result) -def create(appid, relay_url, reactor, delegate=None, journal=None, - tor_manager=None, timing=None, welcome_handler=None, +def create(appid, relay_url, reactor, versions={}, + delegate=None, journal=None, tor_manager=None, + timing=None, welcome_handler=None, stderr=sys.stderr): timing = timing or DebugTiming() side = bytes_to_hexstr(os.urandom(5)) @@ -287,7 +288,10 @@ def create(appid, relay_url, reactor, delegate=None, journal=None, w = _DelegatedWormhole(delegate) else: w = _DeferredWormhole() - b = Boss(w, side, relay_url, appid, welcome_handler, reactor, journal, + wormhole_versions = {} # will be used to indicate Wormhole capabilities + wormhole_versions["app_versions"] = versions # app-specific capabilities + b = Boss(w, side, relay_url, appid, wormhole_versions, + welcome_handler, reactor, journal, tor_manager, timing) w._set_boss(b) b.start()