From 7955a36bfd3e6d2ce4e2c6a2e016cb1f0bd5fc67 Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Fri, 12 May 2017 13:12:36 -0700 Subject: [PATCH] switch to new API This renames all the existing API methods, to use a consistent "d=get_XYZ()" (for Deferred mode) or "dg.wormhole_got_XYZ()" (for Delegated mode). It updates cmd_send/cmd_receive/cmd_ssh to use the new API. Since we now have get_welcome(), apps handle the Welcome message with a Deferred callback instead of registering a "welcome handler". This lets us make sure we've finished printing any server message-of-the-day or "you should update your client" message to stdout before using stdio to ask for the wormhole code. (Previously, the code-input prompt was overwritten by the server message, and it was ugly). refs #145. This approach adds an extra roundtrip to the receiver, but we can fix that (see #145 for details). Because of that change, the server-is-being-slow message is printed at a slightly different time, so those tests needed some extra work to exercise it properly. --- src/wormhole/_boss.py | 7 +- src/wormhole/cli/cmd_receive.py | 21 +-- src/wormhole/cli/cmd_send.py | 43 ++--- src/wormhole/cli/welcome.py | 36 ++-- src/wormhole/test/test_cli.py | 53 +++--- src/wormhole/test/test_machines.py | 21 ++- src/wormhole/test/test_wormhole.py | 256 ++++++++++++++--------------- src/wormhole/wormhole.py | 83 +++++----- src/wormhole/xfer_util.py | 13 +- 9 files changed, 264 insertions(+), 269 deletions(-) diff --git a/src/wormhole/_boss.py b/src/wormhole/_boss.py index fec09be..257bd8d 100644 --- a/src/wormhole/_boss.py +++ b/src/wormhole/_boss.py @@ -33,7 +33,6 @@ class Boss(object): _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)) _tor_manager = attrib() # TODO: ITorManager or None @@ -185,11 +184,11 @@ class Boss(object): raise WelcomeError(welcome["error"]) # TODO: it'd be nice to not call the handler when we're in # S3_closing or S4_closed states. I tried to implement this with - # rx_Welcome as an @input, but in the error case I'd be + # rx_welcome as an @input, but in the error case I'd be # delivering a new input (rx_error or something) while in the # middle of processing the rx_welcome input, and I wasn't sure # Automat would handle that correctly. - self._welcome_handler(welcome) # can raise WelcomeError too + self._W.got_welcome(welcome) # TODO: let this raise WelcomeError? except WelcomeError as welcome_error: self.rx_unwelcome(welcome_error) @m.input() @@ -244,7 +243,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/cli/cmd_receive.py b/src/wormhole/cli/cmd_receive.py index 1eece6b..7608c04 100644 --- a/src/wormhole/cli/cmd_receive.py +++ b/src/wormhole/cli/cmd_receive.py @@ -10,7 +10,7 @@ from ..transit import TransitReceiver from ..errors import TransferError, WormholeClosedError, NoTorError from ..util import (dict_to_bytes, bytes_to_dict, bytes_to_hexstr, estimate_free_space) -from .welcome import CLIWelcomeHandler +from .welcome import handle_welcome APPID = u"lothar.com/wormhole/text-or-file-xfer" @@ -68,13 +68,10 @@ class Receiver: # with the user handing off the wormhole code yield self._tor_manager.start() - wh = CLIWelcomeHandler(self.args.relay_url, __version__, - self.args.stderr) w = create(self.args.appid or APPID, self.args.relay_url, self._reactor, tor_manager=self._tor_manager, - timing=self.args.timing, - welcome_handler=wh.handle_welcome) + timing=self.args.timing) self._w = w # so tests can wait on events too # I wanted to do this instead: @@ -112,6 +109,10 @@ class Receiver: @inlineCallbacks def _go(self, w): + welcome = yield w.get_welcome() + handle_welcome(welcome, self.args.relay_url, __version__, + self.args.stderr) + yield self._handle_code(w) def on_slow_key(): @@ -125,7 +126,7 @@ class Receiver: # of that possibility, it's probably not appropriate to give up # automatically after some timeout. The user can express their # impatience by quitting the program with control-C. - yield w.when_key() + yield w.get_unverified_key() finally: if not notify.called: notify.cancel() @@ -147,7 +148,7 @@ class Receiver: # It would be reasonable to give up after waiting here for too # long. - verifier_bytes = yield w.when_verified() + verifier_bytes = yield w.get_verifier() finally: if not notify.called: notify.cancel() @@ -183,12 +184,12 @@ class Receiver: def _send_data(self, data, w): data_bytes = dict_to_bytes(data) - w.send(data_bytes) + w.send_message(data_bytes) @inlineCallbacks def _get_data(self, w): # this may raise WrongPasswordError - them_bytes = yield w.when_received() + them_bytes = yield w.get_message() them_d = bytes_to_dict(them_bytes) if "error" in them_d: raise TransferError(them_d["error"]) @@ -210,7 +211,7 @@ class Receiver: if not used_completion: print(" (note: you can use to complete words)", file=self.args.stderr) - yield w.when_code() + yield w.get_code() def _show_verifier(self, verifier_bytes): verifier_hex = bytes_to_hexstr(verifier_bytes) diff --git a/src/wormhole/cli/cmd_send.py b/src/wormhole/cli/cmd_send.py index a923bd5..307494b 100644 --- a/src/wormhole/cli/cmd_send.py +++ b/src/wormhole/cli/cmd_send.py @@ -10,7 +10,7 @@ from ..errors import TransferError, WormholeClosedError, NoTorError from wormhole import create, __version__ from ..transit import TransitSender from ..util import dict_to_bytes, bytes_to_dict, bytes_to_hexstr -from .welcome import CLIWelcomeHandler +from .welcome import handle_welcome APPID = u"lothar.com/wormhole/text-or-file-xfer" VERIFY_TIMER = 1 @@ -53,13 +53,10 @@ class Sender: # with the user handing off the wormhole code yield self._tor_manager.start() - wh = CLIWelcomeHandler(self._args.relay_url, __version__, - self._args.stderr) w = create(self._args.appid or APPID, self._args.relay_url, self._reactor, tor_manager=self._tor_manager, - timing=self._timing, - welcome_handler=wh.handle_welcome) + timing=self._timing) d = self._go(w) # if we succeed, we should close and return the w.close results @@ -85,10 +82,14 @@ class Sender: def _send_data(self, data, w): data_bytes = dict_to_bytes(data) - w.send(data_bytes) + w.send_message(data_bytes) @inlineCallbacks def _go(self, w): + welcome = yield w.get_welcome() + handle_welcome(welcome, self._args.relay_url, __version__, + self._args.stderr) + # TODO: run the blocking zip-the-directory IO in a thread, let the # wormhole exchange happen in parallel offer, self._fd_to_send = self._build_offer() @@ -110,21 +111,21 @@ class Sender: else: w.allocate_code(args.code_length) - code = yield w.when_code() + code = yield w.get_code() if not args.zeromode: print(u"Wormhole code is: %s" % code, file=args.stderr) # flush stderr so the code is displayed immediately args.stderr.flush() print(u"", file=args.stderr) - # We don't print a "waiting" message for when_key() here, even though - # we do that in cmd_receive.py, because it's not at all surprising to - # we waiting here for a long time. We'll sit in when_key() until the - # receiver has typed in the code and their PAKE message makes it to - # us. - yield w.when_key() + # We don't print a "waiting" message for get_unverified_key() here, + # even though we do that in cmd_receive.py, because it's not at all + # surprising to we waiting here for a long time. We'll sit in + # get_unverified_key() until the receiver has typed in the code and + # their PAKE message makes it to us. + yield w.get_unverified_key() - # TODO: don't stall on w.verify() unless they want it + # TODO: don't stall on w.get_verifier() unless they want it def on_slow_connection(): print(u"Key established, waiting for confirmation...", file=args.stderr) @@ -132,13 +133,13 @@ class Sender: try: # The usual sender-chooses-code sequence means the receiver's # PAKE should be followed immediately by their VERSION, so - # w.when_verified() should fire right away. However if we're + # w.get_verifier() should fire right away. However if we're # using the offline-codes sequence, and the receiver typed in # their code first, and then they went offline, we might be # sitting here for a while, so printing the "waiting" message # seems like a good idea. It might even be appropriate to give up # after a while. - verifier_bytes = yield w.when_verified() # might WrongPasswordError + verifier_bytes = yield w.get_verifier() # might WrongPasswordError finally: if not notify.called: notify.cancel() @@ -162,7 +163,7 @@ class Sender: } self._send_data({u"transit": sender_transit}, w) - # TODO: move this down below w.get() + # TODO: move this down below w.get_message() transit_key = w.derive_key(APPID+"/transit-key", ts.TRANSIT_KEY_LENGTH) ts.set_transit_key(transit_key) @@ -174,13 +175,13 @@ class Sender: while True: try: - them_d_bytes = yield w.when_received() + them_d_bytes = yield w.get_message() except WormholeClosedError: if done: returnValue(None) raise TransferError("unexpected close") - # TODO: when_received() fired, so now it's safe to use - # w.derive_key() + # TODO: get_message() fired, so get_verifier must have fired, so + # now it's safe to use w.derive_key() them_d = bytes_to_dict(them_d_bytes) #print("GOT", them_d) recognized = False @@ -209,7 +210,7 @@ class Sender: if ok.lower() == "no": err = "sender rejected verification check, abandoned transfer" reject_data = dict_to_bytes({"error": err}) - w.send(reject_data) + w.send_message(reject_data) raise TransferError(err) def _handle_transit(self, receiver_transit): diff --git a/src/wormhole/cli/welcome.py b/src/wormhole/cli/welcome.py index 50a60a4..93ac1cb 100644 --- a/src/wormhole/cli/welcome.py +++ b/src/wormhole/cli/welcome.py @@ -1,24 +1,18 @@ from __future__ import print_function, absolute_import, unicode_literals -import sys -from ..wormhole import _WelcomeHandler -class CLIWelcomeHandler(_WelcomeHandler): - def __init__(self, url, cli_version, stderr=sys.stderr): - _WelcomeHandler.__init__(self, url, stderr) - self._current_version = cli_version - self._version_warning_displayed = False - - def handle_welcome(self, welcome): - # Only warn if we're running a release version (e.g. 0.0.6, not - # 0.0.6+DISTANCE.gHASH). Only warn once. - if ("current_cli_version" in welcome - and "+" not in self._current_version - and not self._version_warning_displayed - and welcome["current_cli_version"] != self._current_version): - print("Warning: errors may occur unless both sides are running the same version", file=self.stderr) - print("Server claims %s is current, but ours is %s" - % (welcome["current_cli_version"], self._current_version), - file=self.stderr) - self._version_warning_displayed = True - _WelcomeHandler.handle_welcome(self, welcome) +def handle_welcome(welcome, relay_url, my_version, stderr): + if "motd" in welcome: + motd_lines = welcome["motd"].splitlines() + motd_formatted = "\n ".join(motd_lines) + print("Server (at %s) says:\n %s" % (relay_url, motd_formatted), + file=stderr) + # Only warn if we're running a release version (e.g. 0.0.6, not + # 0.0.6+DISTANCE.gHASH). Only warn once. + if ("current_cli_version" in welcome + and "+" not in my_version + and welcome["current_cli_version"] != my_version): + print("Warning: errors may occur unless both sides are running the same version", file=stderr) + print("Server claims %s is current, but ours is %s" + % (welcome["current_cli_version"], my_version), + file=stderr) diff --git a/src/wormhole/test/test_cli.py b/src/wormhole/test/test_cli.py index 7aba918..71393d1 100644 --- a/src/wormhole/test/test_cli.py +++ b/src/wormhole/test/test_cli.py @@ -433,9 +433,16 @@ class PregeneratedCode(ServerBase, ScriptsBase, unittest.TestCase): receive_d = cmd_receive.receive(recv_cfg) else: KEY_TIMER = 0 if mode == "slow-sender-text" else 1.0 + rxw = [] with mock.patch.object(cmd_receive, "KEY_TIMER", KEY_TIMER): send_d = cmd_send.send(send_cfg) - receive_d = cmd_receive.receive(recv_cfg) + receive_d = cmd_receive.receive(recv_cfg, + _debug_stash_wormhole=rxw) + # we need to keep KEY_TIMER patched until the receiver + # gets far enough to start the timer, which happens after + # the code is set + if mode == "slow-sender-text": + yield rxw[0].get_unverified_key() # The sender might fail, leaving the receiver hanging, or vice # versa. Make sure we don't wait on one side exclusively @@ -832,26 +839,26 @@ class NotWelcome(ServerBase, unittest.TestCase): class Cleanup(ServerBase, unittest.TestCase): - def setUp(self): - d = super(Cleanup, self).setUp() - self.cfg = cfg = config("send") + def make_config(self): + cfg = config("send") # common options for all tests in this suite cfg.hide_progress = True cfg.relay_url = self.relayurl cfg.transit_helper = "" cfg.stdout = io.StringIO() cfg.stderr = io.StringIO() - return d + return cfg @inlineCallbacks @mock.patch('sys.stdout') def test_text(self, stdout): # the rendezvous channel should be deleted after success - self.cfg.text = "hello" - self.cfg.code = "1-abc" + cfg = self.make_config() + cfg.text = "hello" + cfg.code = "1-abc" - send_d = cmd_send.send(self.cfg) - receive_d = cmd_receive.receive(self.cfg) + send_d = cmd_send.send(cfg) + receive_d = cmd_receive.receive(cfg) yield send_d yield receive_d @@ -863,12 +870,14 @@ class Cleanup(ServerBase, unittest.TestCase): def test_text_wrong_password(self): # if the password was wrong, the rendezvous channel should still be # deleted - self.cfg.text = "secret message" - self.cfg.code = "1-abc" - send_d = cmd_send.send(self.cfg) + send_cfg = self.make_config() + send_cfg.text = "secret message" + send_cfg.code = "1-abc" + send_d = cmd_send.send(send_cfg) - self.cfg.code = "1-WRONG" - receive_d = cmd_receive.receive(self.cfg) + rx_cfg = self.make_config() + rx_cfg.code = "1-WRONG" + receive_d = cmd_receive.receive(rx_cfg) # both sides should be capable of detecting the mismatch yield self.assertFailure(send_d, WrongPasswordError) @@ -951,12 +960,9 @@ class AppID(ServerBase, unittest.TestCase): self.assertEqual(used[0]["app_id"], u"appid2") class Welcome(unittest.TestCase): - def do(self, welcome_message, my_version="2.0", twice=False): + def do(self, welcome_message, my_version="2.0"): stderr = io.StringIO() - h = welcome.CLIWelcomeHandler("url", my_version, stderr) - h.handle_welcome(welcome_message) - if twice: - h.handle_welcome(welcome_message) + welcome.handle_welcome(welcome_message, "url", my_version, stderr) return stderr.getvalue() def test_empty(self): @@ -973,15 +979,6 @@ class Welcome(unittest.TestCase): "Server claims 3.0 is current, but ours is 2.0\n") self.assertEqual(stderr, expected) - def test_version_old_twice(self): - stderr = self.do({"current_cli_version": "3.0"}, twice=True) - # the handler should only emit the version warning once, even if we - # get multiple Welcome messages (which could happen if we lose the - # connection and then reconnect) - expected = ("Warning: errors may occur unless both sides are running the same version\n" + - "Server claims 3.0 is current, but ours is 2.0\n") - self.assertEqual(stderr, expected) - def test_version_unreleased(self): stderr = self.do({"current_cli_version": "3.0"}, my_version="2.5+middle.something") diff --git a/src/wormhole/test/test_machines.py b/src/wormhole/test/test_machines.py index 233524e..be6166c 100644 --- a/src/wormhole/test/test_machines.py +++ b/src/wormhole/test/test_machines.py @@ -1152,15 +1152,15 @@ class Boss(unittest.TestCase): def build(self): events = [] wormhole = Dummy("w", events, None, - "got_code", "got_key", "got_verifier", "got_version", + "got_welcome", + "got_code", "got_key", "got_verifier", "got_versions", "received", "closed") - self._welcome_handler = mock.Mock() versions = {"app": "version1"} reactor = None journal = ImmediateJournal() tor_manager = None b = MockBoss(wormhole, "side", "url", "appid", versions, - self._welcome_handler, reactor, journal, tor_manager, + reactor, journal, tor_manager, timing.DebugTiming()) b._T = Dummy("t", events, ITerminator, "close") b._S = Dummy("s", events, ISend, "send") @@ -1179,8 +1179,11 @@ class Boss(unittest.TestCase): self.assertEqual(events, [("w.got_code", "1-code")]) events[:] = [] - b.rx_welcome("welcome") - self.assertEqual(self._welcome_handler.mock_calls, [mock.call("welcome")]) + welcome = {"howdy": "how are ya"} + b.rx_welcome(welcome) + self.assertEqual(events, [("w.got_welcome", welcome), + ]) + events[:] = [] # pretend a peer message was correctly decrypted b.got_key(b"key") @@ -1190,7 +1193,7 @@ class Boss(unittest.TestCase): b.got_message("0", b"msg1") self.assertEqual(events, [("w.got_key", b"key"), ("w.got_verifier", b"verifier"), - ("w.got_version", {}), + ("w.got_versions", {}), ("w.received", b"msg1"), ]) events[:] = [] @@ -1206,6 +1209,12 @@ class Boss(unittest.TestCase): b.closed() self.assertEqual(events, [("w.closed", "happy")]) + def test_unwelcome(self): + b, events = self.build() + unwelcome = {"error": "go away"} + b.rx_welcome(unwelcome) + self.assertEqual(events, [("t.close", "unwelcome")]) + def test_lonely(self): b, events = self.build() b.set_code("1-code") diff --git a/src/wormhole/test/test_wormhole.py b/src/wormhole/test/test_wormhole.py index 8c99081..dd826fe 100644 --- a/src/wormhole/test/test_wormhole.py +++ b/src/wormhole/test/test_wormhole.py @@ -12,23 +12,6 @@ from ..errors import (WrongPasswordError, APPID = "appid" -class Welcome(unittest.TestCase): - def test_tolerate_no_current_version(self): - w = wormhole._WelcomeHandler("relay_url") - w.handle_welcome({}) - - def test_print_motd(self): - stderr = io.StringIO() - w = wormhole._WelcomeHandler("relay_url", stderr=stderr) - w.handle_welcome({"motd": "message of\nthe day"}) - self.assertEqual(stderr.getvalue(), - "Server (at relay_url) says:\n message of\n the day\n") - # motd can be displayed multiple times - w.handle_welcome({"motd": "second message"}) - self.assertEqual(stderr.getvalue(), - ("Server (at relay_url) says:\n message of\n the day\n" - "Server (at relay_url) says:\n second message\n")) - # event orderings to exercise: # # * normal sender: set_code, send_phase1, connected, claimed, learn_msg2, @@ -42,21 +25,24 @@ class Welcome(unittest.TestCase): class Delegate: def __init__(self): + self.welcome = None self.code = None self.key = None self.verifier = None - self.version = None + self.versions = None self.messages = [] self.closed = None - def wormhole_code(self, code): + def wormhole_got_welcome(self, welcome): + self.welcome = welcome + def wormhole_got_code(self, code): self.code = code - def wormhole_key(self, key): + def wormhole_got_unverified_key(self, key): self.key = key - def wormhole_verified(self, verifier): + def wormhole_got_verifier(self, verifier): self.verifier = verifier - def wormhole_version(self, version): - self.version = version - def wormhole_received(self, data): + def wormhole_got_versions(self, versions): + self.versions = versions + def wormhole_got_message(self, data): self.messages.append(data) def wormhole_closed(self, result): self.closed = result @@ -76,12 +62,12 @@ class Delegated(ServerBase, unittest.TestCase): w2.set_code(dg.code) yield poll_until(lambda: dg.key is not None) yield poll_until(lambda: dg.verifier is not None) - yield poll_until(lambda: dg.version is not None) + yield poll_until(lambda: dg.versions is not None) - w1.send(b"ping") - got = yield w2.when_received() + w1.send_message(b"ping") + got = yield w2.get_message() self.assertEqual(got, b"ping") - w2.send(b"pong") + w2.send_message(b"pong") yield poll_until(lambda: dg.messages) self.assertEqual(dg.messages[0], b"pong") @@ -124,7 +110,7 @@ class Wormholes(ServerBase, unittest.TestCase): def test_allocate_default(self): w1 = wormhole.create(APPID, self.relayurl, reactor) w1.allocate_code() - code = yield w1.when_code() + code = yield w1.get_code() mo = re.search(r"^\d+-\w+-\w+$", code) self.assert_(mo, code) # w.close() fails because we closed before connecting @@ -134,7 +120,7 @@ class Wormholes(ServerBase, unittest.TestCase): def test_allocate_more_words(self): w1 = wormhole.create(APPID, self.relayurl, reactor) w1.allocate_code(3) - code = yield w1.when_code() + code = yield w1.get_code() mo = re.search(r"^\d+-\w+-\w+-\w+$", code) self.assert_(mo, code) yield self.assertFailure(w1.close(), LonelyError) @@ -149,11 +135,11 @@ class Wormholes(ServerBase, unittest.TestCase): w2 = wormhole.create(APPID, self.relayurl, reactor) #w2.debug_set_trace(" W2") w1.allocate_code() - code = yield w1.when_code() + code = yield w1.get_code() w2.set_code(code) - yield w1.when_key() - yield w2.when_key() + yield w1.get_unverified_key() + yield w2.get_unverified_key() key1 = w1.derive_key("purpose", 16) self.assertEqual(len(key1), 16) @@ -163,29 +149,29 @@ class Wormholes(ServerBase, unittest.TestCase): with self.assertRaises(TypeError): w1.derive_key(12345, 16) - verifier1 = yield w1.when_verified() - verifier2 = yield w2.when_verified() + verifier1 = yield w1.get_verifier() + verifier2 = yield w2.get_verifier() self.assertEqual(verifier1, verifier2) - self.successResultOf(w1.when_key()) - self.successResultOf(w2.when_key()) + self.successResultOf(w1.get_unverified_key()) + self.successResultOf(w2.get_unverified_key()) - version1 = yield w1.when_version() - version2 = yield w2.when_version() + versions1 = yield w1.get_versions() + versions2 = yield w2.get_versions() # app-versions are exercised properly in test_versions, this just # tests the defaults - self.assertEqual(version1, {}) - self.assertEqual(version2, {}) + self.assertEqual(versions1, {}) + self.assertEqual(versions2, {}) - w1.send(b"data1") - w2.send(b"data2") - dataX = yield w1.when_received() - dataY = yield w2.when_received() + w1.send_message(b"data1") + w2.send_message(b"data2") + dataX = yield w1.get_message() + dataY = yield w2.get_message() self.assertEqual(dataX, b"data2") self.assertEqual(dataY, b"data1") - version1_again = yield w1.when_version() - self.assertEqual(version1, version1_again) + versions1_again = yield w1.get_versions() + self.assertEqual(versions1, versions1_again) c1 = yield w1.close() self.assertEqual(c1, "happy") @@ -193,19 +179,19 @@ class Wormholes(ServerBase, unittest.TestCase): self.assertEqual(c2, "happy") @inlineCallbacks - def test_when_code_early(self): + def test_get_code_early(self): w1 = wormhole.create(APPID, self.relayurl, reactor) - d = w1.when_code() + d = w1.get_code() w1.set_code("1-abc") code = self.successResultOf(d) self.assertEqual(code, "1-abc") yield self.assertFailure(w1.close(), LonelyError) @inlineCallbacks - def test_when_code_late(self): + def test_get_code_late(self): w1 = wormhole.create(APPID, self.relayurl, reactor) w1.set_code("1-abc") - d = w1.when_code() + d = w1.get_code() code = self.successResultOf(d) self.assertEqual(code, "1-abc") yield self.assertFailure(w1.close(), LonelyError) @@ -218,12 +204,12 @@ class Wormholes(ServerBase, unittest.TestCase): w1 = wormhole.create(APPID, self.relayurl, reactor) w2 = wormhole.create(APPID, self.relayurl, reactor) w1.allocate_code() - code = yield w1.when_code() + code = yield w1.get_code() w2.set_code(code) - w1.send(b"data") - w2.send(b"data") - dataX = yield w1.when_received() - dataY = yield w2.when_received() + w1.send_message(b"data") + w2.send_message(b"data") + dataX = yield w1.get_message() + dataY = yield w2.get_message() self.assertEqual(dataX, b"data") self.assertEqual(dataY, b"data") yield w1.close() @@ -234,13 +220,13 @@ class Wormholes(ServerBase, unittest.TestCase): w1 = wormhole.create(APPID, self.relayurl, reactor) w2 = wormhole.create(APPID, self.relayurl, reactor) w1.allocate_code() - code = yield w1.when_code() + code = yield w1.get_code() w2.set_code(code) - w1.send(b"data1") - dataY = yield w2.when_received() + w1.send_message(b"data1") + dataY = yield w2.get_message() self.assertEqual(dataY, b"data1") - d = w1.when_received() - w2.send(b"data2") + d = w1.get_message() + w2.send_message(b"data2") dataX = yield d self.assertEqual(dataX, b"data2") yield w1.close() @@ -251,10 +237,10 @@ class Wormholes(ServerBase, unittest.TestCase): w1 = wormhole.create(APPID, self.relayurl, reactor) w2 = wormhole.create(APPID, self.relayurl, reactor) w1.allocate_code() - code = yield w1.when_code() + code = yield w1.get_code() w2.set_code(code) - w1.send(b"data1") - dataY = yield w2.when_received() + w1.send_message(b"data1") + dataY = yield w2.get_message() self.assertEqual(dataY, b"data1") yield w1.close() yield w2.close() @@ -262,9 +248,9 @@ class Wormholes(ServerBase, unittest.TestCase): @inlineCallbacks def test_early(self): w1 = wormhole.create(APPID, self.relayurl, reactor) - w1.send(b"data1") + w1.send_message(b"data1") w2 = wormhole.create(APPID, self.relayurl, reactor) - d = w2.when_received() + d = w2.get_message() w1.set_code("123-abc-def") w2.set_code("123-abc-def") dataY = yield d @@ -278,8 +264,8 @@ class Wormholes(ServerBase, unittest.TestCase): 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.when_received(), w2.when_received()) + w1.send_message(b"data1"), w2.send_message(b"data2") + dl = yield self.doBoth(w1.get_message(), w2.get_message()) (dataX, dataY) = dl self.assertEqual(dataX, b"data2") self.assertEqual(dataY, b"data1") @@ -300,8 +286,8 @@ class Wormholes(ServerBase, unittest.TestCase): yield poll_until(lambda: w2._boss._K._debug_pake_stashed) h.choose_words("purple-elephant") - w1.send(b"data1"), w2.send(b"data2") - dl = yield self.doBoth(w1.when_received(), w2.when_received()) + w1.send_message(b"data1"), w2.send_message(b"data2") + dl = yield self.doBoth(w1.get_message(), w2.get_message()) (dataX, dataY) = dl self.assertEqual(dataX, b"data2") self.assertEqual(dataY, b"data1") @@ -315,13 +301,13 @@ class Wormholes(ServerBase, unittest.TestCase): 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.when_received(), w2.when_received()) + w1.send_message(b"data1"), w2.send_message(b"data2") + w1.send_message(b"data3"), w2.send_message(b"data4") + dl = yield self.doBoth(w1.get_message(), w2.get_message()) (dataX, dataY) = dl self.assertEqual(dataX, b"data2") self.assertEqual(dataY, b"data1") - dl = yield self.doBoth(w1.when_received(), w2.when_received()) + dl = yield self.doBoth(w1.get_message(), w2.get_message()) (dataX, dataY) = dl self.assertEqual(dataX, b"data4") self.assertEqual(dataY, b"data3") @@ -337,19 +323,19 @@ class Wormholes(ServerBase, unittest.TestCase): w2.set_code("123-foo") # let it connect and become HAPPY - yield w1.when_version() - yield w2.when_version() + yield w1.get_versions() + yield w2.get_versions() yield w1.close() yield w2.close() # once closed, all Deferred-yielding API calls get an immediate error - f = self.failureResultOf(w1.when_code(), WormholeClosed) + f = self.failureResultOf(w1.get_code(), WormholeClosed) self.assertEqual(f.value.args[0], "happy") - self.failureResultOf(w1.when_key(), WormholeClosed) - self.failureResultOf(w1.when_verified(), WormholeClosed) - self.failureResultOf(w1.when_version(), WormholeClosed) - self.failureResultOf(w1.when_received(), WormholeClosed) + self.failureResultOf(w1.get_unverified_key(), WormholeClosed) + self.failureResultOf(w1.get_verifier(), WormholeClosed) + self.failureResultOf(w1.get_versions(), WormholeClosed) + self.failureResultOf(w1.get_message(), WormholeClosed) @inlineCallbacks @@ -357,20 +343,20 @@ class Wormholes(ServerBase, unittest.TestCase): w1 = wormhole.create(APPID, self.relayurl, reactor) w2 = wormhole.create(APPID, self.relayurl, reactor) w1.allocate_code() - code = yield w1.when_code() + code = yield w1.get_code() w2.set_code(code+"not") - code2 = yield w2.when_code() + code2 = yield w2.get_code() self.assertNotEqual(code, code2) # That's enough to allow both sides to discover the mismatch, but # only after the confirmation message gets through. API calls that # don't wait will appear to work until the mismatched confirmation # message arrives. - w1.send(b"should still work") - w2.send(b"should still work") + w1.send_message(b"should still work") + w2.send_message(b"should still work") - key2 = yield w2.when_key() # should work + key2 = yield w2.get_unverified_key() # should work # w2 has just received w1.PAKE, and is about to send w2.VERSION - key1 = yield w1.when_key() # should work + key1 = yield w1.get_unverified_key() # should work # w1 has just received w2.PAKE, and is about to send w1.VERSION, and # then will receive w2.VERSION. When it sees w2.VERSION, it will # learn about the WrongPasswordError. @@ -378,54 +364,54 @@ class Wormholes(ServerBase, unittest.TestCase): # API calls that wait (i.e. get) will errback. We collect all these # Deferreds early to exercise the wait-then-fail path - d1_verified = w1.when_verified() - d1_version = w1.when_version() - d1_received = w1.when_received() - d2_verified = w2.when_verified() - d2_version = w2.when_version() - d2_received = w2.when_received() + d1_verified = w1.get_verifier() + d1_versions = w1.get_versions() + d1_received = w1.get_message() + d2_verified = w2.get_verifier() + d2_versions = w2.get_versions() + d2_received = w2.get_message() # wait for each side to notice the failure - yield self.assertFailure(w1.when_verified(), WrongPasswordError) - yield self.assertFailure(w2.when_verified(), WrongPasswordError) + yield self.assertFailure(w1.get_verifier(), WrongPasswordError) + yield self.assertFailure(w2.get_verifier(), WrongPasswordError) # and then wait for the rest of the loops to fire. if we had+used # eventual-send, this wouldn't be a problem yield pause_one_tick() # now all the rest should have fired already self.failureResultOf(d1_verified, WrongPasswordError) - self.failureResultOf(d1_version, WrongPasswordError) + self.failureResultOf(d1_versions, WrongPasswordError) self.failureResultOf(d1_received, WrongPasswordError) self.failureResultOf(d2_verified, WrongPasswordError) - self.failureResultOf(d2_version, WrongPasswordError) + self.failureResultOf(d2_versions, WrongPasswordError) self.failureResultOf(d2_received, WrongPasswordError) # and at this point, with the failure safely noticed by both sides, - # new when_key() calls should signal the failure, even before we - # close + # new get_unverified_key() calls should signal the failure, even + # before we close # any new calls in the error state should immediately fail - self.failureResultOf(w1.when_key(), WrongPasswordError) - self.failureResultOf(w1.when_verified(), WrongPasswordError) - self.failureResultOf(w1.when_version(), WrongPasswordError) - self.failureResultOf(w1.when_received(), WrongPasswordError) - self.failureResultOf(w2.when_key(), WrongPasswordError) - self.failureResultOf(w2.when_verified(), WrongPasswordError) - self.failureResultOf(w2.when_version(), WrongPasswordError) - self.failureResultOf(w2.when_received(), WrongPasswordError) + self.failureResultOf(w1.get_unverified_key(), WrongPasswordError) + self.failureResultOf(w1.get_verifier(), WrongPasswordError) + self.failureResultOf(w1.get_versions(), WrongPasswordError) + self.failureResultOf(w1.get_message(), WrongPasswordError) + self.failureResultOf(w2.get_unverified_key(), WrongPasswordError) + self.failureResultOf(w2.get_verifier(), WrongPasswordError) + self.failureResultOf(w2.get_versions(), WrongPasswordError) + self.failureResultOf(w2.get_message(), WrongPasswordError) yield self.assertFailure(w1.close(), WrongPasswordError) yield self.assertFailure(w2.close(), WrongPasswordError) # API calls should still get the error, not WormholeClosed - self.failureResultOf(w1.when_key(), WrongPasswordError) - self.failureResultOf(w1.when_verified(), WrongPasswordError) - self.failureResultOf(w1.when_version(), WrongPasswordError) - self.failureResultOf(w1.when_received(), WrongPasswordError) - self.failureResultOf(w2.when_key(), WrongPasswordError) - self.failureResultOf(w2.when_verified(), WrongPasswordError) - self.failureResultOf(w2.when_version(), WrongPasswordError) - self.failureResultOf(w2.when_received(), WrongPasswordError) + self.failureResultOf(w1.get_unverified_key(), WrongPasswordError) + self.failureResultOf(w1.get_verifier(), WrongPasswordError) + self.failureResultOf(w1.get_versions(), WrongPasswordError) + self.failureResultOf(w1.get_message(), WrongPasswordError) + self.failureResultOf(w2.get_unverified_key(), WrongPasswordError) + self.failureResultOf(w2.get_verifier(), WrongPasswordError) + self.failureResultOf(w2.get_versions(), WrongPasswordError) + self.failureResultOf(w2.get_message(), WrongPasswordError) @inlineCallbacks def test_wrong_password_with_spaces(self): @@ -442,21 +428,21 @@ class Wormholes(ServerBase, unittest.TestCase): w1 = wormhole.create(APPID, self.relayurl, reactor) w2 = wormhole.create(APPID, self.relayurl, reactor) w1.allocate_code() - code = yield w1.when_code() + code = yield w1.get_code() w2.set_code(code) - v1 = yield w1.when_verified() # early - v2 = yield w2.when_verified() + v1 = yield w1.get_verifier() # early + v2 = yield w2.get_verifier() self.failUnlessEqual(type(v1), type(b"")) self.failUnlessEqual(v1, v2) - w1.send(b"data1") - w2.send(b"data2") - dataX = yield w1.when_received() - dataY = yield w2.when_received() + w1.send_message(b"data1") + w2.send_message(b"data2") + dataX = yield w1.get_message() + dataY = yield w2.get_message() self.assertEqual(dataX, b"data2") self.assertEqual(dataY, b"data1") - # calling when_verified() this late should fire right away - v1_late = self.successResultOf(w2.when_verified()) + # calling get_verifier() this late should fire right away + v1_late = self.successResultOf(w2.get_verifier()) self.assertEqual(v1_late, v1) yield w1.close() @@ -470,11 +456,11 @@ class Wormholes(ServerBase, unittest.TestCase): w2 = wormhole.create(APPID, self.relayurl, reactor, versions={"w2": 456}) w1.allocate_code() - code = yield w1.when_code() + code = yield w1.get_code() w2.set_code(code) - w1_versions = yield w2.when_version() + w1_versions = yield w2.get_versions() self.assertEqual(w1_versions, {"w1": 123}) - w2_versions = yield w1.when_version() + w2_versions = yield w1.get_versions() self.assertEqual(w2_versions, {"w2": 456}) yield w1.close() yield w2.close() @@ -496,8 +482,8 @@ class Wormholes(ServerBase, unittest.TestCase): 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.when_received(), w2.when_received()) + w1.send_message(b"data1"), w2.send_message(b"data2") + dl = yield self.doBoth(w1.get_message(), w2.get_message()) (dataX, dataY) = dl self.assertEqual(dataX, b"data2") self.assertEqual(dataY, b"data1") @@ -537,7 +523,7 @@ class Errors(ServerBase, unittest.TestCase): def test_allocate_and_set_code(self): w = wormhole.create(APPID, self.relayurl, reactor) w.allocate_code() - yield w.when_code() + yield w.get_code() with self.assertRaises(OnlyOneCodeError): w.set_code("123-nope") yield self.assertFailure(w.close(), LonelyError) @@ -550,8 +536,8 @@ class Reconnection(ServerBase, unittest.TestCase): w1._boss._RC._debug_record_inbound_f = w1_in.append #w1.debug_set_trace("W1") w1.allocate_code() - code = yield w1.when_code() - w1.send(b"data1") # will be queued until wormhole is established + code = yield w1.get_code() + w1.send_message(b"data1") # queued until wormhole is established # now wait until we've deposited all our messages on the server def seen_our_pake(): @@ -577,11 +563,11 @@ class Reconnection(ServerBase, unittest.TestCase): #w2.debug_set_trace(" W2") w2.set_code(code) - dataY = yield w2.when_received() + dataY = yield w2.get_message() self.assertEqual(dataY, b"data1") - w2.send(b"data2") - dataX = yield w1.when_received() + w2.send_message(b"data2") + dataX = yield w1.get_message() self.assertEqual(dataX, b"data2") c1 = yield w1.close() diff --git a/src/wormhole/wormhole.py b/src/wormhole/wormhole.py index 8c7150f..4100f9b 100644 --- a/src/wormhole/wormhole.py +++ b/src/wormhole/wormhole.py @@ -15,18 +15,18 @@ from .util import to_bytes # We can provide different APIs to different apps: # * Deferreds -# w.when_code().addCallback(print_code) -# w.send(data) -# w.when_received().addCallback(got_data) +# w.get_code().addCallback(print_code) +# w.send_message(data) +# w.get_message().addCallback(got_data) # w.close().addCallback(closed) # * delegate callbacks (better for journaled environments) # w = wormhole(delegate=app) -# w.send(data) +# w.send_message(data) # app.wormhole_got_code(code) # app.wormhole_got_verifier(verifier) -# app.wormhole_got_version(versions) -# app.wormhole_receive(data) +# app.wormhole_got_versions(versions) +# app.wormhole_got_message(data) # w.close() # app.wormhole_closed() # @@ -34,18 +34,6 @@ from .util import to_bytes # wormhole(delegate=app, delegate_prefix="wormhole_", # delegate_args=(args, kwargs)) -class _WelcomeHandler: - def __init__(self, url, stderr=sys.stderr): - self.relay_url = url - self.stderr = stderr - - def handle_welcome(self, welcome): - if "motd" in welcome: - motd_lines = welcome["motd"].splitlines() - motd_formatted = "\n ".join(motd_lines) - print("Server (at %s) says:\n %s" % - (self.relay_url, motd_formatted), file=self.stderr) - @attrs @implementer(IWormhole) class _DelegatedWormhole(object): @@ -72,7 +60,7 @@ class _DelegatedWormhole(object): ## } ## return s - def send(self, plaintext): + def send_message(self, plaintext): self._boss.send(plaintext) def derive_key(self, purpose, length): @@ -94,23 +82,27 @@ class _DelegatedWormhole(object): self._boss._set_trace(client_name, which, file) # from below + def got_welcome(self, welcome): + self._delegate.wormhole_got_welcome(welcome) def got_code(self, code): - self._delegate.wormhole_code(code) + self._delegate.wormhole_got_code(code) def got_key(self, key): - self._delegate.wormhole_key(key) + self._delegate.wormhole_got_unverified_key(key) self._key = key # for derive_key() def got_verifier(self, verifier): - self._delegate.wormhole_verified(verifier) - def got_version(self, versions): - self._delegate.wormhole_version(versions) + self._delegate.wormhole_got_verifier(verifier) + def got_versions(self, versions): + self._delegate.wormhole_got_versions(versions) def received(self, plaintext): - self._delegate.wormhole_received(plaintext) + self._delegate.wormhole_got_message(plaintext) def closed(self, result): self._delegate.wormhole_closed(result) @implementer(IWormhole) class _DeferredWormhole(object): def __init__(self): + self._welcome = None + self._welcome_observers = [] self._code = None self._code_observers = [] self._key = None @@ -129,7 +121,7 @@ class _DeferredWormhole(object): self._boss = boss # from above - def when_code(self): + def get_code(self): # TODO: consider throwing error unless one of allocate/set/input_code # was called first. It's legit to grab the Deferred before triggering # the process that will cause it to fire, but forbidding that @@ -143,7 +135,16 @@ class _DeferredWormhole(object): self._code_observers.append(d) return d - def when_key(self): + def get_welcome(self): + if self._observer_result is not None: + return defer.fail(self._observer_result) + if self._welcome is not None: + return defer.succeed(self._welcome) + d = defer.Deferred() + self._welcome_observers.append(d) + return d + + def get_unverified_key(self): if self._observer_result is not None: return defer.fail(self._observer_result) if self._key is not None: @@ -152,7 +153,7 @@ class _DeferredWormhole(object): self._key_observers.append(d) return d - def when_verified(self): + def get_verifier(self): if self._observer_result is not None: return defer.fail(self._observer_result) if self._verifier is not None: @@ -161,7 +162,7 @@ class _DeferredWormhole(object): self._verifier_observers.append(d) return d - def when_version(self): + def get_versions(self): if self._observer_result is not None: return defer.fail(self._observer_result) if self._versions is not None: @@ -170,7 +171,7 @@ class _DeferredWormhole(object): self._version_observers.append(d) return d - def when_received(self): + def get_message(self): if self._observer_result is not None: return defer.fail(self._observer_result) if self._received_data: @@ -187,7 +188,8 @@ class _DeferredWormhole(object): self._boss.set_code(code) # no .serialize in Deferred-mode - def send(self, plaintext): + + def send_message(self, plaintext): self._boss.send(plaintext) def derive_key(self, purpose, length): @@ -217,6 +219,11 @@ class _DeferredWormhole(object): self._boss._set_trace(client_name, which, file) # from below + def got_welcome(self, welcome): + self._welcome = welcome + for d in self._welcome_observers: + d.callback(welcome) + self._welcome_observers[:] = [] def got_code(self, code): self._code = code for d in self._code_observers: @@ -227,12 +234,13 @@ class _DeferredWormhole(object): for d in self._key_observers: d.callback(key) self._key_observers[:] = [] + def got_verifier(self, verifier): self._verifier = verifier for d in self._verifier_observers: d.callback(verifier) self._verifier_observers[:] = [] - def got_version(self, versions): + def got_versions(self, versions): self._versions = versions for d in self._version_observers: d.callback(versions) @@ -245,7 +253,7 @@ class _DeferredWormhole(object): self._received_data.append(plaintext) def closed(self, result): - #print("closed", result, type(result)) + #print("closed", result, type(result), file=sys.stderr) if isinstance(result, Exception): self._observer_result = self._closed_result = failure.Failure(result) else: @@ -253,6 +261,8 @@ class _DeferredWormhole(object): self._observer_result = WormholeClosed(result) # but w.close() only gets error if we're unhappy self._closed_result = result + for d in self._welcome_observers: + d.errback(self._observer_result) for d in self._code_observers: d.errback(self._observer_result) for d in self._key_observers: @@ -270,13 +280,11 @@ class _DeferredWormhole(object): def create(appid, relay_url, reactor, # use keyword args for everything else versions={}, delegate=None, journal=None, tor_manager=None, - timing=None, welcome_handler=None, + timing=None, stderr=sys.stderr): timing = timing or DebugTiming() side = bytes_to_hexstr(os.urandom(5)) journal = journal or ImmediateJournal() - if not welcome_handler: - welcome_handler = _WelcomeHandler(relay_url).handle_welcome if delegate: w = _DelegatedWormhole(delegate) else: @@ -284,8 +292,7 @@ def create(appid, relay_url, reactor, # use keyword args for everything else 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) + reactor, journal, tor_manager, timing) w._set_boss(b) b.start() return w diff --git a/src/wormhole/xfer_util.py b/src/wormhole/xfer_util.py index 7dcfba9..1de6873 100644 --- a/src/wormhole/xfer_util.py +++ b/src/wormhole/xfer_util.py @@ -41,14 +41,14 @@ def receive(reactor, appid, relay_url, code, wh = wormhole.create(appid, relay_url, reactor, tor_manager=tm) if code is None: wh.allocate_code() - code = yield wh.when_code() + code = yield wh.get_code() else: wh.set_code(code) # we'll call this no matter what, even if you passed in a code -- # maybe it should be only in the 'if' block above? if on_code: on_code(code) - data = yield wh.when_received() + data = yield wh.get_message() data = json.loads(data.decode("utf-8")) offer = data.get('offer', None) if not offer: @@ -58,7 +58,8 @@ def receive(reactor, appid, relay_url, code, msg = None if 'message' in offer: msg = offer['message'] - wh.send(json.dumps({"answer": {"message_ack": "ok"}}).encode("utf-8")) + wh.send_message(json.dumps({"answer": + {"message_ack": "ok"}}).encode("utf-8")) else: raise Exception( @@ -104,20 +105,20 @@ def send(reactor, appid, relay_url, data, code, wh = wormhole.create(appid, relay_url, reactor, tor_manager=tm) if code is None: wh.allocate_code() - code = yield wh.when_code() + code = yield wh.get_code() else: wh.set_code(code) if on_code: on_code(code) - wh.send( + wh.send_message( json.dumps({ "offer": { "message": data } }).encode("utf-8") ) - data = yield wh.when_received() + data = yield wh.get_message() data = json.loads(data.decode("utf-8")) answer = data.get('answer', None) yield wh.close()